気の向くままに辿るIT/ICT/IoT
IoT・電子工作

自作スマートロック/色々な方法で施錠・解錠

ホーム前へ次へ
ESP8266って?

自作スマートロック/色々な方法で施錠・解錠

自作スマートロック/色々な方法で施錠・解錠

2024/04/23

 (トップページから転載・追記)

 スマートロックシステムを自作してみた話。

 もちろん、後付け可能。

 既に運用から2ヶ月以上経過。

 2023/11/21に思い立ち、2023/12/30にスケッチ完成、モノもほぼできていたのに稼働は、その2ヶ月後の2024/02/13、更に記事のアップが更に2ヶ月以上経った今日2024/04/23という本来あり得ない状況になったのは、G社周りでゲンナリする状況が相次いだから...。

 さておき、全く作る気も予定もなかったスマート系デバイスだったこともあり、今更感はありますが、いざ作ってできてみると、もう手放せない、なくてはならないものになっており、常備はしているものの、以来、物理鍵を使ったことは一度もありません。

 これを作るモチベーションが上がらなかった理由は、ほぼ唯一「締め出される可能性を払拭できない」ことでしたが、よく考えてみたら、払拭できる目処が立ったので。

仕様概要リスト

 自作したスマートドアロックの仕様は、おおまかに言うとこんな感じ。

 ブラウザからの操作は基本、在宅時でも外出時でも鍵のロック状態確認がメイン、自作中のドアホン完成後なら、併用して親類・友人・知人来訪時に使うかどうかというところ。

 尚、仕様変更で理由などは詳述しますが、カード/タグ認証は、Mifareのみとし、FeliCa、そして増設つまみについては、今回は見送ることにしました。

 気づけば、自身が参考にさせて頂いたRFID/Keypad/指紋認証併用の参照先やWi-FiとBluetoothの併用は別として、世の自作スマートロックのほとんどは、ロック機構部の実現に重きを置いているのか、宅内ボタンはともかく、顔認証、指紋認証、ICカード認証、ノック(リズム)認証、ブラウザを介して開閉、Bluetooth内蔵スマホで近づいた時・離れた時に開・閉、オートロック等々、何れかのみの単機能なものな模様。

 一方、1〜3万円ほどで買える市販スマートロックも基本スペックは単機能、また、遠隔ロックするためのWi-Fiユニットが別売りだったりする模様。

 自身は、構想当初から、前述の機能は必須かなというところからスタートしたのですが、結果、期せずして贅沢なハイスペック仕様になってしまったようです。

 ちなみにオートロック含め、ロック・アンロックにおける他の方法や各種認証を利用しなかった理由については、仕様決定までの経緯で触れています。

もくじ

認証パネルシステム概要

自作スマートドアロックFeliCa/Mifareカード/RFIDタグとキーパッドパスワード認証パネル

 ドア外に設置の認証パネル。

 左が扉付きキーパッド、その右側のスペースがRFID認証パネル、中間上部の黒く見える部分の小さな窓はLED用でキーパッドの扉を開いた時にも視認可能。

 この位置まで雨が吹き込むことは、まず滅多にないものの、ケースの蓋裏面全面には、簡易な防水を兼ねつつ、キーパッド保護用に液晶保護フィルムを、ケース外側開閉付け根部分とキーパッド扉の同部分に90度折り曲げ可能な透明防水テープを貼付。

自作スマートドアロック扉付きキーパッド/写真左側とRFID認証パネル/写真右側スペース

 キーパッドの扉をチラッと開けたところ。

 キーパッドに登録済みパスワードを入力するか、登録済みNFCカードやタグ、シールを貼ったものを認証パネルに近づけるか当てるとキーロック側にパスワード、またはカード・タグのIDが送信され、NGならレッド、OKならグリーンにLEDが点灯し、グリーンの場合、キーロック側センサーで鍵が開いているか閉まっているかを検出、結果に応じて解錠・施錠。

 写真左に見える黒い帯は、撤去することも考慮し、養生テープ+シリコンテープ、更にケース付近ではLEDテープ用シリコンチューブで防水処理、ちょっとしたテープ1枚でも開閉できなくなる玄関ドアの隙間を通したUSB用フレキシブルフラットケーブル/FFC。

壁に超強力両面テープで貼ったPVC製フックに掛けた取り外し可能な自作スマートドアロック認証パネル

 認証パネルは簡単に取り外せるように引っ掛け式。

 壁に凹凸面でも効果抜群の超強力両面テープで水回りでも使えるというPVC製透明フックを貼り付け、ケース裏側にガタ付き防止の発泡スチロール貼付とケース加工時の端材やスペーサでパネルケース側に引っ掛ける部分を追加工。

 追加工が終わってみれば、何もわざわざ、わずかながらも水の侵入の可能性を高めずとも穴あけ不要な金属プレート+(ケース内から)磁石にするのもよかったかと思いつつも、億劫でそのまま採用。

 ケースごと完全に外す場合は、コントローラに給電しているFFC/Flexible Flat Cable先端USBプラグモジュールのロックを解除、FFCを外してケースから抜くことで可。

 ただ、回路は、取り外し可能な同ケース中蓋に固定してあり、回路を取り出すだけなら、ケースを開けてコントローラからUSBプラグを抜き、中蓋を外すだけでOK。

 盗難・持ち去りの可能性は(あるわけないし、そんな物好きいるはず)ないものと想定している一方、後日、構想自体は先行していた自作テレビドアホンを認証パネル上に位置するインターホン・ドアホン専用スイッチボックス内に取り付け予定なので、尚、心配無用かと。

ロック・アンロック機構システム概要

自作スマートドアロック施錠・解錠機構

 我が家のドアの鍵は、MIWA(美和)ロック製でサムターンが楕円の円柱形?のもの。

 そんなサムターン周辺に設置のドアを解錠・施錠するドアロック・アンロック構造物。

 ドア外の認証パネルからのパスワードやIDを受信、認証、OK/NG結果送信、認証OKなら解錠・施錠。

 また、宅内でLAN上にあるパソコンやスマホ、タブレットから、外からはVPN経由で鍵のライブ映像を表示したブラウザ上のボタン押下で施錠・解錠。

 ドア内側からは、ボタン(レバー)で施錠・解錠。

 そのボタンとなるのが、ケース上に伸びる白いプレートでケース内のタクトスイッチを押下することで機能するレバースイッチ。

 尚、施錠・解錠については、モーションセンサーで傾きを検知、その状態に応じて作動。

 ロック機構の固定については、幸いなことにドアが内外ともに金属製ゆえ磁石。

 気づけば4年以上前に買ったWi-Fiドアベルの押しボタンもホットボンドで貼り付けたHDDから部品取りした超々強力な磁石の磁力でドア外面に固定、外そうにも容易には取れないほどドアとガッツリ、すっかり一体化していたほど。

 ちなみにその電源は、電池で、なんと12V/23A、ドアベル以前の5年以上前にそんなんあるんか!?と5本セット買ってあったものの、付属電池の初交換は、つい数日前という長持ちっぷり。

 そんな古い3.5インチHDDから部品取りした超々強力な金属プレート付き磁石や磁力はやや弱いながら2.5インチHDDから部品取りしたものなど、いくつか持っていたりします。

 その内、3.5インチHDDの超々強力な金属プレート付き磁石+追加購入した補助の強力丸磁石2個でケースに固定したL字アングルステーを挟んでドアと磁力で固定。

 加えてサムターンをつまんでいる(スライドさせて挿抜できる)恰好のグリップと給電用プラグがつながっているのみ。

 よって給電プラグを抜き、手で外そうとすると、かなり強力ながら磁石をドアから、グリップをサムターンから、(サムターンの向きに応じた方向に)ズラすように外すことで簡単に脱着可能。

 構造的には、磁石を挟んでアングルステーの穴とケース、タミヤのユニバーサルアームをボルト+スプリングワッシャー+平ワッシャー+ナット締め。

 モータ2箇所の取り付け穴を挟むように組んだユニバーサルアーム2本にモータを固定、モータカップリング、スペーサー、歯車と共締めしたパイプ椅子グリップ、同軸上に共に回転するようサムターンの角度を検出するセンサーを設置。

 当初構想製作、よりコンパクトで丸いドアノブより内側、少なくとも面一ほどに収まるはずだった、モータ軸がドアノブ方向を向く恰好のモータ・中間・サムターン用の3枚ギア構成の構造物の強度不足により、急遽、写真のようにモータ軸延長上でサムターンを回す構造に変更。

自作スマートドアロックへの配線と配線モール

 給電は、12V 2AのACアダプタによる有線、ドア軸方向に対して前述の追加購入した複数個の強力丸磁石で配線モールを横方向と縦方向2本固定、ドア開閉を考慮し、遊び部分を配線チューブで包みつつ配線。

 ただ、手で外そうとすると容易ではない強力な磁力ながら、さすがに服がひっかかったり、身体が激突するようなことがあると簡単にズレたり、落下したり、そうなると給電ケーブルから芋づる式に磁石留めの配線モールも落下したりすることも。

 よって奥行きも限りなく薄くでき、なんならサムターン部以外は設置の自由度が上がり、ひっかけたり、激突のしようもない、まさにスマートな構成にすることも可能な複数枚ギア構成を再検討の上、うまく機能すれば差し替え予定。

 また、当初の構想では、樹脂製ギアに固定用ビスのタップを立てステッピングモータ軸に直接はめ込むべく、中学時代、技術の授業で真鍮にタップを立てて以来ながら、工具を買ってやってみたところ2箇所の内、1箇所失敗、うまく固定できず、宿題として今回は断念したものの、これができればカップリング分だけ更に薄型にできる為、再チャレンジ予定。

 => [2024/05/21]
 現行構造でも滑りが出てホットボンドでごまかしていましたが、タップに再挑戦してみたら、前回はM3に対し2.0mmと下穴径を間違えていたのも一因だったようで下穴を2.5mmとしたからか、KHK樹脂歯車DS1-28軸上の2箇所のタップ、上手くいきました。
 よって当初構想の構造に替える際もカップリング分の空間を省いて28BYJ-48を直接固定できます。

 これに伴い、丸ノブのまま回せなくもないものの、角度的に干渉しやすい為、買ったのが丸ノブに装着できるドアレバー。

 他のメーカーなら色も選べたものの、構造上、最も安定しているように感じ選定、なおかつ、見た中では最も安価で尚良しということで。

 お気に入りのダイソーの『らくらく蛇口レバー』があるくらいだから、100円ショップにはないにせよ、玄関ドアの丸ノブ用もあるでしょと探したら、まさに数種あり、市販のスマートドアロックを買った丸ノブな人々も愛用している模様でそうしたレビューも多数。

 尚、サムターン付近のギアは3枚ギア構造物の名残で、この代替した構造上、当然ながら歯車である必要なし...。

回路

 ドア内外でESP32開発ボード2つ、Web操作含むESP32カメラボード1つとマイコンは3つ。

 ドア内外の2つのESP32ボードについては、CPUコアを2つ(デュアルコア・マルチコア)に加え、複数タスク(マルチタスク)を使用。

自作スマートドアロックFeliCa/Mifareカード/RFIDタグとキーパッドパスワード認証 by Fritzing

 ドア外の認証パネルにおいては各種タグに対応のRFIDリーダー/ライターモジュールPN532/ELECHOME NFC MODULE V3を使って電子マネーや交通系ICカード、各種RFIDカードなどのカードキー・タグキー、これらに対応のスマホやスマートウォッチなどのスマートデバイス、もしくは、メンブレンマトリックス4x4キーパッドによるパスワード入力、ライブ映像が表示されたブラウザ上のボタン押下、室内のボタン押下(か物理的な金属製の鍵)の何れかでロック(施錠)・アンロック(解錠)。

 ブラウザ操作については、VPN経由で外出先からライブ映像を確認しながらのロック・アンロックも可。

自作スマートドアロックにおけるサムターンのロック・アンロック by Fritzing

 TCP通信でドア外の認証パネルからキーを送信、これをドア内ロックシステムが受信、認証、結果に応じたALLOW/DENY応答によって認証パネル上にLEDのグリーン/レッドで色分け表示、加速度・ジャイロ6軸モーションセンサーで開閉状態(サムターンの向き)を検知しつつ、バイポーラに改造したお気に入りのユニポーラ激安ステッピングモーター28BYJ-48-5V+モータードライバDRV8825でサムターンを回し、状態に応じて施錠・解錠。

 我が家の場合、平常の静けさなら鍵を開け閉めする程度のドア際なら外からでも、なんともカッコよく、心地よい響きでウィーーンといった微かなモータ動作音も確認可能。

 電源断の際はシャフトはフリー、また、モータードライバDRV8825+ステッピングモータ28BYJ-48は、駆動していない時はシャフトフリーになるように配線、相応にプログラムすることで電源の供給の有無によらず、フリーとは言え、28BYJ-48なりの僅かな重みはありつつも物理鍵による解錠・施錠が可能。

 尚、モーションセンサーが引っかかったり、固定が緩んだり、それによって想定角度外になったりといった有事の際に備え、開閉に必要な一定の時間を測定の上、所定角度範囲への到達か、経過時間、何れか早い方でモータを停止すべくプログラム。

 結果、仮に電動動作しない場合でもモーションセンサー周りは機能に支障ない一方で物理鍵での開閉を邪魔しない程度に固定してあり、万一、センサー構造物が他の装置部分と引っかかることがあったとしても締め出される可能性はまずなし。

 Wi-Fi・ブラウザを介したドアロック・アンロックボタンは、宅内から、もしくはVPN経由で外出先からロック状態の確認を主としてロック・アンロックボタンを映像・ストリミーング、スマート機器用操作パネルホームへのボタンと共にESP32カメラWebサーバ上に実装、ドアロック用ESP32ボードをWebsocketサーバとしてESP32カメラサーバ上のボタン押下時にWebsocket通信することで認証なし、同様の機構で施錠・解錠。

 万一、ハッキングされて侵入された場合、ここはESP32ボード側で認証しておくべきか?と思わなくもありませんが、何れにせよ後で考えることに。

 ドア内では、押しボタンスイッチにより、(当然、認証なしの)同様の機構で施錠・解錠。

 電源と配線については、

 ドア外側の認証パネルについては、防水も考慮しつつ、ドアやサッシにも配線できる薄いFFC/Flexible Flat Cable(Flat Flexible Cable)タイプのUSBケーブル+USB充電器+スイッチ付きコンセントで給電(建物既存のスイッチボックスに設置の自作ドアホン用ESP32カメラも同様にする予定)。

 ドア内側のロック機構については、ドアの開閉軸側経由で配線モールを設置(固定は数個の磁石)、ドアの軸付近は、開閉時支障ないように、かつ、噛まない程度に配線モールなしのケーブルに遊びを設け、ACアダプタ12V+ACアダプタコネクタ付きケーブル+配線ケーブル+ACアダプタプラグ付きケーブルを個別スイッチ付きコンセントから給電。

解錠・施錠方法

 解錠・施錠方法について。

 NFC/RFIDカードキー・タグ・シールキー、対応スマホ・スマートウォッチなどについては、パネルに近づけるだけ。

 キーパッドについては、後掲スケッチでは、指定した桁数のパスワード(暗証番号)の後に[*]を、入力をやり直すには、[#]を押すだけ。

 ブラウザについては、ライブ映像を確認しつつ、[施錠/解錠]ボタンをタップ・クリックするだけ。

 ボタン(レバー)は、押すだけ。

 何れの方法もモーションセンサーでサムターンの傾きを検出し、施錠されていれば解錠、解錠されていれば施錠します。

 ストレスフリーなこともあり、正確に計測したことはありませんが、どの方法もタイムラグは、ないようなもの、あって1秒くらいでしょうか。

 物理鍵で開閉する方法については、言わずもがな。

スケッチ共通事項と連携

 ドア内外とWeb操作機能付きカメラ、3つのスケッチ共にmDNS(Avahi/Bonjour)対応としつつ、Android対策でIP固定、無線アップロードArduinoOTA対応、送受信は、ルーターを介したLAN内TCP通信。

 ドア外の認証パネルについては、メッセージ内に仕込んだID/パスワードをパネルからドア内へ、ALLOW/DENYをドア内からドア外パネルへ送信する恰好で認証。

 続けてドア内ロック・アンロック機構も同様に、サムターンと共に回転するよう仕込んだモーションセンサーで角度を検出し、サムターンの向きを検知しつつ、状態に合わせて作動(解錠・施錠)。

 認証パネルではRFIDとキーパッド、ドア内ロックについては認証パネル操作とブラウザ経由のWebロック、ボタンスイッチ及びモーションセンサー用に何れもFreeRTOS(リアルタイムOS)搭載のESP32のCPUにおいてデュアルコア(コア2つ)+追加タスクでマルチタスク。

 コアもタスクも同じxTaskCreatePinnedToCore関数でコアを何れにするかによってマルチコア(ESP32の場合、0か1別々のコア)、マルチタスク(同一コア上で別タスク)、これらの併用を使い分けできます。

 ESP32のCPUにおいてデュアルコア(コア2つ)、マルチタスク(CPUコアに関わらず複数タスク)を使うにあたり、タスク関数名とタスクハンドル名を同じにするとエラーになるので注意。

 各タスクのスタックサイズについては、限界まで追い込んだわけではないものの、大小共に過ぎたるは動作・挙動に影響があるにせよ、それほど制限はきつくなさ気、1000もしくは、1024の倍数が無難と思われ、今回は後者を採用、処理に応じて十分なサイズを模索することになるでしょう。

 今回の処理については、1024だと支障が出るケースがあった一方、コア・スタック数との兼ね合いもあるでしょうが、2048、3072、4096、5120、6144、7168、8192、10000などでも動作しました。

 実は、知らず知らずに決まった値に置き換えられているなんてこともあったりするのかも?しれませんが。

 コア・タスクには優先順位(1〜24、大きいほど優先度が高い)があります。

 今回認証パネル側のカードとパスワードの読み取り・送信については、何れも甲乙つけがたいので共に1とし、期待する動作をしていますが、一方を1増やしただけで機能しないなど動作に影響が出たこともあり、これも処理に応じて設定する必要があるようです。

 デュアルコアでコアを別にした認証パネル側については、コアが別なのであまり意味はなさないでしょうが、それぞれ優先度1としました。

 デュアルコアでタスクを4つとしたドアロック機構側については、幾通りか試した結果、サムターン角度の計算のみ優先順位3、キーパッド認証とカード・タグ認証のみ通常のコアと同一のcore1、ブラウザボタンスイッチ、物理ボタンスイッチ、それぞれの応答、解錠・施錠タスクをcore0でデュアルコアとしつつ、優先度は何れも1としました。

 また、センサー値をリアルタイムに受け取るべく、今回は、キュー(xQueueCreate()/xQueueOverwrite()/xQueueReceive()など)を使いました。

 サンプルスケッチでは、パスワードの桁数は固定、6桁としてあり、より強固にするには、双方のスケッチで桁数を変更のこと。

 また、サーバ(ロック機構)側では、FeliCa、MIFARE、パスワードを同一の配列に入れていますが、数が多ければ多いほど、別にした方がよりスマートであり、実感できるレベルか否かは別として認証にかかる時間もより短縮できるかなと。

 そういえば、この配列、Arduino IDEでWarningが出ていて深く考えることもなく無視しましたが、修正?するか、もしくは、他の方法の方が良さ気。

ESP32/玄関外ゲート側スケッチ

// ドアロック・アンロックシステム/認証パネル
// ESP32 TCPクライアント
// RFID&KeyPad入力/転送 + OK/NG LED
#include <WiFi.h>
#include <Arduino.h>
#include <ArduinoOTA.h>
#include <ESPmDNS.h>
 
#include <Keypad.h>
 
#include <Wire.h>
#include <PN532_I2C.h>
#include <PN532.h>
 
PN532_I2C pn532i2c(Wire);
PN532 nfc(pn532i2c);
 
#include <PN532_debug.h>
 
#define RED_LED 13
#define GREEN_LED 16
#define BULE_LED 17
 
const String IMYME = "DoorKeyer";
const String MES_ALLOW = "ALLOW";
const String MES_DENY = "DENY";
const String MES_RECEIVED = "RECEIVED";
const String NUM_RESET = "0000";
 
const char* ssid     = "SSID";
const char* password = "PASSPHRASE";
 
const char* serverAddress = "192.168.1.251";
const int serverPort = 12345;
 
const char common_name[40] = "esp32_door_keyer";
const char* OTAName = common_name;
const char* mdnsName = common_name;
 
IPAddress ip(192, 168, 1, 250);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
 
WiFiClient TCPkeyer;
 
String idcard;
String key_val;
int key_digit = 6; // パスワード桁数
 
const byte ROWS = 4;
const byte COLS = 4;
char hexaKeys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};
byte row_pins[ROWS] = {23, 19, 18, 27};
byte col_pins[COLS] = {26, 25, 33, 32};
 
Keypad customKeypad = Keypad( makeKeymap(hexaKeys), row_pins, col_pins, ROWS, COLS);
 
TaskHandle_t Task1;
TaskHandle_t Task2;
 
void setup() {
  Serial.begin(9600);
  Serial.println("ESP32: TCP CLIENT + send ID & turn on Color LED according to ret");
 
  nfc.begin();
 
  uint32_t versiondata = nfc.getFirmwareVersion();
  if (!versiondata) {
    Serial.print("Didn't find PN53x board");
    while (1) {
      delay(1);
    };
  }
 
  Serial.print("Found chip PN5"); Serial.println((versiondata >> 24) & 0xFF, HEX);
  Serial.print("Firmware ver. "); Serial.print((versiondata >> 16) & 0xFF, DEC);
  Serial.print('.'); Serial.println((versiondata >> 8) & 0xFF, DEC);
 
  // 最大試行回数
  nfc.setPassiveActivationRetries(0xFF);
 
  nfc.SAMConfig();
 
  pinMode(GREEN_LED, OUTPUT);
  pinMode(RED_LED, OUTPUT);
  pinMode(BULE_LED, OUTPUT);
 
  xTaskCreatePinnedToCore(
    Task4Rfid,   /* タスク関数 */
    "Task1",     /* タスク名 */
    8192,        /* タスクのスタックサイズ */
    NULL,        /* タスクのパラメータ */
    1,           /* タスクの優先順 */
    &Task1,      /* 生成したタスクのトラックを維持するためのタスクハンドル */
    0);          /* core 0へのピンタスク */
  delay(500);
 
  xTaskCreatePinnedToCore(
    Task4Keypad, /* タスク関数 */
    "Task2",     /* タスク名 */
    4096,        /* タスクのスタックサイズ */
    NULL,        /* タスクのパラメータ */
    1,           /* タスクの優先順 */
    &Task2,      /* 生成したタスクのトラックを維持するためのタスクハンドル */
    1);          /* core 1へのピンタスク */
  delay(500);
 
  startWiFi();
  startOTA();
  startMDNS();
 
  if (TCPkeyer.connect(serverAddress, serverPort)) {
    Serial.println("Connected to TCP server");
  } else {
    Serial.println("Failed to connect to TCP server");
  }
}
 
void loop() {
  ArduinoOTA.handle();
  delay(10);
}
 
void Task4Rfid( void * pvParameters ) {
  Serial.print("Task1 running on core ");
  Serial.println(xPortGetCoreID());
 
  for (;;) {
    read_card();
  }
  delay(1);
}
 
void Task4Keypad( void * pvParameters ) {
  Serial.print("Task2 running on core ");
  Serial.println(xPortGetCoreID());
 
  for (;;) {
    read_key();
  }
  delay(1);
}
 
void read_card() {
  bool success ;
  uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 };  // Buffer to store the returned UID
  uint8_t uidLength;
 
  success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, 10);
  if (success) {
    idcard = "";
    for (byte i = 0; i <= uidLength - 1; i++) {
      idcard += (uid[i] < 0x10 ? "0" : "") +
                String(uid[i], HEX);
    }
    if (!TCPkeyer.connected()) {
      Serial.println("Connection is disconnected");
      TCPkeyer.stop();
 
      if (TCPkeyer.connect(serverAddress, serverPort)) {
        Serial.println("Reconnected to TCP server");
      } else {
        Serial.println("Failed to reconnect to TCP server");
      }
    }
    if (TCPkeyer.connect(serverAddress, serverPort)) {
      TCPkeyer.println(IMYME + ": key n" + idcard + "\r");
      delay(1000);
      //Serial.println("idcard " + idcard);
      String answer = TCPkeyer.readStringUntil('\r');
      //Serial.println("from " + answer);
      if (answer.indexOf("x") >= 0) {
        String res = answer.substring(answer.indexOf("x") + 1, answer.length());
        Serial.print("response from DoorLocker : ");
        Serial.println(res);
        if (res == MES_ALLOW) {
          Serial.println("GREEN LED ON");
          digitalWrite(GREEN_LED, HIGH);
          delay(1000);
          digitalWrite(GREEN_LED, LOW);
        } else if (res == MES_DENY) {
          Serial.println("RED LED ON");
          digitalWrite(RED_LED, HIGH);
          delay(1000);
          digitalWrite(RED_LED, LOW);
        }
      }
    } else {
        Serial.println("Connection is disconnected on else state");
        TCPkeyer.stop();
 
        if (TCPkeyer.connect(serverAddress, serverPort)) {
            Serial.println("Reconnected to TCP server 2");
        } else {
            Serial.println("Failed to reconnect to TCP server 2");
        }
        }
    }
  }
  delay(10);
}
 
void reset(String keyval) {
  if (!TCPkeyer.connected()) {
    Serial.println("Connection is disconnected");
    TCPkeyer.stop();
 
    if (TCPkeyer.connect(serverAddress, serverPort)) {
      Serial.println("Reconnected to TCP server");
    } else {
      Serial.println("Failed to reconnect to TCP server");
    }
  }
  if (TCPkeyer.connect(serverAddress, serverPort)) {
    Serial.println("send keyval to server");
    TCPkeyer.println(IMYME + ": key n" + keyval + "\r");
    delay(1000);
    String answer = TCPkeyer.readStringUntil('\r');
    Serial.println("from " + answer);
    if (answer.indexOf("x") >= 0) {
      String res = answer.substring(answer.indexOf("x") + 1, answer.length());
      Serial.print("response: ");
      Serial.println(res);
      if (res == MES_RECEIVED) {
        Serial.println("GREEN LED ON");
        digitalWrite(GREEN_LED, HIGH);
        delay(500);
        digitalWrite(GREEN_LED, LOW);
        delay(500);
        digitalWrite(GREEN_LED, HIGH);
        delay(500);
        digitalWrite(GREEN_LED, LOW);
        delay(500);
        digitalWrite(GREEN_LED, HIGH);
        delay(500);
        digitalWrite(GREEN_LED, LOW);
        //delay(100);
        //ESP.restart();
      }
    }
  }
}
 
void read_key() {
  char Key = customKeypad.getKey();
  if (Key) {
    if (Key >= '0' && Key <= '9' || Key == 'A' || Key == 'B' || Key == 'C' || Key == 'D') {
      key_val += Key;
    } else if (Key == '#') {
      if (key_val.length() > 0) {
        if (key_val == NUM_RESET) {
          reset(key_val);
        }
        key_val = "";
      }
    } else if (Key == '*') {
      /*
        Serial.println(key_val);
        Serial.print(" key_val.length() : ");
        Serial.println(key_val.length());
      */
      if (key_val.length() == key_digit) {
        if (!TCPkeyer.connected()) {
          Serial.println("Connection is disconnected");
          TCPkeyer.stop();
 
          if (TCPkeyer.connect(serverAddress, serverPort)) {
            Serial.println("Reconnected to TCP server");
          } else {
            Serial.println("Failed to reconnect to TCP server");
          }
        }
        if (TCPkeyer.connect(serverAddress, serverPort)) {
          Serial.println("send key_val to server");
          TCPkeyer.println(IMYME + ": key n" + key_val + "\r");
          delay(1000);
          String answer = TCPkeyer.readStringUntil('\r');
          Serial.println("from " + answer);
          if (answer.indexOf("x") >= 0) {
            String res = answer.substring(answer.indexOf("x") + 1, answer.length());
            Serial.print("response: ");
            Serial.println(res);
            if (res == MES_ALLOW) {
              Serial.println("GREEN LED ON");
              digitalWrite(GREEN_LED, HIGH);
              delay(1000);
              digitalWrite(GREEN_LED, LOW);
            } else if (res == MES_DENY) {
              Serial.println("RED LED ON");
              digitalWrite(RED_LED, HIGH);
              delay(1000);
              digitalWrite(RED_LED, LOW);
            }
            key_val = "";
          }
        }
      } else {
        key_val = "";
      }
    }
  }
  delay(10);
}
 
void startWiFi() {
  WiFi.disconnect();
 
  //  if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
  if (!WiFi.config(ip, gateway, subnet)) {
    Serial.println("STA Failed to configure");
  }
  //  WiFi.softAP(ssid, password);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while ( WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("SSID \"");
  Serial.print(ssid);
  Serial.println("\" started\r\n");
 
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.println("");
}
 
void startMDNS() {
  MDNS.begin(mdnsName);
  Serial.print("mDNS responder started: http://");
  Serial.print(mdnsName);
  Serial.println(".local");
}
 
void startOTA() {
  ArduinoOTA.setHostname(OTAName);
  //  ArduinoOTA.setPassword(OTAPassword);
 
  ArduinoOTA.onStart([]() {
    Serial.println("Start");
    // turn off the LEDs
    for (int i = 0; i < 6; i++) {
      //      digitalWrite(LED_BUILTIN, HIGH);
      //      digitalWrite(LED_BUILTIN, LOW);
    }
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\r\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("End Failed");
  });
  ArduinoOTA.begin();
  Serial.println("OTA ready\r\n");
}

 ドアロック・アンロックシステムの認証パネル(ドア外RFIDモジュール/キーパッド)側のスケッチはこんな感じ。

ESP32/玄関内認証・鍵側スケッチ

// ドアロック・アンロックシステム/ロック機構
// ESP32 TCPサーバ+Websocketサーバ
// FeliCa/Mifare等RFIDタグ/Keypadパスワード認証 + Webロック + ボタンロック
// Bipolar改 28BYJ-48 5V + drv8825駆動 + MPU6050
 
#include <WiFi.h>
#include <Arduino.h>
#include <ArduinoOTA.h>
#include <ESPmDNS.h>
 
#include <SPIFFS.h>
#include <FS.h>
//#include <ESP32WebServer.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
 
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
 
const int led_pin = 32;
const int push_btn_pin = 33;
 
const int Enable = 23;
const int Reset = 19;
const int Sleep = 18;
const int Dir = 17;
const int Step = 16;
 
Adafruit_MPU6050 mpu;
 
float yaw = 0.0;
float gyroBiasZ = 0.0;
unsigned long lastTime = 0;
 
const String IMYME = "DoorLocker";
const String MES_ALLOW = "ALLOW";
const String MES_DENY = "DENY";
const String MES_RECEIVED = "RECEIVED";
const String NUM_RESET = "0000";
 
const char* ssid     = "SSID";
const char* password = "PASSPHRASE";
 
const char common_name[40] = "esp32_door_locker";
const char* OTAName = common_name;
const char* mdnsName = common_name;
#define serverPort 12345
 
IPAddress ip(192, 168, 1, 251);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
 
WiFiServer TCPlocker(serverPort);
 
//ESP32WebServer server(80);
WebServer server(80);
WebSocketsServer webSocket(81);
 
File fsUploadFile;
 
const char* path_root = "/index.html";
#define BUFFER_SIZE 16384
uint8_t buf[BUFFER_SIZE];
const char* path_maincss = "/main.css";
const char* path_swcss = "/sw_design.css";
//const char* path_js = "/script.js";
 
static char* registered_auth_id[] = {"a1b2c3d4", "e5f6a7b8", "c9d0e1f2", "c3d4e5f6", "123456", "567890", "ABCD12", "ABCD123456789ABC"};
const static int element_num = 7;
const static int felica_digit = 16; // FeliCa桁数
const static int id_digit = 8; // MIFAREカード桁数
const static int key_digit = 6; // パスワード桁数
 
const long open_range_begin = 0;
const long open_range_end = 30;
const long close_range_begin = 55;
const long close_range_end = 95;
const long half_range = 45;
 
const int StepsPerRevolution = 256;
const int StepDelay = 1000;
 
clock_t startc, endc;
unsigned int idifftime = 0;
boolean emergencyflg = false;
const unsigned int lock_unlock_time = 1;
 
bool weblocknum = false;
 
TaskHandle_t AuthLock;
TaskHandle_t ButtonLock;
TaskHandle_t WebLock;
TaskHandle_t MPUTask;
 
QueueHandle_t rollQ;
QueueHandle_t rollQview;
const int Qmax = 1;
const int Qsize = 3; // 0-90度
 
void startWiFi() {
  WiFi.disconnect();
 
  //  if (!WiFi.config(ip, gateway, subnet, primaryDNS, secondaryDNS)) {
  if (!WiFi.config(ip, gateway, subnet)) {
    Serial.println("STA Failed to configure");
  }
  //  WiFi.softAP(ssid, password);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while ( WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("SSID \"");
  Serial.print(ssid);
  Serial.println("\" started\r\n");
 
  Serial.print("Connected to ");
  Serial.println(ssid);
 
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
 
  Serial.print("ESP32 #2: TCP Server IP address: ");
  Serial.println(WiFi.localIP());
  Serial.println("ESP32 #2: -> Please update the serverAddress in ESP32 #1 code");
  /*
    Serial.print("hostname : ");
    Serial.println(WiFi.hostname());
  */
  Serial.println("");
}
 
void startMDNS() {
  MDNS.begin(mdnsName);
  Serial.print("mDNS responder started: http://");
  Serial.print(mdnsName);
  Serial.println(".local");
}
 
void startOTA() {
  ArduinoOTA.setHostname(OTAName);
  //  ArduinoOTA.setPassword(OTAPassword);
 
  ArduinoOTA.onStart([]() {
    Serial.println("Start");
    // turn off the LEDs
    for (int i = 0; i < 6; i++) {
      //      digitalWrite(LED_BUILTIN, HIGH);
      //      digitalWrite(LED_BUILTIN, LOW);
    }
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\r\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
    else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
    else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
    else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
    else if (error == OTA_END_ERROR) Serial.println("End Failed");
  });
  ArduinoOTA.begin();
  Serial.println("OTA ready\r\n");
}
 
void setup() {
  Serial.begin(9600);
 
  pinMode(led_pin, OUTPUT);
  pinMode(push_btn_pin, INPUT_PULLUP);
 
  // ステッピングモータドライバDRV8825|A4988設定start
  pinMode(Enable, OUTPUT);
  pinMode(Reset, OUTPUT);
  pinMode(Sleep, OUTPUT);
 
  //digitalWrite(Enable, LOW); // LOW:DRV8825有効(デフォルト)|HIGH:無効:シャフトフリー
  digitalWrite(Enable, HIGH); // 無効:シャフトフリー
  // Reset/Sleep 一方HIGH、他方LOWでもシャフトフリー
  // Reset/Sleepは、共にLOW:する(デフォルト)|HIGH:しない
  digitalWrite(Reset, HIGH); // リセットしない
  digitalWrite(Sleep, HIGH); // スリープしない
 
  pinMode(Dir, OUTPUT);
  pinMode(Step, OUTPUT);
 
  digitalWrite(Dir, LOW); // 方向 LOW|HIGHで反転
  digitalWrite(Step, LOW); // ステップ LOW:(停止/デフォルト)|HIGH:1ステップ動く
  // DRV8825設定end
 
  Serial.println("ESP32 #2: TCP SERVER + ID Check & return OK/NG + key lock/unlock");
 
  xTaskCreatePinnedToCore(
    AuthLocker,  /* タスク関数 */
    "AuthLock",  /* タスク名 */
    1024 * 5,        /* タスクのスタックサイズ */
    NULL,        /* タスクのパラメータ */
    1,           /* タスクの優先順 */
    &AuthLock,   /* 生成したタスクのトラックを維持するためのタスクハンドル */
    0);          /* core 0へのピンタスク */
  delay(500);
 
  xTaskCreatePinnedToCore(
    ButtonLocker,
    "ButtonLock",
    1024 * 3,
    NULL,
    1,
    &ButtonLock,
    1);
  delay(500);
 
  xTaskCreatePinnedToCore(
    WebLocker,
    "WebLock",
    1024 * 3,
    NULL,
    1,
    &WebLock,
    1);
  delay(500);
 
  rollQ = xQueueCreate(Qmax, Qsize);
  rollQview = xQueueCreate(Qmax, Qsize);
 
  startWiFi();
  startOTA();
  startMDNS();
 
  startSPIFFS();
 
  TCPlocker.begin();
 
  startWebSocket();
  startServer();
}
 
void loop() {
  ArduinoOTA.handle();
  webSocket.loop();
  server.handleClient();
  delay(10);
}
 
void timeLapse(int open_close) {
  if (emergencyflg) {
    delay(100);
    digitalWrite(Enable, HIGH);
    emergencyflg = false;
    delay(500);
  } else {
    digitalWrite(Enable, LOW);
    if (open_close == 0) {
      digitalWrite(Dir, HIGH);
      for (int i = 1; i <= StepsPerRevolution; i++) {
        digitalWrite(Step, HIGH);
        delayMicroseconds(StepDelay);
        digitalWrite(Step, LOW);
        delayMicroseconds(StepDelay);
      }
    } else if (open_close == 1) {
      digitalWrite(Dir, LOW);
      for (int i = 1; i <= StepsPerRevolution; i++) {
        digitalWrite(Step, HIGH);
        delayMicroseconds(StepDelay);
        digitalWrite(Step, LOW);
        delayMicroseconds(StepDelay);
      }
    }
  }
  endc = clock();
  delay(1);
  idifftime = (endc - startc) / CLOCKS_PER_SEC;
  if (idifftime >= lock_unlock_time) {
    emergencyflg = true ;
    //Serial.println("Time Lapse : " + idifftime);
  }
}
 
void directionThumb() {
  sensors_event_t a, g, temp;
  mpu.getEvent(&a, &g, &temp);
 
  float ax = a.acceleration.x;
  float ay = a.acceleration.y;
  float az = a.acceleration.z;
 
  float pitch = atan2(ay, sqrt(ax * ax + az * az)) * 180.0 / PI;
  float froll = atan2(-ax, az) * 180.0 / PI;
 
  if (froll >= 0) {
    unsigned int roll = (unsigned int)froll;
    xQueueOverwrite(rollQ, &roll);
    xQueueOverwrite(rollQview, &roll);
    Serial.print("xQueueOverwrite(rollQ, &roll) roll : ");
    Serial.println(roll);
    vTaskDelay(100);
  }
  unsigned long currentTime = millis();
  float deltaTime = (currentTime - lastTime) / 1000.0;
 
  float gyroZ = g.gyro.z - gyroBiasZ; // Subtract bias
  yaw += gyroZ * deltaTime;
  lastTime = currentTime;
 
  delay(250);
}
 
void calibrateGyro() {
  const int numReadings = 100;
  gyroBiasZ = 0.0;
  for (int i = 0; i < numReadings; i++) {
    sensors_event_t a, g, temp;;
    mpu.getEvent(&a, &g, &temp);
    gyroBiasZ += g.gyro.z;
    delay(10);
  }
  gyroBiasZ /= numReadings;
}
 
void startMPU() {
 
  if (!mpu.begin()) {
    Serial.println("Failed to find MPU6050 chip");
    while (1) {
        delay(10);
    }
  }
 
  Serial.println("MPU6050 Found!");
 
  mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
  mpu.setGyroRange(MPU6050_RANGE_250_DEG);
  mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
 
  calibrateGyro();
  lastTime = millis();
  delay(100);
}
 
void DirectionCheck( void * pvParameters ) {
  Serial.print("DirectionCheck running on core ");
  Serial.println(xPortGetCoreID());
  startMPU();
 
  for (;;) {
    directionThumb();
  }
  delay(1);
  vTaskDelete(NULL);
}
 
void keyMotor() {
  xTaskCreatePinnedToCore(
    DirectionCheck, /* タスク関数 */
    "DirCheck", /* タスク名 */
    1024 * 4,         /* タスクのスタックサイズ */
    NULL,         /* タスクのパラメータ */
    3,            /* タスクの優先順 */
    &MPUTask,  /* 生成したタスクのトラックを維持するためのタスクハンドル */
    1);           /* core 1へのピンタスク */
  //delay(500);
  delay(1000);
 
  startc = clock();
 
  BaseType_t xStatus;
  unsigned int roll;
  xStatus = xQueueReceive(rollQ, &roll, 0);
  vTaskDelay(100);
  Serial.print("roll Q: ");
  Serial.println(roll);
  UBaseType_t uxNumberOfItems;
  uxNumberOfItems = uxQueueMessagesWaiting( rollQ );
  Serial.print("after remain Q : ");
  Serial.println(uxNumberOfItems);
  if (xStatus == pdPASS) {
    if (open_range_begin <= roll && roll <= open_range_end) {
      Serial.println("Close");
      while (close_range_begin > roll) {
        timeLapse(0);
        if (emergencyflg) {
          emergencyflg = false;
          break;
        }
      }
    } else if (close_range_begin <= roll && roll <= close_range_end) {
      Serial.println("Open");
      while (open_range_end < roll) {
        timeLapse(1);
        if (emergencyflg) {
          emergencyflg = false;
          break;
        }
      }
    }
    delay(100);
    digitalWrite(Enable, HIGH);
    delay(500);
    vTaskDelete(MPUTask);
  } else {
    if (uxQueueMessagesWaiting(rollQ) != 0)
    {
      while (1)
      {
        Serial.println("rtos queue receive error, stopped");
        delay(1000);
      }
    }
  }
}
 
void toKeyer() {
  WiFiClient keyer = TCPlocker.available();
 
  if (keyer) {
    if (keyer.connected()) {
      Serial.print(" ->"); Serial.println(keyer.remoteIP());
      String request = keyer.readStringUntil('\r');
 
      if (request.indexOf("DoorKeyer") == 0) {
        //Serial.print("From ");
        //Serial.println(request);
        int index = request.indexOf(":");
        String keyid = request.substring(request.indexOf("n") + 1, request.length());
        Serial.print("keyid received from DoorKeyer ");
        //Serial.print(keyid);
        Serial.println("");
        keyer.print(IMYME);
        if (keyid.length() == id_digit || keyid.length() == key_digit || keyid.length() == felica_digit) {
          if (keyid == NUM_RESET) {
            keyer.println(": " + keyid + "! result x" + MES_RECEIVED + "\r");
            delay(1000);
            ESP.restart();
          } else {
            int cnt = 0;
            for (int i = 0; i < element_num; i++) {
              if (keyid == registered_auth_id[i]) {
                keyer.println(": " + keyid + "! result x" + MES_ALLOW + "\r");
                keyMortor();
              } else {
                cnt++;
                if (cnt == element_num) {
                  keyer.println(": " + keyid + "! result x" + MES_DENY + "\r");
                }
              }
            }
          }
        }
        keyer.stop();
      }
    }
  }
  delay(1);
}
 
void AuthLocker( void * pvParameters ) {
  Serial.print("AuthLock running on core ");
  Serial.println(xPortGetCoreID());
 
  for (;;) {
    toKeyer();
  }
  delay(1);
  vTaskDelete(NULL);
}
 
void insideLock() {
  int openclose = digitalRead(push_btn_pin);
  if (!openclose) {
    digitalWrite(led_pin, HIGH);
    delay(500);
    digitalWrite(led_pin, LOW);
    delay(500);
    keyMotor();
  }
  delay(1);
}
 
void ButtonLocker( void * pvParameters ) {
  Serial.print("ButtonLock running on core ");
  Serial.println(xPortGetCoreID());
 
  for (;;) {
    insideLock();
  }
  delay(1);
  vTaskDelete(NULL);
}
 
void webLock() {
  if (weblocknum) {
    digitalWrite(led_pin, HIGH);
    delay(500);
    digitalWrite(led_pin, LOW);
    delay(500);
    keyMotor();
    weblocknum = false;
  }
  delay(1);
}
 
void WebLocker( void * pvParameters ) {
  Serial.print("WebLock running on core ");
  Serial.println(xPortGetCoreID());
 
  for (;;) {
    webLock();
  }
  delay(1);
  vTaskDelete(NULL);
}
 
void listDir(fs::FS &fs, const char * dirname, uint8_t levels) {
  //  Serial.printf("Listing directory: %s\n", dirname);
 
  File root = fs.open(dirname);
  if (!root) {
    Serial.println("Failed to open directory");
    return;
  }
  if (!root.isDirectory()) {
    Serial.println("Not a directory");
    return;
  }
 
  File file = root.openNextFile();
  while (file) {
    if (file.isDirectory()) {
    Serial.print("  DIR : ");
    Serial.println(file.name());
    if (levels) {
      listDir(fs, file.name(), levels - 1);
    }
    } else {
    Serial.print("  FILE: ");
    Serial.print(file.name());
    Serial.print("  SIZE: ");
    Serial.println(file.size());
    }
    file = root.openNextFile();
  }
}
 
void startSPIFFS() {
  SPIFFS.begin();
  Serial.println("SPIFFS started. Contents:");
  {
    listDir(SPIFFS, "/", 0);
  }
  Serial.printf("\n");
}
 
void handleFileUpload() {
  HTTPUpload& upload = server.upload();
  String path;
  if (upload.status == UPLOAD_FILE_START) {
    path = upload.filename;
    if (!path.startsWith("/")) path = "/" + path;
    if (!path.endsWith(".gz")) {
    String pathWithGz = path + ".gz";
    if (SPIFFS.exists(pathWithGz))
      SPIFFS.remove(pathWithGz);
    }
    Serial.print("handleFileUpload Name: "); Serial.println(path);
    fsUploadFile = SPIFFS.open(path, "w");
    path = String();
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    if (fsUploadFile)
    fsUploadFile.write(upload.buf, upload.currentSize);
  } else if (upload.status == UPLOAD_FILE_END) {
    if (fsUploadFile) {
    fsUploadFile.close();
    Serial.print("handleFileUpload Size: "); Serial.println(upload.totalSize);
    server.sendHeader("Location", "/success.html");
    server.send(303);
    } else {
    server.send(500, "text/plain", "500: couldn't create file");
    }
  }
}
 
void handleNotFound() {
  if (!handleFileRead(server.uri())) {
    server.send(404, "text/plain", "404: File Not Found");
  }
}
 
boolean readHTML() {
  File htmlFile = SPIFFS.open(path_root, "r");
  if (!htmlFile) {
    Serial.println("Failed to open index.html");
    return false;
  }
  size_t size = htmlFile.size();
  if (size >= BUFFER_SIZE) {
    Serial.print("File Size Error:");
    Serial.println((int)size);
  } else {
    Serial.print("File Size OK:");
    Serial.println((int)size);
  }
  //  htmlFile.read(buf, size);
  htmlFile.close();
  return true;
}
 
String getContentType(String filename) {
  if (filename.endsWith(".html")) return "text/html";
  else if (filename.endsWith(".css")) return "text/css";
  else if (filename.endsWith(".json")) return "text/css";
  else if (filename.endsWith(".js")) return "application/javascript";
  else if (filename.endsWith(".ico")) return "image/x-icon";
  else if (filename.endsWith(".png")) return "image/x-icon";
  else if (filename.endsWith(".gz")) return "application/x-gzip";
  return "text/plain";
}
 
void loadFile(String path) {
  String contents = "";
  String line = "";
  char c;
  File target_file= SPIFFS.open(path, "r");
  if (target_file) {
    Serial.println("file open succeeded!");
    while (target_file.available()) {
    c = target_file.read();
    if (c == '\n' || c == '\r') {
      if (line.length() > 0) {
      contents = contents + line + '\n';
      }
      line = "";
    } else {
      line = line + String(c);
    }
    }
    if (line.length() > 0) {
    contents = contents + line;
    }
    target_file.close();
  } else {
    Serial.println("file open failed!");
  }
  if (path == path_root) {
    server.send(200, "text/html", contents);
  } else if (path == path_maincss || path == path_swcss) {
    server.send(200, "text/css", contents);
    //} else if (path == path_js) {
    //  server.send(200, "text/javascript", contents);
  }
}
 
bool handleFileRead(String path) {
  Serial.println("handleFileRead: " + path);
  if (path.endsWith("/")) path += "index.html";
  String contentType = getContentType(path);
  String pathWithGz = path + ".gz";
  if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) {
    if (SPIFFS.exists(pathWithGz))
    path += ".gz";
    File file = SPIFFS.open(path, "r");
    //size_t sent = server.streamFile(file, contentType);
    file.close();
    Serial.println(String("\tSent file: ") + path);
    return true;
  }
  Serial.println(String("\tFile Not Found: ") + path);
  return false;
}
 
void handleRoot() {
  Serial.println("Access");
  char message[20];
  String(server.arg(0)).toCharArray(message, 20);
  server.send(200, "text/html", (char *)buf);
}
 
void RESTART() {
  Serial.println("RESTART");
 
  Serial.println("");
  for (int i = 0; i < 5; i++) {
    digitalWrite(led_pin, HIGH);
    delay(100);
    digitalWrite(led_pin, LOW);
    delay(100);
  }
  delay(2000);
  ESP.restart();
}
 
void startServer() {
  server.on("/", handleRoot);
  server.on("/Restart", RESTART);
  server.on("/main.css", []() {
    loadFile("/main.css");
  });
  server.on("/sw_design.css", []() {
    loadFile("/sw_design.css");
  });
  server.onNotFound(handleNotFound);
 
  server.begin();
  Serial.println("HTTP server started.");
}
 
void startWebSocket() { // Start a WebSocket server
  webSocket.begin();                // start the websocket server
  webSocket.onEvent(webSocketEvent);      // if there's an incomming websocket message, go to function 'webSocketEvent'
  Serial.println("WebSocket server started.");
}
 
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { // When a WebSocket message is received
  switch (type) {
    case WStype_DISCONNECTED:
    Serial.printf("[%u] Disconnected!\n", num);
    break;
    case WStype_CONNECTED: {
      IPAddress ip = webSocket.remoteIP(num);
      Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload);
    }
    break;
    case WStype_TEXT: {
      Serial.printf("[%u] get Text: %s\n", num, payload);
      int bufsize = 2;
      char str[bufsize];
      snprintf(str, bufsize, "%s", payload);
      Serial.print("str : ");
      Serial.println(str);
 
      if (strcmp(str, "0") == 0 || strcmp(str, "1") == 0) {
        weblocknum = true;
      } else {
        weblocknum = false;
      }
    }
    break;
    case WStype_BIN:
    Serial.printf("WStype_BIN : [%u] get binary length: %u\n", num, length);
    break;
    case WStype_ERROR:
    Serial.printf("WStype_ERROR : [%u]\n", num);
    break;
    case WStype_FRAGMENT_BIN_START:
    case WStype_FRAGMENT:
    case WStype_FRAGMENT_FIN:
    case WStype_FRAGMENT_TEXT_START:
    default:
    break;
  }
}

 ドアロック・アンロックシステムの認証、施錠・解錠機構(ドア内側ロック機構)のスケッチはこんな感じ。

ESP32カメラ/Webロック側スケッチ

 ブラウザを介したロック・アンロックボタンは、鍵のライブ映像配信用WebサーバとしたESP32カメラにもたせることにしました。

 まず、ESP32カメラには、技適ありのESP32S3(freenove ESP32-S3-WROOM-1)を使用しました。

 ついては、スケッチもFreenove技適済みカメラ付きESP32-S3-WROOM-1開発ボードの通りに[FNK0085 Freenove ESP32-S3-WROOM Board]用をダウンロード、その中の[Sketch_07.2_As_VideoWebServer.ino]をベースとしました。

Sketch_07.2_As_VideoWebServer/
--Sketch_07.2_As_VideoWebServer.ino
--app_httpd.cpp
--camera_pins.h
--sd_read_write.cpp
--sd_read_write.h

 その際の注意事項もリンク先にありますが、ファイル構成は次のようになっています。

esp32_cam_with_web_lock/
--esp32_cam_with_web_lock.ino
--app_httpd.cpp
--camera_pins.h

 これをmicroSDカードは使わないものとしつつ、スケッチとディレクトリ名を任意のファイル名に改名してこのようにしました。

 この内、編集するのは、esp32_cam_with_web_lock.inoとapp_httpd.cppの2つ。

#include <WiFi.h>
#include <ESPmDNS.h>
#include "esp_camera.h"
 
// Select camera model
#define CAMERA_MODEL_ESP32S3_EYE
//#define CAMERA_MODEL_WROVER_KIT
//#define CAMERA_MODEL_AI_THINKER
//...etc.
 
#include "camera_pins.h"
 
void cameraInit(void);
void startCameraServer();
 
const char* ssid     = "SSID";
const char* password = "PASSWORD";
 
IPAddress ip(192, 168, 1, 248);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
//IPAddress primaryDNS(8, 8, 8, 8);
//IPAddress secondaryDNS(8, 8, 4, 4);
 
const char common_name[40] = "esp32_door_incam";
const char* mdnsName = common_name;
 
void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();
 
  cameraInit();
 
  startWiFi();
  startCameraServer();
  startMDNS();
}
 
void loop() {
  delay(10000);
}
 
void cameraInit(void){
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  //config.frame_size = FRAMESIZE_UXGA;
  config.frame_size = FRAMESIZE_QVGA;
  config.pixel_format = PIXFORMAT_JPEG; // for streaming
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 1;
 
  // if PSRAM IC present, init with UXGA resolution and higher JPEG quality
  // for larger pre-allocated frame buffer.
  if(psramFound()){
    config.jpeg_quality = 10;
    config.fb_count = 2;
    config.grab_mode = CAMERA_GRAB_LATEST;
  } else {
    // Limit the frame size when PSRAM is not available
    config.frame_size = FRAMESIZE_SVGA;
    config.fb_location = CAMERA_FB_IN_DRAM;
  }
 
  // camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
 
  sensor_t * s = esp_camera_sensor_get();
  //s->set_vflip(s, 1);
  s->set_vflip(s, 0);
  s->set_brightness(s, 1);
  s->set_saturation(s, 0);
  //s->set_hmirror(s, 1);
}
 
void startWiFi() {
  WiFi.disconnect();
 
//  if (!WiFi.config(ip, gateway, subnet, primaryDNS, secondaryDNS)) {
  if (!WiFi.config(ip, gateway, subnet)) {
    Serial.println("STA Failed to configure");
  }
  //  WiFi.softAP(ssid, password);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while ( WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("SSID \"");
  Serial.print(ssid);
  Serial.println("\" started\r\n");
 
  Serial.print("Connected to ");
  Serial.println(ssid);
 
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
 
  Serial.print("hostname : ");
  //  Serial.println(WiFi.hostname());
  Serial.println("");
}
 
void startMDNS() {
  MDNS.begin(mdnsName);
  Serial.print("mDNS responder started: http://");
  Serial.print(mdnsName);
  Serial.println(".local");
}

 そのesp32_cam_with_web_lock.inoのスケッチがこれ。

 技適ありのESP32S3(freenove ESP32-S3-WROOM-1)カメラではないカメラを使う場合は、camera_pins.hを参考にスケッチのdefine値を変更のこと。

...
const char index_web[]=R"rawliteral(
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Smart Lock and Streaming</title>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" />
<script>
var connection = new WebSocket("ws://"+"192.168.1.247"+":81/", ["arduino"]);
connection.onopen = function () {
  connection.send("Connect " + new Date());
};
connection.onerror = function (error) {
  console.log("WebSocket Error ", error);
};
connection.onmessage = function (e) {
  console.log("Server: ", e.data);
};
connection.onclose = function(){
  console.log("WebSocket connection closed");
};
 
function sendCtrl(num) {
  console.log("num : " + num);
  connection.send(num);
}
 
function http_req(url) {
  var req = new XMLHttpRequest();
  req.open("GET", url, true);
  req.send();
}
</script>
</head>
<body>
<h1 style="font-size:18px ;">Webスマートロック</h1>
<div>
<input type="button" id="lockingSW" value="解錠/施錠" onclick="sendCtrl('1')" style="width:200px ;height:60px ;font-size:120% ;">
</div>
<p><img id="stream" src="" width="80%" height="40%" /></p>
<iframe width=0 height=0 frameborder=0 id="myiframe" name="myiframe"></iframe>
<div><input type="button" name="home" value="メインメニュー" class="home" onClick="http_req(location.href='http://192.168.1.230')"></div>
</body>
<script>
document.addEventListener('DOMContentLoaded', function (event) {
var baseHost = document.location.origin
var streamUrl = baseHost + ':81'
const view = document.getElementById('stream')
view.src = `${streamUrl}/stream`
});
</script>
</html>)rawliteral";
...

 また、Webロック用の施錠解錠ボタン、カメラ映像、自作スマートホーム操作パネルのメインメニュー遷移ボタンが配置されたWebsocket通信等JavaScriptを含むHTMLファイルソースなどをapp_httpd.cppに定義しました。

 内、定義部分がこれ。

 htmlでサイズを%指定しただけ、スマホには良いですが、パソコンやタブレットには、ライブ映像サイズが大き過ぎる感があるのでCSSなどで調整した方が良いでしょう。

 この場合、WebSocketクライアントは、ESP32カメラ、WebSocketサーバとなるのは、キーロック機構用ESP32ボードなのでアクセスすべきIPアドレス(とポート)は、ESP32カメラではなく、キーロック機構用ESP32ボードのものとします。

 これにより、ESP32カメラのIPアドレスにアクセスした際、ロック・アンロックボタンやライブ映像が表示され、ロック・アンロックボタン押下時、設定値をWebsocketを介してロック機構用ESP32ボードに送信します。

 ちなみに当初、小洒落たCSSでタップ・クリックの度に角丸の四角形が回転しつつ、中央の「開」「閉」の文字が入れ替わり、色も変わるようにしてみたら、結構カッコよく良い感じになり、使いたかったのですが、センサー値を取得するのも面倒だし、仮にもラグがあったらしょうもないしと普通のボタンにしました。

認証パネルにおける注意事項

PN532/ELECHOUSE NFC MODULE V3 elechouse.com内PN532_ Manual_V3.pdfから引用

 今回、RFID/NFC認証に使ったのは、ELECHOUSE NFC MODULE V3(PN532互換)ボード。

 PN532/ELECHOUSE NFC MODULE V3ボードは、HSU/IIC(I2C)/SPIに対応、それぞれ同ボード上の印字に従ってディップスイッチ2つの組み合わせで設定する必要があります。

 ここでは、I2Cとしたのでスイッチ[1]を[ON]、[2]を[OFF]としました。

 尚、このスイッチ、表面にセロファンのようなものが貼られており、回路に組み込んだ方は、これを剥がして設定しましたが、再検証用にもう1つ買った方は、ふと、このまま切り替えるのが正解か?と少しシワが寄りつつも、剥がさずに切り替えました。

 ちなみにスケッチ書き換え・アップロード直後に関しては、結構な頻度でというか、ほぼほぼ起動時にNFCボードが見つからず、そうなるとソフトウェアリセット(ESP.restart())ではダメでUSBポートに挿し直すなど電源を入れ直す以外、NFCボードを見つけることができなくなることがありました。

 NFCモジュールボードが見つからない場合、停止するロジックにしてるからですが、キーパッド入力もできない為、OTAでもこうだとするとスケッチを書き換える度にUSB抜・挿なり、スイッチ付きコンセントならOFF/ONなりする必要が生じます(今のところ、ソフトウェア的な解決策はなさ気)。

 一方、運用してみるとキーパッド入力による認証、施錠・解錠はできるのに、当初できていたカード認証自体ができない状況がたまに発現、つまり、当初、機能していたPN532モジュールボードが何らかの理由で機能停止したようで、電源切・入を要する状態になることが...。

 認証パネルが外だけにWi-Fiが一時的に切断され、再接続される(その際、ELECHOUSE NFC MODULE V3がnot foundになる)ような状況があるのでしょうか?

 そこで2台あるアクセスポイントにしている無線LANルーターの内、距離にすると僅かながらも、よりドアに近く、電波障害が少なそうな方にしてみたところ、この現象はなくなったので、やはり、Wi-Fiが途切れるほどに電波が弱かったことが原因だったようです。

 尚、通電中、なんらかの状況でVCC/GNDに限らず、PN532モジュールへの配線が外れてしまうと無反応になるので再配線の上、電源断・電源再投入必須。

 本稼働させてみると不具合が出る(無線が途切れる)ことはなくなったので特に対処する必要はないと判断しましたが、必要なら遠隔操作を考慮し、認証パネル側は自作スマートプラグから給電、これと別電源のキーロック側の再起動時に認証パネル側のスマートプラグをOFF/ONすれば良さ気。

 これに関して前述のスケッチ上は、キーパッドから[#0000]を入力すると認証パネル側とロック機構側をソフトウェアリセットESP.restart()するようプログラムしたものの、現在は、ロック機構側のみ実行、認証パネル側はソフトウェアリセット部分をコメントアウトして実行しないようにしてあります。

 尤もロック機構側はそもそも不具合が起きることはなく、認証パネル側も不具合が無線が途切れることに起因し、今となっては解消されたことから、ロック機構側のソフトウェアリセットも必要ないので、ここ以外で機能として紹介もしていませんが。

 というわけでRFIDについては、基本、解錠・施錠をし始める程度(認証OKのLEDが点灯する)まで認証パネル(モジュール)とできる限り水平かつ、認識可能な距離に近づけないと機能しないことがありますが、そこさえ留意すれば正常に機能することを確認済み。

 他方、キーパッドからのパスワード認証時、何度も繰り返しやっていると反応しなくなることがあり、コアが別だからというのはありますが、一度なるとRFID認証はできるのにキーパッド側は一向に機能しなくなることがありました。

 と思ったら、これは、気づかないうちにキーパッドを押し損ねること(があって実際の入力数と認識にズレがあること)に起因する模様であることが判明。

 この場合は、前掲スケッチでは[#]で入力値をクリアできるようになっており、クリア後、適切なキーワードを入力し直すことで正常に機能することを確認済み。

 なんなら、毎回、[#]から入力を始めても良さ気。

必要なモノ

 今回、使ったものは以下の通り。

 共有できたり、内容物の一部しか使っていないものなどを除き、ざっと使った分だけでも7500円前後ですかね。

 3000〜4000円でできるかななんて安易に思っていたものの、結果、案外かかってしまいましたが...。

 言うまでもなく、単なる個人の趣味、むき出しだったり、仕上がりの雑さはさておき、単品製作でハイスペックであることを考えると市販品よりは遥かに安価なので良しと言うことで。

 それにワケあっていくつか機能を削ぎ落としつつもスマートロックにRFID認証とパスワード認証が付いていてパソコンに限らず、RFID認証だけでなく、在宅時や外出時にブラウザ操作でもスマホやタブレットが使えて鍵のライブ映像付きとなれば、尚、お得!?

 自作ライブカメラ分コストアップしてますが、それは市販品を使っても同じということで。

 ちなみにウェットシート用の開閉扉、後に気づけば、サイズはともかく、ダイソー、セリア、キャンドゥなど100均各社ともに扉のみ単品で取り扱いがあり、ダイソーとセリアに関しては選択肢が複数ありました。

 認証パネルについては、当初、流用を想定、100均やAliExpressにもあり、何れも調達してみた市販の「お風呂でスマホ操作可能なケース」だとESP32開発ボードのピンヘッダをストレートからアングル(L字)タイプのものに替えてはんだ付けし直しても尚、微妙に空間が足りず、結果、先のケースを使用するに至りました。

仕様決定までの経緯

 通信については、結構な回数、いろいろやってみた結果、ESPNowもUDPも試すまでもなく、TCP通信で実用に十分と判断。

 尚、それぞれは難なく実用的な時間で操作可能も、いざMifare/FeliCaを併用してみると共に反応が激遅(FeliCaに至っては30秒前後)になることが判明。

 タイミング的には併用直後からも通信方法に起因する可能性を排除できることを確認すべく、UDP通信で試しても同じ様子。

 よって今回は、FeliCaの使用を断念、予定通り、TCP通信を採用。

 オートロックについては、時間経過やBluetooth等いくつかのケースで実装可能も、玄関先のポストを確認とか、草花に水やりとか、隣家訪問とか、スマホやNFCカード、物理鍵を持たず、鍵をかけずにちょっと玄関を出ることもあり、キーパッド入力があっても万一停電になったら締め出される可能性を考慮し、不採用。

 サムターンの向き(開閉状態)のチェックは、物理的な鍵での開閉もあり、ESP32ボードのリセットや停電・復帰もあり得、電源復帰したなら、まさに、その時点の状態を知りたい為、DBデータ等ではなく、都度、センサーによる検出で判定。

 というわけでどこかでひっかかったり、故障したりする可能性はさておき、普段はセンサーで事足りる気がする、万一の停電時も停電前の状態を保持しておいても意味はなさ気なのでEEPROM/SPIFFS/littleFS/SDや外部DBへの保存はしない。

 臨時キーについては、考慮していないながら宅内にいればもちろん、外からでもVPN経由など遠隔からスケッチを修正する恰好でなら発行することも可能も、今まで必要だと思ったことは1度もないので今後もないものとして臨時キーの発行はしない。

 鍵の遠隔操作については、そもそもドア内やドア外での操作は時間がかかるため、基本、他の方法を使い、Webロックを使うことはなく、締め忘れなどロック状態確認以外での使用機会はまずない、同居人がいる場合などかえって操作が迷惑な可能性などもあり、一般にも不要と思われる。

 が、数日〜長期不在などを想定した場合、なんらかの防犯対策になり得る可能性をも考慮し、Web操作により、ロック機構部のライブ映像を表示させつつ、iPhone、iPadやAndroidデバイスなどスマホ・タブレットからも操作可能としておく。

 鍵の開閉(サムターンの回転)機構において当初想定し仮組みしてみたギア3枚による間接的なサムターン操作構造は強度的に無理があり、手持ちのSG90/MG90S/28BYJ-48など何れも機能させるに至らず、MG995はタイミング悪く壊れており、MG996Rを追加購入するも到着を待つまでもなく、構造上の問題と見切って見送り。

 また、手持ち材料から軸径6mmのMG996Rより5mmのステッピングモータ28BYJ-48の方が都合が良い中、作業の流れ上、他のモータや改造前には試さなかったものの、気分転換にトルクや回転数アップとなることが知られている28BYJ-48をユニポーラからバイポーラに改造、先んじてこれを試してみるとモーター軸延長上で直接サムターンを操作することはできたため、今回はこれを採用することに。

 尤もより省スペース化できることに越したことはなく、当初想定の3枚ギア構造の再検討、各種モータでの再検証の上、いけるのなら、運用後にでもギア3枚構造に差し替える予定。

 あと意外とセキュリティ的に脆弱、誤動作の可能性、オーバースペックなどの理由からノックのリズム、音声操作、また、指紋認証、静脈認証、顔認証など生体認証による解錠・施錠は除外。

 給電には、次のような理由からスイッチ式コンセント経由を採用。

 電池式なら停電時でもキーロックシステムを使えるとは言え、オートロックなし、ロックして出かける際は、万一に備え、物理鍵常備前提、通電時も電源断時も物理鍵での開閉が可能であること。

 予定されたものならまだしも、不測の停電になることは稀、それに比べると仮に数年もてば似たようなものも数ヶ月程度なら電池交換の方が頻繁な可能性が高く、電池にする場合、今回の構想だと玄関ドア内外の2つのESP32ボード共にそうする必要もあり、電池切れの心配含め、むしろ煩雑そうであること。

 停電時と(今回の構想・構造上、外だけでなく内の交換も要することになる)電池切れ時の状況は似たようなものとすれば、今のところ、電池にするメリットを思いつかないこと。

 というか、過去検証の為だけにESP32開発ボードにおける電池からの給電をテスト含め、考えてみたことがあったものの、一定時間間隔によるセンサー値のロガー(ログ取り)や植物への水やり、(万一にも電池切れして食いっぱぐれるとかわいそうなので実際コンセント給電にしたくなるだろう)給餌器など時間のほとんどがディープスリープ状態で、かつ、GPIO入力で復帰する方法がない限り、年単位の交換頻度は無理があると思えたのでハナから除外。

仕様変更

FeliCa認証見送り
 Mifareには、Type A(NFC-A)/Type B(NFC-B)があり、今回は、その内、Type-Aのタグ・カード、併せてFeliCa(NFC-F)の認証...を可能としたいところでしたが...
 それぞれは快適も、併用するとMIFAREで10秒程度、FeliCaに至っては30秒程度と反応が遅すぎるという謎現象があり、FeliCaについては今回見送り。
 というか、FeliCaも単独なら快適に機能するのでMIFAREを見送っても良かったのですが、なんとなく。
 ただし、後日、再検証予定、スケッチは対応済みでFeliCa対応の交通系カードもあるので解決するようならFeliCaも運用に加える予定。
ESP32ボードリセットは不要と判断
 製作・検証中、起動、接続、稼働が不安定に思えたことから、パスワード認証において特定のキーでドア内外の各ESP32ボードをソフトウェアリセットするようにしようかと思っていたのですが、完成後は安定し、不安定な状況が再現することがなかったので必要ないと判断し、今回は除外。
 尚、認証パネル側のリセットについては、使用したRFIDリーダー/ライターモジュールPN532 ELECHOUSE製NFC MODULE V3のライブラリにおいて(ボード上のボタン押下やスケッチ内でESP.restart()による)ソフトウェアリセットやスケッチアップロード後のリセットなどでは、ほぼ100%モジュールを見失う性質(バグ?)があって断念。
 ESPHomeでは、これを解決する方法があった模様ですが、本家ライブラリでは同様の方法は使えそうもないなと。
 USB抜・挿とかスイッチ付きコンセントなどのOFF/ONとか電源断からの通電だと、まずもって見失うことはないんですけどね。
 尤もRFIDリーダーを見失っても停止させず、キーパッドか物理鍵で開閉という手もあり、コンセント給電なので入室後、電源断・再通電という手もあるにはあるのですが。
 これを解消すべく、認証パネル側の給電に自作スマートプラグを使用することも検討したものの、前述のように不具合が再現しないため、見送り。
サムターンの向きを検出するセンサーの変更
 当初、昔買った37センサーキットに複数あったチルトスイッチか、リミットスイッチを2つ使うことを想定した中、チルトスイッチモジュールについては、接点側は良きに計らうにしても接点断の際は断したことしかわからない、物理的に2箇所リミットスイッチを付けるスマートな方法を思いつかないなど他のセンサーの使用を模索。
 結果、同じく昔、興味本位で買って使いみちにあぐねていた6軸モーションセンサー(3軸加速度・3軸ジャイロセンサー)を採用することに。
複数枚ギア構造からモータ直でサムターンを回す構造に変更
 構造変更の理由は、当初、想定した3枚ギアの間接的な鍵の開閉機構の強度が足りなかったり、ギアの軸固定の不備などで駆動時、水平を保つことができなかったり、トルク伝達に失敗した為。
 そんな不備のある構造で回せるはずもなく、手持ちのサーボSG90、MG90S、28BYJ-48-5V(ユニポーラ)何れもうまく動かずじまい。
 当初想定のギア構造で再製作、差し替える場合も後述のバイポーラ版28BYJ-48を採用する予定も、余力があれば、改造前のユニポーラ28BYJ-48や各種サーボでも、更に気力があれば、現行構造での動作確認もしてみるかも。
サーボからステッピングモータに変更
 構造変更に伴う動作検証時、バイポーラに改造した28BYJ-48が、他を試すまでもなく機能したため、これに決定。
 ステッピングモータを選択するに至ったのは、検証前に不慮の故障を遂げたMG995の代替としてMG996Rを追加購入も、ポチった直後、届く前に28BYJ-48のバイポーラへの改造でトルクを稼ぐことができることを思い出し、試すに至り、モータードライバにバイポーラ対応のA4988/DRV8825を使い、サムターンを回すことに成功したから。
 また、当初こそサーボを想定していたものの、ギアを噛ます構造検討時にギア構成によって最大回転角が180度では微妙なケースもある、240度だか270度、360度などこれ以上回るサーボは高価になりがちな点もあり、28BYJ-48でいけるに越したことはないと考えていたので好都合でもありました。
サムターン代替つまみ増設の見送り
 構造変更に伴い、構造・スペース的に設置場所がなくなってしまったため。
 増設つまみについては、ロック機構を当初思い描いた構造とすべく再製作予定につき、差し替える場合は、ボタンも残しつつ、つまみも増設予定。

RFID/NFC規格概要

 RFID/Radio Frequency IDentificationとは、タグとリーダー間の近距離無線通信であり、タグ自体は電源をもたず、リーダーからの電波で起動・データ送信する技術やその概念。

 タグには、カード型や各種形状のシールなど様々なタイプがあり。

 ソニー発のFeliCaやオランダPhilips社開発のMifare Classic/Mifare Type-AなどMifareは、非接触型の内、近接型ICカードやICタグ、ICシールなどで使われている通信方式。

 FeliCa(ISO/IEC 18092|NFCIP-1 => ISO/IEC 21481|NFCIP-2(NFCIP-1+ISO/IEC 14443+ISO/IEC 15693、NFCフォーラム仕様:NFC/Near Field Communication:NFC-F)は、プリペイド式の電子マネーや交通系カードなどで採用されている日本で主流の通信規格。

 Mifareは、日本でも古くはテレホンカードなどで使用されたこともある欧州で主流の通信規格。

参照先

 RFID/キーパッド実装については、合わせて指紋認証までやっている次のURLを参考にさせて頂きました(なぜかアクセスできないこともあり)。

 TCP通信実装については下記を参照させて頂きました。

 感謝。

備考

 作ってからカード認証についての注意喚起の記事を見つけてしまいましたが、とりあえず、カードには、スキミング防止カードやケースを使用、セキュリティ・防犯・安全対策するものとします。

2024/05/14

 FFC/Flexible Flat Cableをドアの隙間に配線していましたが、経路が過酷だったらしく、認証に影響が出て、もう1本買ってあった方と交換、ほぼ開閉しないサッシ経由で配線、快調。

ホーム前へ次へ