M5StackとRFID Unitで賞味期限(制限時間)が来たら教えてくれるシステムを作ってみました。
ソースコードはこちらです
https://github.com/coppercele/RFID
RFIDによる賞味期限管理システム完成!
— もけ@ムギ㌠ (@coppercele) January 1, 2023
賞味期限までの時間をセットしてRFIDをスキャンするとSDカードに記録され時間が来ると赤くなって教えてくれます#M5Stack pic.twitter.com/Vu8onEWpda
そもそもこれを着想したのが新型コロナのワクチンの使用期限が切れたものを接種してしまったというニュースが流れた時だったんですが、
RFID Unitの売り切れと作成をサボってたせいで意味がなくなっちゃったし、
逸般の誤家庭にしかワクチンなんてないので賞味期限を管理するっていう微妙なものになっちゃいました(´・ω・`)
ワクチンの箱にFRIDタグ貼って一定期間入れっぱなしだったら警告上げるようなシステム組めないかしら
— もけ@ムギ㌠ (@coppercele) September 11, 2022
M5Stack用WS1850S搭載 RFID 2ユニット - スイッチサイエンスhttps://t.co/EgfdCvThrx
使用するデバイス
M5Stack Basic - スイッチサイエンス
https://www.switch-science.com/catalog/3647/
Arduino互換なESP32がケースに入っててボタンとディスプレイとバッテリーがついててIOも豊富でWifiとBluetoothも使えるニクい奴
バリエーションによって9軸センサがついてたりするけどbasicは一番基本的な奴
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のサンプルからコードを持ってきます
#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
#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();
}
}
データをJSONで扱う
データを保存するときにはCSVなどでもいいのですが、
人間にも読みやすいものという事でJSONを使うことにしました。
ライブラリはArduinoJSONを利用します
ArduinoJson: Efficient JSON serialization for embedded C++
https://arduinojson.org/
ArduinoIDEのライブラリマネージャなどを使ってインストールしておいてください
ネットワークに接続する
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を書き込む
・画面にクソデカメッセージを表示する
// 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を編集すればいいんだな」ってのは伝わるのではないかなと(´・ω・`)
{
"ssid": "",
"passoword": ""
}
スキャンしたデータをJSONに保存する
RFIDをスキャンしてuidを読み取ったらデータを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()内でファイルが存在するかチェックしてなければ空ファイルを作ります
// 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分)
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": [
{
"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で更新する
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;
}
JSONデータを編集する
・配列にデータを挿入する
jsonArray.createNestedObject()で配列に空要素が追加されるので
戻り値のJsonObjectをcreateNewRecord()で操作してデータを設定できます
その時idが重複しないようにsearchNewestID()を実行して既存のidに1追加します
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)することで要素が削除できます
// 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円)いろいろと応用が利きますので
軽率に買って使ってみましょう(゚∀゚)