2022年 SPRESENSE™ 活用コンテスト 」の応募受付は終了しました。結果発表まで今しばらくお待ちください。

mametarou963のアイコン画像
mametarou963 2022年09月20日作成 (2022年09月20日更新) © MIT
製作品 製作品 閲覧数 134
mametarou963 2022年09月20日作成 (2022年09月20日更新) © MIT 製作品 製作品 閲覧数 134

spresenseでつくる法人向け入退室管理装置

spresenseでつくる法人向け入退室管理装置

背景

QrioをはじめとしてSmartLockが徐々に流行ってきています。
これらはスマートホームだけではなく、法人やレンタルスペースにも取り付けられ、FeliCaなどの非接触ICカードや、QRコードの読み取り等による認証により解錠し、運用されます。

問題1:セキュリティリスク

これらは「知識情報」「所持情報」「生体情報」の3つの認証要素のうちの「所持情報」で認証されています。しかし、単一情報ではセキュリティが甘くなりがちです。所持しているメンバーのうち、一人でも紛失すると、発見や報告の間、その施設自体はリスクにさらされます。

問題2:コストの高さ

生体情報による認証を組み合わせることでセキュリティ性が向上しますが、法人などは対象となる扉の数が多いため、生体読み取り装置を各扉に設置すること自体がコストになります。

問題3:操作の煩雑さ

SmartLockのスマホアプリケーションには、アプリケーションから扉を選択し、解施錠するものがありますが、多くの施設が複数のSmartLockをもっているため、どの位置の扉を解施錠するかを選択しなければならないため、操作が煩雑になりがちです。

背景のまとめ

そこで、spresenseをベースとし、上記3つの問題を解決する法人向けSmartLockの開発を行いました。

本プロダクトの特徴

  • 従来の認証法のサポート:FeliCaカードをあてると認証し、扉の制御(今回はサーボモーター)が行うことができる
  • 多要素認証,コスト低,操作数軽減の認証方法のサポート:液晶に表示されているQRコードをiphoneの専用アプリで読み込むと認証し、扉の制御(今回はサーボモーター)が行うことができる
  • 認証時には記録として、接続されたカメラで画像を保存する
  • LTEでネットワークに接続するため、5VDCさえ取れれば、設置場所の制約はない

部品・材料

キャプションを入力できます

キャプションを入力できます

キャプションを入力できます

キャプションを入力できます

キャプションを入力できます

キャプションを入力できます

  • 本プロダクト用SpresenseHAT
    • 結線・配線は後述
    • 表面/裏面

キャプションを入力できます

キャプションを入力できます

結線・配線

「本プロダクト用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をベースとし、各部品をつけた、全体図を示します。

キャプションを入力できます

キャプションを入力できます

  • 持ち上げた様子

キャプションを入力できます

動作シーケンス

  • FeliCaで認証してサーボを動作

キャプションを入力できます

  • QRコードを読み込んでサーボを動作

キャプションを入力できます

電源を入れた様子

キャプションを入力できます

動作する様子

  • ※撮影日当日に液晶をHATに挿し間違えて故障してしまったため、液晶部分はM5Stackで代用しています。液晶は基本的にQRを表示するだけなので、動作に大きな影響はありません

  • FeliCaで認証してサーボを動作させる様子

ここに動画が表示されます

  • QRコードを読み込んでサーボを動作させる様子

ここに動画が表示されます

将来の展望

  • Webサーバーからの通知にしたがい、液晶に表示させるQRコードを一定間隔で更新することでセキュリティ性が向上する
  • iphoneアプリに指紋認証やFaceIDを取り入れることでセキュリティ性が向上する(基本的にスマホにはロックがかかっているはずなのであまり有益ではないかもしれない)

コード

Spresenseのコード

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サーバーのコード

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コードを読み取るの記事を参考に読み取ったURLにてWebAPIをコールするように修正しました
mametarou963のアイコン画像
広島で細々と活動しています。 最近はM5Stack関係をいじっています。
ログインしてコメントを投稿する