spresenseでつくる法人向け入退室管理装置
背景
QrioをはじめとしてSmartLockが徐々に流行ってきています。
これらはスマートホームだけではなく、法人やレンタルスペースにも取り付けられ、FeliCaなどの非接触ICカードや、QRコードの読み取り等による認証により解錠し、運用されます。
問題1:セキュリティリスク
これらは「知識情報」「所持情報」「生体情報」の3つの認証要素のうちの「所持情報」で認証されています。しかし、単一情報ではセキュリティが甘くなりがちです。所持しているメンバーのうち、一人でも紛失すると、発見や報告の間、その施設自体はリスクにさらされます。
問題2:コストの高さ
生体情報による認証を組み合わせることでセキュリティ性が向上しますが、法人などは対象となる扉の数が多いため、生体読み取り装置を各扉に設置すること自体がコストになります。
問題3:操作の煩雑さ
SmartLockのスマホアプリケーションには、アプリケーションから扉を選択し、解施錠するものがありますが、多くの施設が複数のSmartLockをもっているため、どの位置の扉を解施錠するかを選択しなければならないため、操作が煩雑になりがちです。
背景のまとめ
そこで、spresenseをベースとし、上記3つの問題を解決する法人向けSmartLockの開発を行いました。
本プロダクトの特徴
- 従来の認証法のサポート:FeliCaカードをあてると認証し、扉の制御(今回はサーボモーター)が行うことができる
- 多要素認証,コスト低,操作数軽減の認証方法のサポート:液晶に表示されているQRコードをiphoneの専用アプリで読み込むと認証し、扉の制御(今回はサーボモーター)が行うことができる
- 認証時には記録として、接続されたカメラで画像を保存する
- LTEでネットワークに接続するため、5VDCさえ取れれば、設置場所の制約はない
部品・材料
- SIM7080G CAT-M/NB-IoT Unit
- SIMはpovo SIMを採用
- ILI9341搭載2.8インチSPI制御タッチパネル付TFT液晶 MSP2807
- ※タッチパネルは使用しない
- 本プロダクト用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
さんが
2022/09/20
に
編集
をしました。
(メッセージ: 初版)
-
mametarou963
さんが
2022/09/20
に
編集
をしました。
-
mametarou963
さんが
2022/09/20
に
編集
をしました。
ログインしてコメントを投稿する