M5StackのRFID Unitで賞味期限を警告するシステムを作る

M5StackとRFID Unitで賞味期限(制限時間)が来たら教えてくれるシステムを作ってみました。

ソースコードはこちらです
https://github.com/coppercele/RFID


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">RFIDによる賞味期限管理システム完成！<br><br>賞味期限までの時間をセットしてRFIDをスキャンするとSDカードに記録され時間が来ると赤くなって教えてくれます<a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> <a href="https://t.co/Vu8onEWpda">pic.twitter.com/Vu8onEWpda</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1609570499545432064?ref_src=twsrc%5Etfw">January 1, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

そもそもこれを着想したのが新型コロナのワクチンの使用期限が切れたものを接種してしまったというニュースが流れた時だったんですが、
RFID Unitの売り切れと作成をサボってたせいで意味がなくなっちゃったし、
逸般の誤家庭にしかワクチンなんてないので賞味期限を管理するっていう微妙なものになっちゃいました(´・ω・`)

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ワクチンの箱にFRIDタグ貼って一定期間入れっぱなしだったら警告上げるようなシステム組めないかしら<br><br>M5Stack用WS1850S搭載 RFID 2ユニット - スイッチサイエンス<a href="https://t.co/EgfdCvThrx">https://t.co/EgfdCvThrx</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1568879176022900736?ref_src=twsrc%5Etfw">September 11, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 使用するデバイス
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/9380540d-f7cf-a080-59db-7edfcdff3970.png)

M5Stack Basic - スイッチサイエンス
https://www.switch-science.com/catalog/3647/

Arduino互換なESP32がケースに入っててボタンとディスプレイとバッテリーがついててIOも豊富でWifiとBluetoothも使えるニクい奴

バリエーションによって9軸センサがついてたりするけどbasicは一番基本的な奴

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/a8f53b46-3356-7147-f090-c69e7883f012.png)

M5Stack用WS1850S搭載 RFID 2ユニット
https://www.switch-science.com/products/8301?_pos=5&_sid=1a9b95207&_ss=r

無線周波数の認証が可能なユニットです。WS1850Sを内蔵しており、動作周波数は13.56 MHzです。カードの読み込み/書き込み/認識/記録や、RFカードのエンコーディング、認証など様々な機能を備えています。(スイッチサイエンスさんから引用)

Amazon.co.jp: uxcell RFIDタグ 13.56MHz 読み取り専用ブランクICプロキシミティ 非接触型スマートコインカード ラウンド 直径25 mm アクセス制御用 ホワイト 5パック : 産業・研究開発用品
https://www.amazon.co.jp/gp/product/B0B4RT1P93/ref=ppx_yo_dt_b_asin_title_o08_s00?ie=UTF8&psc=1

13.56MHzに対応したRFIDタグなら何でもいいと思います
必要な数と予算で適切なのを選んでください

## RFIDを読み込む
M5Stackのサンプルからコードを持ってきます

https://github.com/m5stack/M5Stack/blob/master/examples/Unit/RFID_RC522/RFID_RC522.ino

```cpp
#include "MFRC522_I2C.h"

  if (!mfrc522.PICC_IsNewCardPresent() || !mfrc522.PICC_ReadCardSerial()) {
    // RFIDカードが読めていないとreturn
    delay(50);
    return;
  }
  char buf[9];

  sprintf(buf, "%02X%02X%02X%02X", mfrc522.uid.uidByte[0],
          mfrc522.uid.uidByte[1], mfrc522.uid.uidByte[2],
          mfrc522.uid.uidByte[3]);
  char *uidChar = buf;
  Serial.printf("RFID read\n");
```

読み込んだbyte列をASCIIコードに変換してchar*に格納します。
4バイトを前提としてますがRFIDのidって4バイト以外もあるんですかね？

## 賞味期限を設定するUIを構築する
M5Stackには3ボタンあるのでそれで制限時間を設定するUIを構築します
BtnAをモード切替、BtnBを数字増加、BtnCを時間選択として使います
構造体にday,hour,minuteを用意しBtnBとBtnCで増減するようにします。

なお日本語を表示するためにLovyanGFXを使わせてもらっております

GitHub - lovyan03/LovyanGFX: SPI LCD graphics library for ESP32 (ESP-IDF/ArduinoESP32) / ESP8266 (ArduinoESP8266) / SAMD51(Seeed ArduinoSAMD51)
https://github.com/lovyan03/LovyanGFX

```cpp
#include <M5Stack.h>
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
#define LGFX_AUTODETECT
#include <LGFX_AUTODETECT.hpp>

struct beans {
  int day = 0;
  int hour = 0;
  int minute = 10;
} data;

// Spriteを構築して画面を更新する
void makeSprite() {
  sprite.setFont(&fonts::lgfxJapanGothicP_20);
  sprite.setTextSize(1);

  if (data.isExpired) {
    sprite.clear(TFT_RED);
    sprite.setTextColor(TFT_WHITE, TFT_RED);
  }
  else {
    sprite.clear(TFT_BLACK);
    sprite.setTextColor(TFT_WHITE, TFT_BLACK);
  }

  if (data.message != NULL) {
    sprite.setCursor(0, 0);
    sprite.printf("%s", data.message);
  }

  sprite.setCursor(0, 150);
  sprite.setTextSize(3);
  sprite.printf(" ");
  for (int i = 0; i < data.dateIndex; i++) {
    sprite.printf("　  ");
  }

  sprite.printf("%s", "■");

  sprite.setTextSize(2);
  sprite.setCursor(0, 190);
  switch (data.mode) {
  case 0:
    sprite.printf("%s", "  登録　↑ 　←");
    break;
  case 1:
    sprite.printf("%s", "  削除　↑ 　←");
    break;
  case 2:
    sprite.printf("%s", "  確認　↑ 　←");
    break;

  default:
    break;
  }

  sprite.setCursor(0, 130);
  sprite.printf("  %02d日%02d時%02d分", data.day, data.hour, data.minute);

  sprite.setTextSize(1);
  sprite.setCursor(280, 190);
  sprite.printf("SD");
  sprite.setCursor(280, 210);
  sprite.printf("%s", data.isSdEnable ? "OK" : "NG");
  sprite.setCursor(275, 0);
  sprite.printf("WIFI");
  sprite.setCursor(275, 20);
  sprite.printf("%s", data.isWifiEnable ? "OK" : "NG");

  sprite.pushSprite(&lcd, 0, 0);
}

void loop() {
  M5.update();
  if (M5.BtnA.wasPressed()) {
    switch (data.mode) {
    case 0:
      data.mode = 1;
      break;
    case 1:
      data.mode = 2;
      break;
    case 2:
      data.mode = 0;
      break;

    default:
      break;
    }
    makeSprite();
  }

  if (M5.BtnC.wasPressed()) {
    if (data.dateIndex == 0) {
      data.dateIndex = 2;
    }
    else {
      data.dateIndex--;
    }

    makeSprite();
  }
  if (M5.BtnB.wasPressed()) {
    switch (data.dateIndex) {
    case 2:
      if (59 <= data.minute) {
        data.minute = 0;
      }
      else {
        data.minute++;
      }
      break;
    case 1:
      if (12 <= data.hour) {
        data.hour = 0;
      }
      else {
        data.hour++;
      }
      break;
    case 0:
      if (30 < data.day) {
        data.day = 0;
      }
      else {
        data.day++;
      }
      break;

    default:
      break;
    }
    makeSprite();
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/a8ce8d43-e383-0c7e-963f-c90e41c8663a.png)


## データをJSONで扱う
データを保存するときにはCSVなどでもいいのですが、
人間にも読みやすいものという事でJSONを使うことにしました。
ライブラリはArduinoJSONを利用します

ArduinoJson: Efficient JSON serialization for embedded C++
https://arduinojson.org/

ArduinoIDEのライブラリマネージャなどを使ってインストールしておいてください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/5eb39990-092a-92a1-4119-d9e673ded9ab.png)

## ネットワークに接続する
ESP32はそのままだと1970/01/01の日付を返すので
ネットワークに接続してNTPで時計合わせを行います。

自分はいつもWPSで接続するんですが

ESP32(M5StickC)でWPSを使うと設定ファイルも必要ないし便利ですよ - Qiita
https://qiita.com/coppercele/items/6789deea453826916725

対応ルータがない/事情で使えないという環境もあるだろうという事で
M5StackのSDカードにwifi.jsonと言うファイルを置いてそこで指定することにしました

まずおもむろにWiFi.begin()(引数なし)を実行します
これ以前にネットワークに接続接続していたらNVSにデータが残っているのでこれだけで繋がります
※↑のWPSの記事参照

NVSに情報がない/違うネットワークに移動したなどの理由で繋がらない場合はSDカードのwifi.jsonを参照しに行きます

下のソースですが以下のような流れになっています

・WiFi.begin()実行
・繋がらないまま5秒経過するとWifi.jsonを読みに行く

・wifi.jsonが存在する場合
・SDカードからwifi.jsonを読み込みdeserializeしてissdとpasswordを読み込む
・WiFi.begin(ssid, password)を実行して接続する

・wifi.jsonが無い場合
・wifi.jsonを作成する
・ssid,passwordが""のJSONを書き込む
・画面にクソデカメッセージを表示する

```cpp
// WPSを使う場合コメントを外す
// #include "wpsConnector.h"


  WiFi.begin();
  int count = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500); // 500ms毎に.を表示
    Serial.print(".");
    count++;
    if (count == 10) {

      // wifi設定ファイルチェック
      if (SD.exists("/wifi.json")) {
        File f = SD.open("/wifi.json");
        DynamicJsonDocument doc(128);

        DeserializationError error = deserializeJson(doc, f);

        if (error) {
          Serial.print("deserializeJson() failed: ");
          Serial.println(error.c_str());
          return;
        }

        Serial.printf("wifi setting found\n");
        // wifi.jsonからid,passowordを読み込む
        const char *ssid = doc["ssid"];
        const char *password = doc["password"];

        Serial.printf("ssid:%s pass:%s\n", ssid, password);

        WiFi.disconnect();
        WiFi.begin(ssid, password);
      }
      else {
        // ファイルが存在しなければ作成を試みる
        // ただしSDカードがなくてもエラーにならない
        File f = SD.open("/wifi.json", FILE_WRITE);
        DynamicJsonDocument jsonDocument(48);
        jsonDocument["ssid"] = "";
        jsonDocument["password"] = "";
        serializeJsonPretty(jsonDocument, f);
        f.close();
        Serial.printf("wifi.jspn created\n");

        sprite.setFont(&fonts::lgfxJapanGothicP_20);
        sprite.setTextSize(2);

        sprite.printf("%s", "Pls set Wifi information in wifi.json on SD Card");
        sprite.pushSprite(&lcd, 0, 0);
      }
      // WPSを使う場合コメントを外して上のJSON関係を削除する
      //   // 5秒間待ってからWPSを開始する
      //   // 一度WiFiを切断してwpsConnect()を使う
      //   WiFi.disconnect();
      //   wpsConnect();
    }
  }
  // NTPで時計合わせをする
  if (WiFi.status()) {
    Serial.println("\nConnected");
    Serial.println("ntp configured");

    configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com",
               "ntp.jst.mfeed.ad.jp");
    data.isWifiEnable = true;
  }
```
SDカードが刺さってなかったりwifi.jsonが存在しなかったら画面にクソデカメッセージを表示するので
「SDカードが必要でその中のwifi.jsonを編集すればいいんだな」ってのは伝わるのではないかなと(´・ω・`)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/e70c99cf-9735-caa7-0035-d53707402462.png)

```json:wifi.json
{
  "ssid": "",
  "passoword": ""
}

```

## スキャンしたデータをJSONに保存する
RFIDをスキャンしてuidを読み取ったらデータをJSONに保存します

JSONのデータは以下のようなものにしました

```json

{
  "json": [
    {
      "id": "000",
      "uid": "764D6925",
      "scandate": "2023/01/06 01:24:39",
      "expire": "1672947279"
    },
    {
      "id": "001",
      "uid": "D6576C25",
      "scandate": "2023/01/06 01:24:44",
      "expire": "1672947284"
    }
  ]
}

```

[]内が配列になっていてスキャンするごとにデータが追加されていきます

id: 配列内の通し番号
uid:RFIDのuid
scandate:スキャンをした日時
expire:賞味期限(制限時間)をUnixTimeで保存した物

まずsetup()内でファイルが存在するかチェックしてなければ空ファイルを作ります

```cpp
  // SDチェック
  if (SD.exists("/data.json")) {
    data.isSdEnable = true;
    Serial.printf("SD Card Found\n");
  }
  else {
    // ファイルが存在しなければ作成を試みる
    // SDが存在していれば次のリセット時にチェックが通る
    File f = SD.open("/data.json", FILE_WRITE);
    f.close();
    Serial.printf("SD Card Not Found\n");
  }
```

RFIDを読み込んだら画面の追加時間を反映してJSONに保存します(下の例なら+10分)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/791f485f-508b-bab4-f28c-85b97a9bb367.png)


```cpp

  if (!mfrc522.PICC_IsNewCardPresent() || !mfrc522.PICC_ReadCardSerial()) {
    // RFIDカードが読めていないとreturn
    delay(50);
    return;
  }
  mfrc522.PCD_AntennaOff();
  data.uidSize = mfrc522.uid.size;
  char buf[9];

  sprintf(buf, "%02X%02X%02X%02X", mfrc522.uid.uidByte[0],
          mfrc522.uid.uidByte[1], mfrc522.uid.uidByte[2],
          mfrc522.uid.uidByte[3]);
  // 重複読み込みを防ぐため画面を白くして3秒待つ
  Serial.printf("RFID read\n");
  sprite.clear(TFT_WHITE);
  sprite.pushSprite(&lcd, 0, 0);
  delay(3000);
  mfrc522.PCD_AntennaOn();

  DynamicJsonDocument jsonDocument(1024);
  DeserializationError error = deserializeJson(jsonDocument, f);
  // deserializeするとFileに書き込めなくなるので開きなおす
  f.close();

  // data.jsonが空の場合
  if (error) {
      f = SD.open("/data.json", FILE_WRITE);

      // data.jsonが空
      jsonDocument.clear();
      Serial.print("data.json is empty.\n");
      Serial.println(error.c_str());
      // json配列を作成
      JsonArray array = jsonDocument.createNestedArray("json");
      JsonObject object = array.createNestedObject();
      createNewRecord(object, 0, buf);

      // SDカードに書きこむ
      size_t size = serializeJsonPretty(jsonDocument, Serial);
      Serial.printf("\nSerialize size:%d\n", size);
      size = serializeJsonPretty(jsonDocument, f);
      Serial.printf("Serialize size:%d\n", size);
      displayJSON(object, "追加されました");
      Serial.print("JSON Wrote to SD Card\n");
      // 画面更新
      makeSprite();
      delay(1000);
      f.close();
      return;
  }

// 新しいJsonDocumentレコードを作成する
void createNewRecord(JsonObject &obj, int id, char *uid) {
  // 追加する要素を作成

  char timeStr[64];
  getLocalTime(&timeInfo);

  char buf[4];
  sprintf(buf, "%03d", id);
  obj["id"] = buf;
  obj["uid"] = uid;

  sprintf(timeStr, "%04d/%02d/%02d %02d:%02d:%02d", timeInfo.tm_year + 1900,
          timeInfo.tm_mon + 1, timeInfo.tm_mday, timeInfo.tm_hour,
          timeInfo.tm_min, timeInfo.tm_sec);
  Serial.printf("time:%s\n", timeStr);
  obj["scandate"] = timeStr;

  // 現時刻のUnixTimeを取得
  time_t expireDate = mktime(&timeInfo);

  // 画面UIの日時分から値を取得してUnixTimeに足す
  expireDate += data.minute * 60;
  expireDate += data.hour * 60 * 60;
  expireDate += data.day * 24 * 60 * 60;

  sprintf(timeStr, "%ld", expireDate);
  obj["expire"] = timeStr;
}

```

これで以下のようなJSONが作成されます

```json:data.json
{
  "json": [
    {
      "id": "000",
      "uid": "764D6925",
      "scandate": "2023/01/06 01:24:39",
      "expire": "1672947279"
    }
  ]
}
```

## 制限時間が来たら警告する
JSONにデータが書き込めたら制限時間チェックを実装します

流れは以下のようになってます
・setup()でアプリの起動時刻(time)を取得
・現時刻をnowに取得してtimeから5分経っているかチェック
・5分経っていればdata.jsonの内容を確認
・JSONデータ内のexpireと現時刻のUnixTimeを比較して現時刻が大きければ画面を赤くして警告する
・timeをnowで更新する


```cpp
  unsigned long time;

setup() {
  time = millis();
}
  // ここから期限切れチェック
  unsigned long now = millis();

  // 5分ごとにチェック
  if ((5 * 60 * 1000) <= now - data.time) {
    Serial.printf("5min %ld\n", now / 1000);
    data.time = now;
    File f = SD.open("/data.json");
    DynamicJsonDocument jsonDocument(1024);
    DeserializationError error = deserializeJson(jsonDocument, f);
    f.close();
    if (!error) {
      // JSONデータが存在する
      JsonArray array = jsonDocument["json"].as<JsonArray>();
      for (int i = 0; i < array.size(); i++) {
        JsonObject object = array[i];

        const char *unixTime = object["expire"];

        Serial.printf("expire char:%s\n", unixTime);
        getLocalTime(&timeInfo);
        time_t nowTime = mktime(&timeInfo);
        Serial.printf("nowTime long:%ld\n", nowTime);

        long lUnixTime;
        sscanf(unixTime, "%ld", &lUnixTime);
        // 期限が過去ならば
        if (lUnixTime < nowTime) {
          // TODO displayJSON(JsonObject object, char * message)を作る
          // 期限切れ表示
          displayJSON(object, "期限が切れています");
          data.isExpired = true;
          makeSprite();
          break;
        }
      }
    }
    data.isExpired = false;
  }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/f5155328-2fac-c3f5-d967-09583ec19a07.png)

## JSONデータを編集する

・配列にデータを挿入する
jsonArray.createNestedObject()で配列に空要素が追加されるので
戻り値のJsonObjectをcreateNewRecord()で操作してデータを設定できます
その時idが重複しないようにsearchNewestID()を実行して既存のidに1追加します

```cpp

  data.uidSize = mfrc522.uid.size;
  char buf[9];
  // byte列をchar*(ASCII)に変換
  sprintf(buf, "%02X%02X%02X%02X", mfrc522.uid.uidByte[0],
          mfrc522.uid.uidByte[1], mfrc522.uid.uidByte[2],
          mfrc522.uid.uidByte[3]);
  // 読み込んだuidを構造体に入れる
  data.uidChar = buf;
  Serial.printf("RFID read\n");

  // SDカードからJSONを読み込む
  File f = SD.open("/data.json");
  DynamicJsonDocument jsonDocument(1024);
  DeserializationError error = deserializeJson(jsonDocument, f);
  // deserializeするとFileに書き込めなくなるので開きなおす
  f.close();

(省略)

    // data.jsonの配列要素からuidを取得して比較
    if (!strcmp(uid, data.uidChar)) {
      // スキャンされたuidが既に存在する
      Serial.printf("UID :%s found in json.\n", data.uidChar);
      // 既存データと重複していた時の処理をしてreturn
      return;
    }
 

  // データが見つからなかったのでIDを増やして追加
  f = SD.open("/data.json", FILE_WRITE);
  // 一番古いidを取得
  int newestId = searchNewestId(jsonDocument);
  Serial.printf("newestId:%d\n", newestId);

  // 追加する要素を作成
  // rootを"json"とする配列を取得
  JsonArray jsonArray = jsonDocument["json"].as<JsonArray>();
  JsonObject obj = jsonArray.createNestedObject();
  // データを追加
  createNewRecord(obj, newestId + 1, data.uidChar);
  // jsonを表示
  serializeJsonPretty(jsonDocument, Serial);
  Serial.println();
  // SDカードに書きこむ
  serializeJsonPretty(jsonDocument, f);
  Serial.print("JSON Wrote to SD Card\n");
  displayJSON(obj, "追加されました");
  delay(1000);
  f.close();

---

// 既存のJSONを検索して一番大きいidを返す
int searchNewestId(JsonDocument &jsonDocument) {

  // "json"をrootにする配列を取得
  JsonArray array = jsonDocument["json"].as<JsonArray>();
  int size = array.size();
  // Serial.printf("array size:%d\n", size);

  // 配列の最後の要素を取得
  JsonObject object = array[size - 1];
  // id(文字列)を取得してintに変換
  const char *json_item_id = object["id"];
  int jsonIdInt = 9;
  sscanf(json_item_id, "%d", &jsonIdInt);

  // Serial.printf("jsonId:%s\n", json_item_id);
  // Serial.printf("jsonIdInt:%d\n", jsonIdInt);
  return jsonIdInt;
}
```

・データを削除する
削除モードでRFIDを読み込み該当のデータがあった場合JSONから配列要素を削除します
重複データを検索しJsonArrayにremove(index)することで要素が削除できます

```cpp

  // SDカードからJSONを読み込む
  File f = SD.open("/data.json");
  DynamicJsonDocument jsonDocument(1024);
  DeserializationError error = deserializeJson(jsonDocument, f);
  // deserializeするとFileに書き込めなくなるので開きなおす
  f.close();
  JsonArray array = jsonDocument["json"].as<JsonArray>();
  for (int i = 0; i < array.size(); i++) {
    JsonObject object = array[i];
    const char *uid = object["uid"];
    // Serial.printf("new uid:%s\n", data.uidChar);
    if (uid == nullptr) {
      continue;
    }

    // Serial.printf("json uid:%s\n", uid);

    if (!strcmp(uid, data.uidChar)) {
      // スキャンされたuidが既に存在する
      Serial.printf("UID :%s found in json.\n", data.uidChar);
        f = SD.open("/data.json", FILE_WRITE);
        // 削除
        array.remove(i);
        Serial.println("Matched record removed.");
        displayJSON(object, "削除されました");
        // jsonを表示
        serializeJsonPretty(jsonDocument, Serial);
        Serial.println();
        // SDカードに書きこむ
        size_t size = serializeJsonPretty(jsonDocument, f);
        Serial.printf("Serialize size:%d\n", size);
        f.close();
        Serial.print("JSON Wrote to SD Card\n");
      }
```

## まとめ

RFID Unitはお安いし(825円)いろいろと応用が利きますので
軽率に買って使ってみましょう(ﾟ∀ﾟ)