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

# できあがるもの
![felica_final.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/021e58be-dd48-572f-31c7-fe2d2b0c3e31.gif)
良い感じですね！

# はじめに
前回までの記事で、

- [M5StackとFirebaseの通信](https://qiita.com/bigface00/items/87346c31d12346f85716)
- [M5StackとFeliCaリーダーを使ったFirebaseへのICカードmIDの登録](https://qiita.com/bigface00/items/29511d3a4425da5a499c)
- [M5StackとFeliCaリーダーを使った登録済みmIDの検索（認証）](https://qiita.com/bigface00/items/e92f3a12ed0162c17d84)

を実装していきました。
本記事では、これらの機能を組み合わせて登録と認証をM5Stackで一元で行えるようにしていきます。

また、最終章はこちらです。
[M5StackとFirebaseを連携してスマートロックみたいなものを作る！⑤](https://qiita.com/bigface00/items/590834705146e65f4826)

# アイディア
スマートロックで必要な動作は、「カードの登録」、「入場」、「退場」ぐらいでしょうか。
そこで、カードの情報を登録していくためにUserというテーブルに以下のようなフィールドを構築することにします。

| uid(勝手に付与されるid) |id（ICカードのid）|time(登録した日付)|entered(入場したか）|
| ---- | ---- | ---- | ---- |
|-MOt_X1f9fGJ9ybWr5e2|012e4cd22c478697|2020,12,19,15:0:49|false|
|-MOt_X1f9fGJ9ybQe6w3|012e4cd22c473a32|2020,12,19,15:1:13|true|
|...|...|...|...|

また、M5Stackには3つのボタンがあるので以下のように使うことにします。
![M5_button.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271414/f79cf6cc-c0e7-24a5-d426-6184b436741c.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 ""
#define FIREBASE_AUTH""
#define WIFI_SSID ""
#define 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;

// 時間取得用の定数
// Constant value for getting the time
const char *ntpServer = "ntp.jst.mfeed.ad.jp";
const long gmtOffset_sec = 9 * 3600;
const int daylightOffset_sec = 0;

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

// 時間をStringの形で取得
// Get the time as String
String getTimeAsString()
{
  struct tm timeinfo;
  String _timeNow = "";
  if (!getLocalTime(&timeinfo))
  {
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.print("Failed to obtain time");
    return _timeNow;
  }
  _timeNow = String(1900 + timeinfo.tm_year) + "," + String(1 + timeinfo.tm_mon) + "," + String(timeinfo.tm_mday) + "," + String(timeinfo.tm_hour) + ":" + String(timeinfo.tm_min) + ":" + String(timeinfo.tm_sec);
  return _timeNow;
}

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);

  // 時間の初期化と取得
  // Initialize and get the time
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);

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

void loop()
{
  int ret;
  // 入退場
  // Entered or not
  bool _entered = false;
　// 登録されいているか
  // Registered or not
  bool _registered = false;
  String uid = "";
  rcs620s.timeout = COMMAND_TIMEOUT;
  M5.update();
  M5.Lcd.setTextColor(WHITE);

  // ボタンで機能を変える
  // Push the button to select the function
  if (M5.BtnA.wasPressed())
  {
    _registerPhase = true;
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.print("Card registering");
  }

  if (M5.BtnB.wasPressed())
  {
    _authenticatePhase = true;
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.print("Authentication");
  }

  if (M5.BtnC.wasPressed())
  {
    _registerPhase = false;
    _authenticatePhase = false;
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.print("Session canceled");
    delay(3000);
    M5.Lcd.fillScreen(BLACK);
  }

  // 通信待ち
  // Polling
  ret = rcs620s.polling();
  // 待機フェーズ
  // Waiting for pressing any key
  if (!_authenticatePhase && !_registerPhase)
  {
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.print("Welcome!!");
  }
  // 登録&認証フェーズ
  // Registering & Authentication phase
  else
  {
    M5.Lcd.setCursor(0, 30);
    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);
      }
    }
    // 各フィールドに値を挿入
    // Enter value in each field
    json.set("id", felicaID);
    json.set("date", getTimeAsString());
    json.set("entered", false);
    // 検索キーワードの指定
    // Specify searching keyword
    query.equalTo(felicaID);
    if (!felicaID.isEmpty())
    {
      // "User"というテーブルから検索
      // Search from "User" table
      if (Firebase.getJSON(firebaseData, "User", query))
      {
        String key, value = "";
        int type = 0;
        size_t len = firebaseData.jsonObject().iteratorBegin();
        for (size_t i = 0; i < len; i++)
        {
          firebaseData.jsonObject().iteratorGet(i, type, key, value);
          // uidの取得
          // Get uid
          if (i == 0)
          {
            uid = key;
          }
          // 入退場の状態を取得
          // Get entered or not
          if (key == "entered")
          {
            if (value == "true")
            {
              _entered = true;
            }
          }
        }
        json.iteratorEnd();
        if (len != 0)
        {
          _registered = true;
          M5.Lcd.fillScreen(BLACK);
          M5.Lcd.setCursor(0, 0);
          M5.Lcd.print("Your card is found");
          delay(500);
        }
      }
      else
      {
        M5.Lcd.fillScreen(BLACK);
        M5.Lcd.setTextColor(RED);
        M5.Lcd.setCursor(0, 0);
        M5.Lcd.print("FIREBASE FAILED");
        M5.Lcd.setCursor(0, 30);
        M5.Lcd.print("REASON: " + firebaseData.errorReason());
      }
      if (_registerPhase)
      {
        if (_registered)
        {
          M5.Lcd.setTextColor(RED);
          M5.Lcd.setCursor(0, 30);
          M5.Lcd.print("This card is already registered");
        }
        else
        {
          if (Firebase.pushJSON(firebaseData, "/User", json))
          {
            M5.Lcd.setTextColor(YELLOW);
            M5.Lcd.setCursor(0, 60);
            M5.Lcd.print("Your card is registered now!!");
          }
          else
          {
            M5.Lcd.fillScreen(BLACK);
            M5.Lcd.setTextColor(RED);
            M5.Lcd.setCursor(0, 0);
            M5.Lcd.print("FIREBASE FAILED");
            M5.Lcd.setCursor(0, 30);
            M5.Lcd.print("REASON: " + firebaseData.errorReason());
          }
        }
        delay(5000);
        // 登録が終了したら戻る
        _registerPhase = !_registerPhase;
      }
      else if (_authenticatePhase)
      {
        if (_registered)
        {
          if (!_entered)
          {
            if (Firebase.setBool(firebaseData, "/User/" + uid + "/entered/", true))
            {
              //Success
              M5.Lcd.setTextColor(YELLOW);
              M5.Lcd.setCursor(0, 30);
              M5.Lcd.print("HELLO!!");
            }
            else
            {
              M5.Lcd.fillScreen(BLACK);
              M5.Lcd.setTextColor(RED);
              M5.Lcd.setCursor(0, 0);
              M5.Lcd.print("FIREBASE FAILED");
              M5.Lcd.setCursor(0, 30);
              M5.Lcd.print("REASON: " + firebaseData.errorReason());
            }
          }
          else
          {
            if (Firebase.setBool(firebaseData, "/User/" + uid + "/entered/", false))
            {
              //Success
              M5.Lcd.setTextColor(YELLOW);
              M5.Lcd.setCursor(0, 30);
              M5.Lcd.print("SEE YOU!!");
            }
            else
            {
              M5.Lcd.fillScreen(BLACK);
              M5.Lcd.setTextColor(RED);
              M5.Lcd.setCursor(0, 0);
              M5.Lcd.print("FIREBASE FAILED");
              M5.Lcd.setCursor(0, 30);
              M5.Lcd.print("REASON: " + firebaseData.errorReason());
            }
          }
        }else{
          M5.Lcd.setTextColor(RED);
          M5.Lcd.setCursor(0, 60);
          M5.Lcd.print("This card is not registered");
          M5.Lcd.setCursor(0, 90);
          M5.Lcd.print("Please register");
        }
        delay(5000);
        // 入退場が終了したら戻る
        _authenticatePhase = !_authenticatePhase;
      }
      M5.Lcd.fillScreen(BLACK);
    }
  }
  rcs620s.rfOff();
  delay(POLLING_INTERVAL);
}
```
## ボタン機能の実装
```c++:ボタンに各種状態を登録
  M5.update();
  M5.Lcd.setTextColor(WHITE);
// ボタンで機能を変える
  // Push the button to select the function
  if (M5.BtnA.wasPressed())
  {
    _registerPhase = true;
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.print("Card registering");
  }

  if (M5.BtnB.wasPressed())
  {
    _authenticatePhase = true;
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.print("Authentication");
  }

  if (M5.BtnC.wasPressed())
  {
    _registerPhase = false;
    _authenticatePhase = false;
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.print("Session canceled");
    delay(3000);
    M5.Lcd.fillScreen(BLACK);
  }
```

ボタンAに登録フェーズ、ボタンBに認証フェーズ、ボタンCにキャンセルをそれぞれ割り当てます。
ボタンA、Bが押された後でもボタンCを押すことでフェーズのキャンセルをすることができます。
また```M5.update()```の記載が無いとボタンの入力を受け付けないので、忘れないようにして下さい。

```c++:待機フェーズ
// 待機フェーズ
  // Waiting for pressing any key
  if (!_authenticatePhase && !_registerPhase)
  {
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.print("Welcome!!");
  }
```
何もボタンが押されていなければ待機フェーズとなります。

## ICカード情報の取得
```c++:FeliCaリーダーを使う
    M5.Lcd.setCursor(0, 30);
    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);
      }
    }
    // 各フィールドに値を挿入
    // Enter value in each field
    json.set("id", felicaID);
    json.set("date", getTimeAsString());
    json.set("entered", false);
    // 検索キーワードの指定
    // Specify searching keyword
    query.equalTo(felicaID);
```
次に、FeliCaリーダーを使ってICカードの情報を読み取ります。
"id"に読み取ったmIDを、"date"には時間、"entered"には一旦falseを入れておきましょう。
そして、取得したmIDを元にFirebase RDBから同一mIDの検索を行います。

## mIDの検索
```c++:mID情報の取得
    if (!felicaID.isEmpty())
    {
      // "User"というテーブルから検索
      // Search from "User" table
      if (Firebase.getJSON(firebaseData, "User", query))
      {
        String key, value = "";
        int type = 0;
        size_t len = firebaseData.jsonObject().iteratorBegin();
        for (size_t i = 0; i < len; i++)
        {
          firebaseData.jsonObject().iteratorGet(i, type, key, value);
          // uidの取得
          // Get uid
          if (i == 0)
          {
            uid = key;
          }
          // 入退場の状態を取得
          // Get entered or not
          if (key == "entered")
          {
            if (value == "true")
            {
              _entered = true;
            }
          }
        }
        json.iteratorEnd();
        if (len != 0)
        {
          _registered = true;
          M5.Lcd.fillScreen(BLACK);
          M5.Lcd.setCursor(0, 0);
          M5.Lcd.print("Your card is found");
          delay(500);
        }
      }
      else
      {
        M5.Lcd.fillScreen(BLACK);
        M5.Lcd.setTextColor(RED);
        M5.Lcd.setCursor(0, 0);
        M5.Lcd.print("FIREBASE FAILED");
        M5.Lcd.setCursor(0, 30);
        M5.Lcd.print("REASON: " + firebaseData.errorReason());
      }
```
もし、同一のmIDが見つかった場合は各カード毎に自動で割り振られる"uid"と入退場の"entered"の情報を取得しておきましょう。
ここで気を付けたいのが、```firebaseData.jsonObject().iteratorGet()```で取得できる値はStringになります。
少し泥臭いやり方ではありますが、StringからBooleanに変換しておきましょう。
lenが0でなければ、同一mIDが見つかったことになりますので登録されている旨をM5Stackへ表示しておきましょう。
また、```_registered```をtrueにしておきます。

## 登録フェーズ
```c++:カードの登録を行う
      if (_registerPhase)
      {
        if (_registered)
        {
          M5.Lcd.setTextColor(RED);
          M5.Lcd.setCursor(0, 30);
          M5.Lcd.print("This card is already registered");
        }
        else
        {
          if (Firebase.pushJSON(firebaseData, "/User", json))
          {
            M5.Lcd.setTextColor(YELLOW);
            M5.Lcd.setCursor(0, 60);
            M5.Lcd.print("Your card is registered now!!");
          }
          else
          {
            M5.Lcd.fillScreen(BLACK);
            M5.Lcd.setTextColor(RED);
            M5.Lcd.setCursor(0, 0);
            M5.Lcd.print("FIREBASE FAILED");
            M5.Lcd.setCursor(0, 30);
            M5.Lcd.print("REASON: " + firebaseData.errorReason());
          }
        }
        delay(5000);
        // 登録が終了したら戻る
        _registerPhase = !_registerPhase;
      }
```
もし登録フェーズが選択されていれば登録作業へ入ります。
まず、既に登録済みであればこのカードが登録の必要がないことを示します。
もし登録されていなければ、```Firebase.pushJSON()```を使ってカード情報をプッシュします。
プッシュに成功すれば、カードが登録された旨を表示します。

## 認証フェーズ
```c++:カードの認証を行う
      else if (_authenticatePhase)
      {
        if (_registered)
        {
          if (!_entered)
          {
            if (Firebase.setBool(firebaseData, "/User/" + uid + "/entered/", true))
            {
              //Success
              M5.Lcd.setTextColor(YELLOW);
              M5.Lcd.setCursor(0, 30);
              M5.Lcd.print("HELLO!!");
            }
            else
            {
              M5.Lcd.fillScreen(BLACK);
              M5.Lcd.setTextColor(RED);
              M5.Lcd.setCursor(0, 0);
              M5.Lcd.print("FIREBASE FAILED");
              M5.Lcd.setCursor(0, 30);
              M5.Lcd.print("REASON: " + firebaseData.errorReason());
            }
          }
          else
          {
            if (Firebase.setBool(firebaseData, "/User/" + uid + "/entered/", false))
            {
              //Success
              M5.Lcd.setTextColor(YELLOW);
              M5.Lcd.setCursor(0, 30);
              M5.Lcd.print("SEE YOU!!");
            }
            else
            {
              M5.Lcd.fillScreen(BLACK);
              M5.Lcd.setTextColor(RED);
              M5.Lcd.setCursor(0, 0);
              M5.Lcd.print("FIREBASE FAILED");
              M5.Lcd.setCursor(0, 30);
              M5.Lcd.print("REASON: " + firebaseData.errorReason());
            }
          }
        }else{
          M5.Lcd.setTextColor(RED);
          M5.Lcd.setCursor(0, 60);
          M5.Lcd.print("This card is not registered");
          M5.Lcd.setCursor(0, 90);
          M5.Lcd.print("Please register");
        }
```
そして、認証フェーズです。if文ばかりでこんがらがりそうです笑
カードのmIDを元に検索を行った際に"entered"の情報を取得しました。こちらを使って入場か退場かを判別します。
同時に、入退場の情報を更新するために"uid"も使います。
```Firebase.setBool()```を使って、指定したUIDのenteredフィールドを書き換えます。
また、書き換えたらM5Stackに入退場どっちを行ったか表示しましょう。

# おわりに
これにてM5StackとFirebaseを使ったスマートロックが完成です。
ただ、実際に使う場合には部屋の外と中で状態を分けた識別や、セキュリティ的にmIDはかなり甘いらしいのでそこらへんも気を付けたいですね。
参考：[Suica(IDm)で認証するのは危険なのでFelica Lite-Sの内部認証を使う](https://qiita.com/odetarou/items/bcd65dbfd1f68735ac30)

次回は、Flutterを使って誰が入退場しているのかといった状態を可視化しようと思います！