M5StackとFirebaseを連携してスマートロックみたいなものを作る！③

# できあがるもの
![search_felica_low_quality.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/d27a4b9d-e7a9-3322-50be-51efb6cae8bf.gif)
ホントはFirebase側も動作している感じのやつを見せられるといいのですが😅

# はじめに
前回はFeliCa RC-S620Sを使ってSuicaやPasmoなどといった電子カードを読み取れるようにしました。
更に、読み取ったmIDをFirebaseのRDB上に登録するところまで作成しました。
今回はFirebase Realtime Database(RDB)のルールを設定し、```QueryFilter```を使って検索機能を設けていきましょう。
検索機能を使えば、ある電子カードに対して認証済みか否かを判定することができます。

[M5StackとFirebaseを連携してスマートロックみたいなものを作る！①](https://qiita.com/bigface00/items/87346c31d12346f85716)
[M5StackとFirebaseを連携してスマートロックみたいなものを作る！②](https://qiita.com/bigface00/items/29511d3a4425da5a499c)
[M5StackとFirebaseを連携してスマートロックみたいなものを作る！③](https://qiita.com/bigface00/items/e92f3a12ed0162c17d84)
[M5StackとFirebaseを連携してスマートロックみたいなものを作る！④](https://qiita.com/bigface00/items/d59ba7ccffce99ce259a)
[M5StackとFirebaseを連携してスマートロックみたいなものを作る！⑤](https://qiita.com/bigface00/items/590834705146e65f4826)


# RDBにおけるルール
Firebase RDBではセキュリティルールを設定することによって、データベースへの読み取り/書き込みを制限することができます。
またルールでは、<b>あるインデックスに対してクエリをサポートするように指定することもできます。</b>
今回はこのクエリのサポートを使うことによって、検索機能を使えるようにします。
参考：[Firebase Realtime Databaseルールについて](https://firebase.google.com/docs/database/security?hl=ja)

## 現在のRDBの状態
現在は"User"というテーブルがあって、各ICカードのレコードが格納されており、レコードには"id"と"time"というフィールドがあります。
参考：[データベースの構造と各名称](https://www.koreyome.com/database-structure-name/)
以下はデータベースの例です。
前回の続きからであれば、似たような形でICカードが登録されているかと思います。
![table_felica.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/763b222f-ec48-a57b-5672-41c0e07db5f8.png)
## 今回設定するルール
今回はFeliCaのリーダーで読み取ったmIDに対して、それに一致するICカードのレコードを取得したいです。
![query_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/fa2d6033-52cf-1422-370d-6a3637a9870c.png)
まずは、ルールを書き換えましょう。
![rdb_rule.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/757706f6-a8bf-fe6f-205f-f82d6da3c097.png)
RDBを開いて、「ルール」から設定することができます。
今回は"id"で検索できるようにしたいので、以下の様にデータベースインデックスの定義をします。

```json:.indexOnを追加
{
    "rules": {
        ".read": true,
        ".write": true,
    "User": {
        ".indexOn":"id"
      }
    }
}
```
本来は.read, .writeは認証されたユーザーのみしかアクセスできないようにしたいですが、今回は個人的な利用のみに限るということで設定は見送ります。
Firebase_ESP32で認証機能あるかどうかはまた調べたいところですね🤔
![openrule.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/46ec2642-9f70-0f96-626d-17012ac1eee0.png)
さて、ルールを設定したら上記の表示が出てくると思うので公開を押しましょう。
これでルールの設定は完了です。

# 検索機能の実装
## 必要なライブラリ
- ArduinoJson

今回はレコード全体をjsonとして取得するためにArduinoJsonを使います。
レコード内の特定のフィールドのみ取得する方法もあるので、レコード全体の取得は絶対に必要というわけではありません。
![arduinojson.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/0aa96a15-ba82-0cc4-5816-4ab7e6cad92f.png)

## コード全体
```c++:全体
#include <Arduino.h>
#include <FirebaseESP32.h>
#include <M5Stack.h>
#include "RCS620S.h"
#include <time.h>
#include <ArduinoJson.h>

// WiFi, Firebaseの設定情報
// ID and Auth key for Firebase
#define FIREBASE_HOST "Your Firebase hostname"
#define FIREBASE_AUTH "Your Firebase authkey"
#define WIFI_SSID "Your WiFi SSID"
#define WIFI_PASSWORD "Your WiFi Password"

// Instantiate related with Firebase
// Firebaseのインスタンスを生成
FirebaseData firebaseData;
// FirebaseJsonのインスタンスを生成
FirebaseJson json;
// QueryFilterのインスタンスを生成
QueryFilter query;

// FeliCaの設定情報
// For FeliCa
#define COMMAND_TIMEOUT 400
#define POLLING_INTERVAL 500
RCS620S rcs620s;

// フェーズの設定
// Phase valuable
bool _authenticatePhase = false;

void setup()
{
  M5.begin();
  Serial.begin(115200);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  int _cursorX = 0;
  M5.Lcd.setTextFont(4);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setCursor(0, 0);
  // WiFiに接続
  // Connect to WiFi
  M5.Lcd.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED)
  {
    Serial.print(".");
    M5.Lcd.setCursor(0 + 5 * _cursorX, 30);
    M5.Lcd.print(".");
    delay(300);
    _cursorX++;
    if (_cursorX > 320)
    {
      _cursorX = 0;
    }
  }
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 0);
  M5.Lcd.print("Connected with IP:");
  M5.Lcd.print(WiFi.localIP());
  delay(1000);

  // Firebase関連
  // Related with Firebase
  Firebase.begin(FIREBASE_HOST, FIREBASE_AUTH);
  Firebase.reconnectWiFi(true);

  // FeliCaへの接続確認
  // Check FeliCa Reader is connected or not
  int ret;
  ret = rcs620s.initDevice();
  while (!ret)
  {
    M5.Lcd.setTextColor(RED);
    ret = rcs620s.initDevice();
    Serial.println("Cannot find the RC-S620S");
    M5.Lcd.setCursor(0, 60);
    M5.Lcd.print("Cannot find the RC-S620S");
    delay(1000);
  }
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setCursor(0, 90);
  M5.Lcd.print("Felica reader is detected");
  delay(1000);
  M5.Lcd.fillScreen(BLACK);

  // どのフィールドから探すかを指定
  // Specify field name which you want to search
  query.orderBy("id");
}

void loop()
{
  int ret;
  rcs620s.timeout = COMMAND_TIMEOUT;
  M5.update();
  M5.Lcd.setTextColor(WHITE);

  // ボタンで機能を変える
  // Push the button to select the function
  if (M5.BtnB.wasPressed())
  {
    _authenticatePhase = true;
    M5.Lcd.fillScreen(BLACK);
  }

  // 通信待ち
  // Polling
  ret = rcs620s.polling();
  // 待機フェーズ
  // Waiting for pressing any key
  if (!_authenticatePhase)
  {
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.print("Press any key");
  }
  // 認証フェーズ
  // Authentication phase
  else
  {
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.print("Waiting for card...");
    String felicaID = "";
    if (ret)
    {
      for (int i = 0; i < 8; i++)
      {
        if (rcs620s.idm[i] / 0x10 == 0)
          felicaID += "0";
        felicaID += String(rcs620s.idm[i], HEX);
      }
    }
    // 検索キーワードの指定
    // 今回はリーダーに置かれたICカードのmIDと一致するidを検索する
    // Specify searching keyword
    // In this time, I want to search "id" which is matched mID of IC Card
    query.equalTo(felicaID);
    if (!felicaID.isEmpty())
    {
      // "User"というテーブルから検索
      // Search from "User" table
      if (Firebase.getJSON(firebaseData, "User", query))
      {
        String key, value = "";
        String time = "";
        int type = 0;
        size_t len = firebaseData.jsonObject().iteratorBegin();
        // ArduinoJson用の変数
        // Valuable for ArduinoJson
        const size_t capacity = 500;
        DynamicJsonDocument recordInfo(capacity);
        // 今回の場合、
        // i=0で検索と一致する全てのフィールド（json）、
        // i=1で1番目のフィールド（今回だとid）、
        // i=2で2番目のフィールド（今回だとtime）が返って来る
        // In this time, i = 0: json which is matched the mID, i = 1: 1st field value of matched mID("id"), i = 2: 2nd field value of matched mID("time")
        for (size_t i = 0; i < len; i++)
        {
          firebaseData.jsonObject().iteratorGet(i, type, key, value);
          // iが0のときはmIDと一致するjsonが返ってくる
          // When i = 0, json which is matched the mID is returned
          if (i == 0)
          {
            // jsonとして取得したい場合は、ArduinoJsonライブラリを使う
            // If you want to get data as json, you should use ArudinoJson library
            DeserializationError error = deserializeJson(recordInfo, value);
            if (error)
            {
              M5.Lcd.fillScreen(BLACK);
              M5.Lcd.setCursor(0, 0);
              M5.Lcd.print(error.c_str());
              delay(5000);
              return;
            }
          }
          // キーを指定して取得することもできる
          // You can get the value by using key name
          if (key == "time")
          {
            time = value;
          }
        }
        // 一致するmIDをjsonファイルから取り出す
        // Extract mID from json
        const char* mID = recordInfo["id"];
        if (len != 0)
        {
          json.iteratorEnd();
          M5.Lcd.fillScreen(BLACK);
          M5.Lcd.setCursor(0, 0);
          M5.Lcd.print("ID:");
          M5.Lcd.println(mID);
          M5.Lcd.print("time:");
          M5.Lcd.println(time);
          delay(3000);
        }
        else
        {
          M5.Lcd.fillScreen(BLACK);
          M5.Lcd.setCursor(0, 0);
          M5.Lcd.print("This card is not registered");
          delay(3000);
        }
      }
      else
      {
        // Firebaseから取得に失敗した場合はエラーを返す
        // Return error if you cannot get RDB from Firebase
        Serial.println(firebaseData.errorReason());
      }
      // 登録が終了したら待機フェーズに戻る
      _authenticatePhase = false;
      M5.Lcd.fillScreen(BLACK);
    }
  }
  rcs620s.rfOff();
  delay(POLLING_INTERVAL);
}
```
### クエリのインスタンスを生成と検索キーの設定
```c++:
...
// Instantiate related with Firebase
// Firebaseのインスタンスを生成
FirebaseData firebaseData;
// FirebaseJsonのインスタンスを生成
FirebaseJson json;
// QueryFilterのインスタンスを生成
QueryFilter query;
...
```
今回はクエリを使った検索を行うのでまずは```QueryFilter```のインスタンスを生成します。

```c++:検索キーの指定
void setup()
{
...
   query.orderBy("id");
...
}
```
"id"の中からFeliCaリーダーで読み取ったmIDと一致する項目を検索するようにします。

### 検索機能
```c++:ボタンで認証フェーズへ入る
...
if (M5.BtnB.wasPressed())
  {
    _authenticatePhase = true;
    M5.Lcd.fillScreen(BLACK);
  }
...
```
今回はM5Stackの真中のボタン（ボタンB）が押されたら検索を実施します。

```c++:検索キーワードの指定
    String felicaID = "";
    if (ret)
    {
      for (int i = 0; i < 8; i++)
      {
        if (rcs620s.idm[i] / 0x10 == 0)
          felicaID += "0";
        felicaID += String(rcs620s.idm[i], HEX);
      }
    }
    // 検索キーワードの指定
    // 今回はリーダーに置かれたICカードのmIDと一致するidを検索する
    // Specify searching keyword
    // In this time, I want to search "id" which is matched mID of IC Card
    query.equalTo(felicaID);
```
前回同様、```felicaID```に読み取ったmIDを挿入します。
そして、```query.equalTo()```に検索キーワードを指定します。今回は```felicaID```ですね。

```c++:queryを元に検索
...
    if (!felicaID.isEmpty())
    {
      // "User"というテーブルから検索
      // Search from "User" table
      if (Firebase.getJSON(firebaseData, "User", query))
      {
        String key, value = "";
        String time = "";
        int type = 0;
        size_t len = firebaseData.jsonObject().iteratorBegin();
        // ArduinoJson用の変数
        // Valuable for ArduinoJson
        const size_t capacity = 500;
        DynamicJsonDocument recordInfo(capacity);
        // 今回の場合、
        // i=0で検索と一致する全てのフィールド（json）、
        // i=1で1番目のフィールド（今回だとid）、
        // i=2で2番目のフィールド（今回だとtime）が返って来る
        // In this time, i = 0: json which is matched the mID, i = 1: 1st record of matched mID("id"), i = 2: 2nd record of matched mID("time")
        for (size_t i = 0; i < len; i++)
        {
          firebaseData.jsonObject().iteratorGet(i, type, key, value);
          // iが0のときはmIDと一致するjsonが返ってくる
          // When i = 0, json which is matched the mID is returned
          if (i == 0)
          {
            // jsonとして取得したい場合は、ArduinoJsonライブラリを使う
            // If you want to get data as json, you should use ArudinoJson library
            DeserializationError error = deserializeJson(recordInfo, value);
            if (error)
            {
              M5.Lcd.fillScreen(BLACK);
              M5.Lcd.setCursor(0, 0);
              M5.Lcd.print(error.c_str());
              delay(5000);
              return;
            }
          }
          // キーを指定して取得することもできる
          // You can get the value by using key name
          if (key == "time")
          {
            time = value;
          }
        }
        // 一致するmIDをjsonファイルから取り出す
        // Extract mID from json
        const char* mID = recordInfo["id"];
...
```
検索にはJSONを取得する```Firebase.getJSON()```を使います。ここで```query```を```Firebase.getJSON()```を渡すことによって、事前に指定したフィールドkey(今回だと"id")の中からキーワード（```felicaID```）と一致するレコードを検索します。
そして、```firebaseData.jsonObject().iteratorGet()```を使うことによって```type```,```key```, ```value```へ順々に値を代入していきます。
```type```は取得したデータのタイプ（Object or Array）、```key```はフィールド名、```value```はフィールド内の値になります。
今回使うのは主に```key```と```value```ですね。
データの順番はコメントに書いてある通り、最初はJSON、次に各フィールドの値になります。
フィールドが多い場合は、JSONとして一括で取得しちゃって必要な値だけkeyを指定して取得してもいいかもですね。
今回は、JSONとして取得する例とkeyで取得する例を記述しています。そのため、ArduinoJson用の変数を設定しています。

```c++:取得したデータの表示
        if (len != 0)
        {
          json.iteratorEnd();
          M5.Lcd.fillScreen(BLACK);
          M5.Lcd.setCursor(0, 0);
          M5.Lcd.print("ID:");
          M5.Lcd.println(mID);
          M5.Lcd.print("time:");
          M5.Lcd.println(time);
          delay(3000);
        }
        else
        {
          M5.Lcd.fillScreen(BLACK);
          M5.Lcd.setCursor(0, 0);
          M5.Lcd.print("This card is not registered");
          delay(3000);
        }
```

もし、一致するレコードが取得できなければ取得したレコードの長さである```len```は0になるので、その場合はカードが登録されていないという判定にします。

さてこれで登録機能と検索機能が揃ったので、次回は2つを組み合わせていきます！