mametarou963 が 2022年09月20日13時21分49秒 に編集
初版
タイトルの変更
spresenseでつくる法人向け入退室管理装置
タグの変更
spresense
FeliCa
CAT-M
入退室制御装置
記事種類の変更
製作品
ライセンスの変更
(MIT) The MIT License
本文の変更
# 背景 [Qrio](https://qrio.me/smartlock/)をはじめとしてSmartLockが徐々に流行ってきています。 これらはスマートホームだけではなく、法人やレンタルスペースにも取り付けられ、FeliCaなどの非接触ICカードや、QRコードの読み取り等による認証により解錠し、運用されます。 ## 問題1:セキュリティリスク これらは「知識情報」「所持情報」「生体情報」の[3つの認証要素](https://www.nri.com/jp/knowledge/glossary/lst/ta/multi_factor_authentication#:~:text=3%E3%81%A4%E3%81%AE%E8%AA%8D%E8%A8%BC%E8%A6%81%E7%B4%A0)のうちの「所持情報」で認証されています。しかし、単一情報ではセキュリティが甘くなりがちです。所持しているメンバーのうち、一人でも紛失すると、発見や報告の間、その施設自体はリスクにさらされます。 ## 問題2:コストの高さ 生体情報による認証を組み合わせることでセキュリティ性が向上しますが、法人などは対象となる扉の数が多いため、生体読み取り装置を各扉に設置すること自体がコストになります。 ## 問題3:操作の煩雑さ SmartLockのスマホアプリケーションには、アプリケーションから扉を選択し、解施錠するものがありますが、多くの施設が複数のSmartLockをもっているため、どの位置の扉を解施錠するかを選択しなければならないため、操作が煩雑になりがちです。 ## 背景のまとめ そこで、spresenseをベースとし、上記3つの問題を解決する法人向けSmartLockの開発を行いました。 # 本プロダクトの特徴 * 従来の認証法のサポート:FeliCaカードをあてると認証し、扉の制御(今回はサーボモーター)が行うことができる * 多要素認証,コスト低,操作数軽減の認証方法のサポート:液晶に表示されているQRコードをiphoneの専用アプリで読み込むと認証し、扉の制御(今回はサーボモーター)が行うことができる * 認証時には記録として、接続されたカメラで画像を保存する * LTEでネットワークに接続するため、5VDCさえ取れれば、設置場所の制約はない # 部品・材料 * [SPRESENSEメインボード](https://www.switch-science.com/catalog/3900/) * [SPRESENSE拡張ボード](https://www.switch-science.com/catalog/3901/) * [SPRESENSEカメラボード](https://www.switch-science.com/catalog/4119/) ![キャプションを入力できます](https://camo.elchika.com/d582c7bcd8009063032f55a36c765660704a22ee/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f66343435663333622d316334362d343665382d383237332d363365633430303264616236/) * [FeliCa リーダー・ライター RC-S620S](https://www.switch-science.com/catalog/353/) * [FeliCa RC-S620S/RC-S730 ピッチ変換基板のセット(フラットケーブル付き)](https://www.switch-science.com/catalog/1029/) ![キャプションを入力できます](https://camo.elchika.com/3a384ac2cfe22be7719ff16cc2e8c1bd9ff95ef5/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f32303637336163612d363962612d343533312d393166662d393261643562353264366636/) * [SIM7080G CAT-M/NB-IoT Unit](https://shop.m5stack.com/products/sim7080g-cat-m-nb-iot-unit) * SIMはpovo SIMを採用 ![キャプションを入力できます](https://camo.elchika.com/5135c45ebd6cfc2b9148e16768714bb823ff5c8f/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f34633263336131622d356663392d343838632d396632332d616534613463613634373230/) * [ILI9341搭載2.8インチSPI制御タッチパネル付TFT液晶 MSP2807](https://akizukidenshi.com/catalog/g/gM-16265/) * ※タッチパネルは使用しない ![キャプションを入力できます](https://camo.elchika.com/265bfcd823442765f24d48c8b0f59c03390f424f/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f38623261313032662d363861382d343062632d393830312d623461343137646264613465/) * [Servo Kit 180‘](https://www.switch-science.com/catalog/6478/) ![キャプションを入力できます](https://camo.elchika.com/e83bbb5e46e6a629314527397c957263447c5d89/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f36376265343038642d653266632d346536332d623630662d333836323632373035613061/) * [東芝 microSDカード16GB MU-J016GX](https://www.amazon.co.jp/%E6%9D%B1%E8%8A%9D-microSDHC%E3%83%A1%E3%83%A2%E3%83%AA%E3%82%AB%E3%83%BC%E3%83%89-Class10-UHS-I-MU-J016GX/dp/B07G4PF9ZK/ref=sr_1_5?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=105XYV22UWWJ1&keywords=toshiba+16gb+microsd&qid=1663625340&s=computers&sprefix=toshiba+16gb+microsd%2Ccomputers%2C240&sr=1-5) ![キャプションを入力できます](https://camo.elchika.com/e1cc953dfa19ddff69099e3bf176ef4efaab1b7a/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f34346131326238312d393430332d343132332d393432392d663963306336356337326135/) * 本プロダクト用SpresenseHAT * 結線・配線は後述 * 表面/裏面 ![キャプションを入力できます](https://camo.elchika.com/1281778a83ac9472b4a6f3ffb34e9c18bb247f79/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f34323663383961332d363361662d343964662d623763372d346463636231376233393536/) ![キャプションを入力できます](https://camo.elchika.com/e161eade56a92148f8a89f0fc2a312dc51836df3/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f65316131646237622d373735652d346264392d626437352d373331343930323433313733/) # 結線・配線 「本プロダクト用SpresenseHAT」が各部品の結線・配線を吸収します。 以下のようになっています。 ## RC-S620S |RC-S620S|Spresense| |:---|:---| |GND|GND| |Reserve|-| |GND|GND| |TXD|D22| |RXD|D23| |VDD|Vout 5V| ## SIM7080G CAT-M/NB-IoT Unit |SIM7080G CAT-M/NB-IoT Unitのピン | Spresenseのピン | |:---|:---| |TXD|D00(UART2 RX)| |RXD|D01(UART1 TX)| |5V|Vout 5V| |GND|GND| ## ILI9341 |ILI9341のピン|Spresense| |:---|:---| |MISO|MISO| |LED|3.3V| |SCK|SCK| |MOSI|MOSI| |DC|PWM2| |RESET|GPIO| |CS|CS| |GND|GND| ## Servo Kit 180‘ |Servo Kit 180‘のピン|Spresense| |:---|:---| |S|D06(PWM0)| |V|3.3V| |G|GND| # ハードウェア完成図 Spresense本体と本プロダクト用SpresenseHATをベースとし、各部品をつけた、全体図を示します。 * 表 ![キャプションを入力できます](https://camo.elchika.com/6057e37feb6db5b3ab7c5e9a8b27c367a89d0647/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f62346532363862372d303632362d343334652d396130362d616535303538353566336530/) * 裏 ![キャプションを入力できます](https://camo.elchika.com/258d67ace58ceecf3112649407218421af186b5f/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f39386239396361322d646237372d346265342d393663662d383935343336386166636636/) * 持ち上げた様子 ![キャプションを入力できます](https://camo.elchika.com/6266ed1b939c7d7e078a0f9ac8056ecfa8ee1eab/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f62343133626565642d336365372d343961392d393862652d623038653733356430363461/) # 動作シーケンス * FeliCaで認証してサーボを動作 ![キャプションを入力できます](https://camo.elchika.com/47fe61c1a4c6ffb92606017ad062f107bcadfea4/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f65373131633438312d396532352d343562302d386436652d656635313333356265643164/) * QRコードを読み込んでサーボを動作 ![キャプションを入力できます](https://camo.elchika.com/5a916416e7bd674e99dc4f9ca388c969e109ce20/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f66643265623836652d623336622d343438652d383961342d6164366436373031303335312f65383966393236312d383234662d343362612d383237302d346138613337393066633234/) # 動作する様子 * ※撮影日当日に液晶をHATに挿し間違えて故障してしまったため、液晶部分はM5Stackで代用しています。液晶は基本的にQRを表示するだけなので、動作に大きな影響はありません * FeliCaで認証してサーボを動作させる様子 @[youtube](https://www.youtube.com/watch?v=kuqtHsSnu70) * QRコードを読み込んでサーボを動作させる様子 @[youtube](https://www.youtube.com/watch?v=dOT40JLZIbI) # 将来の展望 * Webサーバーからの通知にしたがい、液晶に表示させるQRコードを一定間隔で更新することでセキュリティ性が向上する * iphoneアプリに指紋認証やFaceIDを取り入れることでセキュリティ性が向上する(基本的にスマホにはロックがかかっているはずなのであまり有益ではないかもしれない) # コード ## Spresenseのコード ```arduino:Lチカの例 // ■include #include <ArduinoQueue.h> // felica #include "RCS620S.h" #include <SoftwareSerial.h> // mqtt #define TINY_GSM_MODEM_SIM7080 #define TINY_GSM_RX_BUFFER 650 // なくても動いたけど、あったほうが安定する気がする #define TINY_GSM_YIELD() { delay(2); } // なくても動いたけど、あったほうが安定する気がする #include <TinyGsmClient.h> #include <PubSubClient.h> #include "mqtt-config.h" // servo include #include <Servo.h> // disp #include "SPI.h" #include "Adafruit_GFX.h" #include "Adafruit_ILI9341.h" #include "qrcode.h" // camera #include <SDHCI.h> #include <stdio.h> #include <Camera.h> // ■constant #define QUEUE_SIZE_ITEMS 10 ArduinoQueue<int> authQueue(QUEUE_SIZE_ITEMS); // felica SoftwareSerial mySerial(22, 23); // RX,TXの割り当て RCS620S rcs620s(mySerial); #define COMMAND_TIMEOUT 400 #define PUSH_TIMEOUT 2100 #define POLLING_INTERVAL 500 // mqtt const char apn[] = "povo.jp"; const char* broker = MY_BROKER; const char* topicTest = "test"; const char* topicTest2 = "test2"; #define GSM_AUTOBAUD_MIN 9600 #define GSM_AUTOBAUD_MAX 115200 TinyGsm modem(Serial2); TinyGsmClient client(modem); PubSubClient mqtt(client); uint32_t lastReconnectAttempt = 0; // servo static Servo s_servo; /**< Servo object */ // disp #define TFT_DC 9 #define TFT_CS -1 #define TFT_RST 8 #define X_BASE_PIXLE 60 #define Y_BASE_PIXLE 90 #define SCALE_SIZE 4 // x3 Adafruit_ILI9341 tft = Adafruit_ILI9341(&SPI, TFT_DC, TFT_CS, TFT_RST); // camera #define BAUDRATE (115200) #define TOTAL_PICTURE_COUNT (10) SDClass theSD; int take_picture_count = 0; // ■function // felica void felicaInit() { int ret = 0; mySerial.begin(115200); delay(1000); while(ret != 1){ ret = rcs620s.initDevice(); Serial.print("RCS620S Init = "); Serial.println(ret); delay(1000); } Serial.print("felica Init success\n"); } void felicaRead() { bool result = false; rcs620s.timeout = COMMAND_TIMEOUT; int ret = rcs620s.polling(); Serial.print("RCS620S polling = "); Serial.println(ret); if (ret) { Serial.print("idm = "); for (int i = 0; i < 8; i++) { Serial.print(rcs620s.idm[i], HEX); Serial.print(" "); } Serial.println(); Serial.print("pmm = "); for (int i = 0; i < 8; i++) { Serial.print(rcs620s.pmm[i], HEX); Serial.print(" "); } Serial.println(); rcs620s.readWithEncryption( rcs620s.pmm, 0x000B, 1 /* block id = 1 の末尾に番号が入っている*/); // 認証Queueにenqueue authQueue.enqueue(1); while(1){ if(rcs620s.polling() == 0){ break; } delay(1); } } rcs620s.rfOff(); return result; } // mqtt void mqttCallback(char* topic, byte* payload, unsigned int len) { Serial.print("Message arrived ["); Serial.print(topic); Serial.println("]: "); Serial.print("bin:"); for(int i = 0;i < len;i++){ Serial.print(payload[i]); } Serial.println(); char payloadBuf[256] = {0}; strncpy(payloadBuf,(const char *)payload,len); String str = String(payloadBuf); Serial.print("str:"); Serial.println(str); Serial.print("Message send to "); Serial.println(topicTest2); Serial.println(str); char buf[256]; str.toCharArray(buf, 256); mqtt.publish(topicTest2, buf); // 認証Queueにenqueue authQueue.enqueue(1); } boolean mqttConnect() { Serial.print("Connecting to "); Serial.println(broker); // Connect to MQTT Broker boolean status = mqtt.connect("GsmClientTest"); // Or, if you want to authenticate MQTT: // boolean status = mqtt.connect("GsmClientName", "mqtt_user", "mqtt_pass"); if (status == false) { Serial.println(" fail"); return false; } Serial.println(" success"); mqtt.subscribe(topicTest); return mqtt.connected(); } void mqttInit() { Serial2.begin(9600, SERIAL_8N1); Serial.println("Wait..."); // Print text on the screen (string) 在屏幕上打印文本(字符串) // Set GSM module baud rate // モデムのリスタート Serial.println("Initializing modem..."); // Print text on the screen (string) 在屏幕上打印文本(字符串) modem.restart(); // モデムの情報の取得 String modemInfo = modem.getModemInfo(); Serial.print("Modem Info: "); Serial.println(modemInfo); // GPRS connection parameters are usually set after network registration Serial.print(F("Connecting to ")); Serial.print(apn); if (!modem.gprsConnect(apn, "", "")) { Serial.println("-> fail"); delay(10000); return; } Serial.println("-> success"); if (modem.isGprsConnected()) { Serial.println("GPRS connected"); } mqtt.setServer(broker, 1883); mqtt.setCallback(mqttCallback); mqtt.publish(topicTest2, "Hello Server! I'm spresense"); } void mqttLoop() { // Make sure we're still registered on the network if (!modem.isNetworkConnected()) { Serial.println("Network disconnected"); if (!modem.waitForNetwork(180000L, true)) { Serial.println(" fail"); delay(10000); return; } if (modem.isNetworkConnected()) { Serial.println("Network re-connected"); } // and make sure GPRS/EPS is still connected if (!modem.isGprsConnected()) { Serial.println("GPRS disconnected!"); Serial.print(F("Connecting to ")); Serial.print(apn); if (!modem.gprsConnect(apn, "", "")) { Serial.println(" fail"); delay(10000); return; } if (modem.isGprsConnected()) { Serial.println("GPRS reconnected"); } } } if (!mqtt.connected()) { Serial.println("=== MQTT NOT CONNECTED ==="); // Reconnect every 10 seconds uint32_t t = millis(); if (t - lastReconnectAttempt > 10000L) { lastReconnectAttempt = t; if (mqttConnect()) { lastReconnectAttempt = 0; } } delay(100); return; } mqtt.loop(); } // servo void servoInit() { s_servo.attach(PIN_D06); servoClose(); delay(5000); Serial.print("servo Init success\n"); } void servoOpen() { s_servo.write(0); } void servoClose() { s_servo.write(90); } void dispInit() { Serial.println("ILI9341 Test!"); tft.begin(40000000); // read diagnostics (optional but can help debug problems) uint8_t x = tft.readcommand8(ILI9341_RDMODE); Serial.print("Display Power Mode: 0x"); Serial.println(x, HEX); x = tft.readcommand8(ILI9341_RDMADCTL); Serial.print("MADCTL Mode: 0x"); Serial.println(x, HEX); x = tft.readcommand8(ILI9341_RDPIXFMT); Serial.print("Pixel Format: 0x"); Serial.println(x, HEX); x = tft.readcommand8(ILI9341_RDIMGFMT); Serial.print("Image Format: 0x"); Serial.println(x, HEX); x = tft.readcommand8(ILI9341_RDSELFDIAG); Serial.print("Self Diagnostic: 0x"); Serial.println(x, HEX); // Create the QR code QRCode qrcode; uint8_t qrcodeData[qrcode_getBufferSize(3)]; qrcode_initText(&qrcode, qrcodeData, 3, 0, DISP_URL); // Serial.print(F("Text ")); // Serial.println(testText()); tft.fillScreen(ILI9341_WHITE); // tft.setCursor(50, 50); for (unsigned int y = 0; y < qrcode.size; y++) { for (unsigned int x = 0; x < qrcode.size; x++) { if (qrcode_getModule(&qrcode, x, y)){ tft.writeFillRect( X_BASE_PIXLE + (x * SCALE_SIZE), Y_BASE_PIXLE + (y * SCALE_SIZE), SCALE_SIZE, SCALE_SIZE, ILI9341_BLACK); } } // Serial.print("\n"); } tft.setCursor(40, 240); tft.setTextColor(ILI9341_BLACK); tft.setTextSize(3); tft.println("Spresense"); } void camPrintError(enum CamErr err) { Serial.print("Error: "); switch (err) { case CAM_ERR_NO_DEVICE: Serial.println("No Device"); break; case CAM_ERR_ILLEGAL_DEVERR: Serial.println("Illegal device error"); break; case CAM_ERR_ALREADY_INITIALIZED: Serial.println("Already initialized"); break; case CAM_ERR_NOT_INITIALIZED: Serial.println("Not initialized"); break; case CAM_ERR_NOT_STILL_INITIALIZED: Serial.println("Still picture not initialized"); break; case CAM_ERR_CANT_CREATE_THREAD: Serial.println("Failed to create thread"); break; case CAM_ERR_INVALID_PARAM: Serial.println("Invalid parameter"); break; case CAM_ERR_NO_MEMORY: Serial.println("No memory"); break; case CAM_ERR_USR_INUSED: Serial.println("Buffer already in use"); break; case CAM_ERR_NOT_PERMITTED: Serial.println("Operation not permitted"); break; default: break; } } void camInit() { CamErr err; /* Initialize SD */ while (!theSD.begin()) { /* wait until SD card is mounted. */ Serial.println("Insert SD card."); } /* begin() without parameters means that * number of buffers = 1, 30FPS, QVGA, YUV 4:2:2 format */ Serial.println("Prepare camera"); err = theCamera.begin(); if (err != CAM_ERR_SUCCESS) { camPrintError(err); } /* Auto white balance configuration */ Serial.println("Set Auto white balance parameter"); err = theCamera.setAutoWhiteBalanceMode(CAM_WHITE_BALANCE_DAYLIGHT); if (err != CAM_ERR_SUCCESS) { camPrintError(err); } /* Set parameters about still picture. * In the following case, QUADVGA and JPEG. */ Serial.println("Set still picture format"); err = theCamera.setStillPictureImageFormat( CAM_IMGSIZE_QUADVGA_H, CAM_IMGSIZE_QUADVGA_V, CAM_IMAGE_PIX_FMT_JPG); if (err != CAM_ERR_SUCCESS) { camPrintError(err); } } void camTakePic() { Serial.println("call takePicture()"); CamImage img = theCamera.takePicture(); /* Check availability of the img instance. */ /* If any errors occur, the img is not available. */ if (img.isAvailable()) { /* Create file name */ char filename[16] = {0}; sprintf(filename, "PICT%03d.JPG", take_picture_count); Serial.print("Save taken picture as "); Serial.print(filename); Serial.println(""); theSD.remove(filename); File myFile = theSD.open(filename, FILE_WRITE); myFile.write(img.getImgBuff(), img.getImgSize()); myFile.close(); } else { Serial.println("Failed to take picture"); } } void ledLightUp() { digitalWrite(LED0, HIGH); delay(100); digitalWrite(LED1, HIGH); delay(100); digitalWrite(LED0, LOW); digitalWrite(LED2, HIGH); delay(100); digitalWrite(LED1, LOW); digitalWrite(LED3, HIGH); delay(100); digitalWrite(LED2, LOW); delay(100); digitalWrite(LED3, LOW); } void setup() { // ログ用シリアル Serial.begin(115200); // LED pinMode(LED0, OUTPUT); pinMode(LED1, OUTPUT); pinMode(LED2, OUTPUT); pinMode(LED3, OUTPUT); felicaInit(); mqttInit(); servoInit(); dispInit(); ledLightUp(); camInit(); } void loop() { felicaRead(); mqttLoop(); if (!authQueue.isEmpty()) { int value = authQueue.dequeue(); delay(500); camTakePic(); servoOpen(); delay(5000); servoClose(); } delay(500); } ``` ## webサーバーのコード ```python from fastapi_mqtt.fastmqtt import FastMQTT from fastapi import FastAPI from fastapi_mqtt.config import MQTTConfig app = FastAPI() mqtt_config = MQTTConfig() fast_mqtt = FastMQTT(config=mqtt_config) fast_mqtt.init_app(app) @fast_mqtt.on_connect() def connect(client, flags, rc, properties): fast_mqtt.client.subscribe("/mqtt") #subscribing mqtt topic print("Connected: ", client, flags, rc, properties) @fast_mqtt.on_message() async def message(client, topic, payload, qos, properties): print("Received message: ",topic, payload.decode(), qos, properties) return 0 @fast_mqtt.subscribe("my/mqtt/topic/#") async def message_to_topic(client, topic, payload, qos, properties): print("Received message to specific topic: ", topic, payload.decode(), qos, properties) @fast_mqtt.on_disconnect() def disconnect(client, packet, exc=None): print("Disconnected") @fast_mqtt.on_subscribe() def subscribe(client, mid, qos, properties): print("subscribed", client, mid, qos, properties) @app.get("/") async def func(): fast_mqtt.publish("test", "Hello from Fastapi") #publishing mqtt topic return {"result": True,"message":"Published" } ``` ## iphoneアプリのコード * 記事に対してコード量が多いこと本題でないため省略します * [SwiftUIでQRコードを読み取る](https://qiita.com/ikaasamay/items/58d1a401e98673a96fd2)の記事を参考に読み取ったURLにてWebAPIをコールするように修正しました