Wokwi ESP32 + Node-Red Dashboard UI Template + AngularJS + RFID (作業5)
WOKWI程式
#include <WiFi.h>
這段程式碼實現了一個完整的 ESP32 IoT 應用,結合了 RFID 讀取、MQTT 遠端控制、I2C LCD 顯示以及非阻塞式(Non-blocking)的 LED 特效。
以下是逐行的詳細說明:
1. 引用函式庫與定義
第 1-5 行:引入必要的函式庫。
WiFi處理網路,PubSubClient處理 MQTT 通訊,SPI與MFRC522用於 RFID 模組,LiquidCrystal_I2C用於螢幕顯示。第 8-15 行:設定 WiFi 資訊、MQTT Broker 位址以及三個通訊主題(UID、控制指令、狀態回報)。
第 17-19 行:定義硬體接腳。RST 為 4,SS (SDA) 為 5,LED 接在 GPIO 2。
2. 物件初始化與變數
第 21-24 行:建立 RFID、WiFi、MQTT 以及 LCD 的物件實例。
第 27-30 行:全域變數。
ledTimerEnd儲存定時結束的時間點;timerActive與flashActive是標記目前是否處於定時或閃爍模式的「旗標」;lastFlash用於計算閃爍間隔。
3. 功能函式 (Utilities)
updateDisplay (第 33-43 行):自定義函式。
將訊息同步印到 Serial Monitor(序列埠監控視窗)。
清空 LCD 第二行後印出新訊息,方便使用者從硬體直接看到狀態。
publishStatus (第 45-47 行):簡化程式,將 LED 的當前狀態(如 ON/OFF)發送到 MQTT
ledst主題。setup_wifi (第 49-53 行):啟動 WiFi 連線,並在連線成功前不斷印出
.顯示進度。
4. MQTT 指令回呼 (Callback)
callback (第 55-89 行):這是最重要的部分。當 Node-RED 發送訊息時,此函式會被觸發:
ON/OFF:直接切換 LED 電位,並將兩個模式旗標設為
false(停止閃爍或定時)。Flash:將
flashActive設為true,並重置lastFlash開始計時。Timer:將 LED 點亮,並設定
ledTimerEnd為「目前時間 + 5000 毫秒」,開啟timerActive。
5. 連線與初始化
reconnect (第 91-100 行):MQTT 斷線重連機制。連線成功後務必執行
client.subscribe重新訂閱指令主題。setup (第 102-124 行):硬體初始化。
啟動序列埠。
初始化 LCD 並開啟背光。
初始化 SPI 與 RFID 晶片。
連線 WiFi 並設定 MQTT 伺服器與回呼函式。
6. 主迴圈 (Loop)
第 127-128 行:檢查 MQTT 連線,並保持背景通訊(
client.loop())。RFID 偵測 (第 131-146 行):
檢查是否有卡片靠近。
將 UID(卡片 ID)轉換成十六進位字串。
透過 MQTT 發布 UID,並顯示在 LCD。
執行
PICC_HaltA()停止重複讀取同一張卡。
Timer 處理 (第 149-156 行):
若定時模式開啟,檢查
millis()是否超過了預設的結束時間。時間一到,自動熄滅 LED,發布 OFF 狀態。
Flash 處理 (第 159-165 行):
若閃爍模式開啟,利用
millis()檢查是否過了 250 毫秒。若是,則反轉(Invert)LED 的目前狀態,達到閃爍效果且不會卡住程式。
邏輯優點總結
這段程式碼最大的優點是使用了 millis() 而非 delay()。這意味著當 LED 在「閃爍」或是「5秒定時」的過程中,ESP32 仍然可以同時偵測 RFID 卡片以及接收新的 MQTT 指令,不會產生遲鈍感。
Node-Red程式
[{"id":"35c33e4f640a7d69","type":"ui_template","z":"25666aa01ece9150","group":"0317d60d4d4e23f4","name":"","order":0,"width":0,"height":0,"format":"<div ng-init=\"lastUID='等待感應...'\">\n <div\n style=\"background: #1e1e1e; padding: 30px; border-radius: 15px; text-align: center; color: white; box-shadow: 0 8px 16px rgba(0,0,0,0.4); border: 2px solid #3498db;\">\n\n <div\n style=\"font-size: 0.9em; color: #3498db; margin-bottom: 10px; font-weight: bold; text-transform: uppercase; letter-spacing: 2px;\">\n <i class=\"fa fa-id-card\"></i> RFID Reader Status\n </div>\n\n <div style=\"background: rgba(255,255,255,0.05); padding: 20px; border-radius: 10px; border: 1px solid #444;\">\n <div style=\"color: #aaa; font-size: 0.8em; margin-bottom: 5px;\">CURRENT SCANNED UID:</div>\n <div\n style=\"font-size: 2em; font-family: 'Courier New', Courier, monospace; font-weight: bold; color: #ffffff; text-shadow: 0 0 10px #3498db;\">\n {{lastUID}}\n </div>\n </div>\n\n <div style=\"margin-top: 15px; font-size: 0.75em; color: #666;\">\n 系統時間: {{lastUpdateTime || '---'}}\n </div>\n </div>\n</div>\n\n<script>\n (function(scope) {\n scope.$watch('msg', function(msg) {\n if (!msg) return;\n\n // 監聽來自 MQTT 的新 UID 掃描 (alex9ufo/ex7/UID)\n if (msg.topic === \"alex9ufo/ex7/UID\") {\n scope.lastUID = msg.payload;\n scope.lastUpdateTime = new Date().toLocaleTimeString();\n }\n });\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":380,"y":200,"wires":[[]]},{"id":"0d74265fb42ae932","type":"function","z":"25666aa01ece9150","name":"function ","func":"let now = new Date();\nlet d = now.toLocaleDateString('zh-TW');\nlet t = now.toLocaleTimeString('zh-TW', { hour12: false });\nvar st= msg.payload;\n\nmsg.topic = \"INSERT INTO data_logs (status, date, time) VALUES ($st, $d , $t)\";\nmsg.payload = [st, d, t];\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":60,"wires":[["284753733ebd1a98","3f81d528c3855f7d"]]},{"id":"68afea41a89aede0","type":"mqtt in","z":"25666aa01ece9150","name":"","topic":"alex9ufo/ex7/UID","qos":"1","datatype":"auto-detect","broker":"b283845a8722b420","nl":false,"rap":false,"rh":0,"inputs":0,"x":140,"y":200,"wires":[["35c33e4f640a7d69"]]},{"id":"284753733ebd1a98","type":"sqlite","z":"25666aa01ece9150","mydb":"0c6e9b903744a9ef","sqlquery":"msg.topic","sql":"","name":"2026-ex7","x":560,"y":80,"wires":[["3e4d572583ed3831"]]},{"id":"3e4d572583ed3831","type":"debug","z":"25666aa01ece9150","name":"debug 378","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":730,"y":80,"wires":[]},{"id":"3f81d528c3855f7d","type":"debug","z":"25666aa01ece9150","name":"debug 379","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"topic","targetType":"msg","statusVal":"","statusType":"auto","x":590,"y":40,"wires":[]},{"id":"0a5d2681d434f50b","type":"mqtt out","z":"25666aa01ece9150","name":"","topic":"alex9ufo/ex7/ledcon","qos":"1","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"b283845a8722b420","x":640,"y":160,"wires":[]},{"id":"90f1d94821a45e30","type":"ui_template","z":"25666aa01ece9150","group":"93ff3ebbc526669a","name":"","order":0,"width":0,"height":0,"format":"<div style=\"padding: 10px; background: #f4f4f4; border-radius: 10px; border: 1px solid #ccc;\">\n <h3 style=\"margin-top: 0; color: #333; border-bottom: 2px solid #3498db; padding-bottom: 5px;\">\n <i class=\"fa fa-database\"></i> 2026-ex7.db 系統維護\n </h3>\n\n <div style=\"display: flex; flex-direction: column; gap: 15px; margin-top: 15px;\">\n\n <div style=\"background: white; padding: 10px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);\">\n <p style=\"margin: 0 0 10px 0; font-size: 0.9em; color: #666;\">若資料庫尚未初始化或換新檔,請執行此項:</p>\n <button ng-click=\"dbAction('CREATE')\"\n style=\"width: 100%; background: #34495e; color: white; border: none; padding: 12px; border-radius: 4px; font-weight: bold; cursor: pointer;\">\n 初始化 / 建立資料表\n </button>\n </div>\n\n <div\n style=\"background: #fff5f5; padding: 10px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border: 1px solid #ffcccc;\">\n <p style=\"margin: 0 0 10px 0; font-size: 0.9em; color: #c0392b; font-weight: bold;\">危險區域:</p>\n <button ng-click=\"dbAction('DELETE')\"\n style=\"width: 100%; background: #e74c3c; color: white; border: none; padding: 12px; border-radius: 4px; font-weight: bold; cursor: pointer;\">\n 清空所有歷史紀錄\n </button>\n </div>\n\n </div>\n\n <div ng-if=\"statusMsg\"\n style=\"margin-top: 15px; padding: 10px; background: #d4edda; color: #155724; border-radius: 4px; font-size: 0.9em; text-align: center;\">\n {{statusMsg}}\n </div>\n</div>\n\n<script>\n (function(scope) {\n scope.statusMsg = \"\";\n\n scope.dbAction = function(type) {\n if (type === 'CREATE') {\n const sql = \"CREATE TABLE IF NOT EXISTS data_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, status TEXT, date TEXT, time TEXT)\";\n scope.send({topic: \"DB_ADMIN\", payload: sql});\n scope.statusMsg = \"已發送指令:建立資料表\";\n } \n else if (type === 'DELETE') {\n if (confirm(\"警告!這將永久刪除所有歷史紀錄,確定要執行嗎?\")) {\n // 發送刪除指令與壓縮資料庫指令\n scope.send({topic: \"DB_ADMIN\", payload: \"DELETE FROM data_logs\"});\n // 這裡延遲發送 VACUUM 以重整空間\n setTimeout(function() {\n scope.send({topic: \"DB_ADMIN\", payload: \"VACUUM\"});\n }, 500);\n scope.statusMsg = \"已發送指令:清空資料庫\";\n }\n }\n \n // 3秒後自動消失狀態文字\n setTimeout(function() {\n scope.statusMsg = \"\";\n scope.$apply();\n }, 3000);\n };\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":120,"y":280,"wires":[["126e9374abfdfcf8"]]},{"id":"847c85ee2909ffce","type":"sqlite","z":"25666aa01ece9150","mydb":"0c6e9b903744a9ef","sqlquery":"msg.topic","sql":"","name":"2026-ex7","x":540,"y":280,"wires":[["e3e5880d94dc1292"]]},{"id":"7d0e0155adb1a9e3","type":"sqlite","z":"25666aa01ece9150","mydb":"0c6e9b903744a9ef","sqlquery":"msg.topic","sql":"","name":"2026-ex7","x":340,"y":380,"wires":[["6d8bf3cf2217dddf"]]},{"id":"6d8bf3cf2217dddf","type":"ui_template","z":"25666aa01ece9150","group":"b8bea6bc6ecd9b16","name":"","order":0,"width":0,"height":0,"format":"<div ng-init=\"history=[]\">\n <div\n style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-left: 5px solid #3498db; padding-left: 10px;\">\n <h3 style=\"margin: 0; font-size: 1.1em; color: #333;\">2026ex7.db 歷史紀錄 (最新 25 筆)</h3>\n <button ng-click=\"send({topic: 'REFRESH_DB', payload: 'SELECT'})\"\n style=\"background: #3498db; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; font-size: 0.8em;\">\n <i class=\"fa fa-refresh\"></i> 手動刷新\n </button>\n </div>\n\n <div\n style=\"max-height: 500px; overflow-y: auto; border: 1px solid #ddd; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);\">\n <table\n style=\"width: 100%; border-collapse: collapse; background: white; font-size: 0.9em; font-family: sans-serif;\">\n <thead style=\"position: sticky; top: 0; background: #34495e; color: white; z-index: 1;\">\n <tr>\n <th style=\"padding: 10px; text-align: center; border-right: 1px solid #455a64;\">ID</th>\n <th style=\"padding: 10px; text-align: left; border-right: 1px solid #455a64;\">事件 / UID</th>\n <th style=\"padding: 10px; text-align: center; border-right: 1px solid #455a64;\">日期</th>\n <th style=\"padding: 10px; text-align: center;\">時間</th>\n </tr>\n </thead>\n <tbody>\n <tr ng-repeat=\"row in history\"\n ng-style=\"{'background-color': ($index % 2 === 0 ? '#ffffff' : '#f9f9f9')}\"\n style=\"border-bottom: 1px solid #eee;\">\n <td style=\"padding: 10px; text-align: center; color: #666;\">{{row.id}}</td>\n <td style=\"padding: 10px;\">\n <span ng-if=\"row.status.indexOf('UID:') !== -1\" style=\"color: #2980b9; font-weight: bold;\">\n <i class=\"fa fa-id-card\"></i> {{row.status}}\n </span>\n <span ng-if=\"row.status.indexOf('UID:') === -1\"\n ng-style=\"{'color': (row.status === 'OFF' ? '#e74c3c' : '#27ae60')}\"\n style=\"font-weight: bold;\">\n <i class=\"fa fa-cog\"></i> {{row.status}}\n </span>\n </td>\n <td style=\"padding: 10px; text-align: center; color: #444;\">{{row.date}}</td>\n <td style=\"padding: 10px; text-align: center; color: #444;\">{{row.time}}</td>\n </tr>\n <tr ng-if=\"!history || history.length === 0\">\n <td colspan=\"4\" style=\"padding: 20px; text-align: center; color: #999;\">暫無紀錄 (或正在載入資料...)</td>\n </tr>\n </tbody>\n </table>\n </div>\n <div style=\"margin-top: 5px; font-size: 0.75em; color: #999; text-align: right;\">\n 最後刷新: {{lastUpdate}}\n </div>\n</div>\n\n<script>\n (function(scope) {\n scope.$watch('msg', function(msg) {\n if (!msg) return;\n\n // 重要:SQLite 節點查詢後的結果通常直接放在 msg.payload 中\n if (Array.isArray(msg.payload)) {\n scope.history = msg.payload;\n scope.lastUpdate = new Date().toLocaleTimeString();\n \n // 強制 AngularJS 檢查變數變更,確保表格刷新\n // scope.$apply(); // 在某些 Node-RED 版本中需要,若報錯可註解掉\n }\n });\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":500,"y":380,"wires":[["d4ec91cbab80f917"]]},{"id":"d4ec91cbab80f917","type":"debug","z":"25666aa01ece9150","name":"debug 380","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":710,"y":380,"wires":[]},{"id":"e3e5880d94dc1292","type":"debug","z":"25666aa01ece9150","name":"debug 381","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":730,"y":280,"wires":[]},{"id":"2ac0e4980e3f4525","type":"debug","z":"25666aa01ece9150","name":"debug 382","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":550,"y":320,"wires":[]},{"id":"126e9374abfdfcf8","type":"function","z":"25666aa01ece9150","name":"function ","func":"// 僅處理來自 UI 維護按鈕的 DB_ADMIN 指令\nif (msg.topic === \"DB_ADMIN\") {\n msg.topic = msg.payload; \n msg.payload = []; \n return msg;\n}\nreturn null; // 攔截其他所有訊息,不讓它們從這裡進入 DB","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":280,"wires":[["2ac0e4980e3f4525","847c85ee2909ffce"]]},{"id":"9ac925e7202aca49","type":"mqtt in","z":"25666aa01ece9150","name":"","topic":"alex9ufo/ex7/ledst","qos":"1","datatype":"auto-detect","broker":"b283845a8722b420","nl":false,"rap":false,"rh":0,"inputs":0,"x":150,"y":60,"wires":[["0d74265fb42ae932"]]},{"id":"852c85434821d2b0","type":"comment","z":"25666aa01ece9150","name":"新增 recoder --2026-ex7.db","info":"CREATE TABLE IF NOT EXISTS data_logs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n status TEXT,\n date TEXT,\n time TEXT\n);","x":170,"y":20,"wires":[]},{"id":"20fb8eea5d166935","type":"mqtt in","z":"25666aa01ece9150","name":"","topic":"alex9ufo/ex7/UID","qos":"1","datatype":"auto-detect","broker":"b283845a8722b420","nl":false,"rap":false,"rh":0,"inputs":0,"x":140,"y":100,"wires":[["0d74265fb42ae932"]]},{"id":"72a50948abab5c44","type":"ui_template","z":"25666aa01ece9150","group":"9cc17ea5438167fa","name":"","order":0,"width":0,"height":0,"format":"<div ng-init=\"ledStatus='OFF'\">\n <div\n style=\"background: #222; padding: 25px; border-radius: 15px; text-align: center; color: white; margin-bottom: 20px; box-shadow: 0 4px 10px rgba(0,0,0,0.3);\">\n <div style=\"font-size: 1.3em; margin-bottom: 15px; letter-spacing: 1px;\">\n LED 模式: <b ng-style=\"{color: (ledStatus === 'OFF' ? '#ff4d4d' : '#00ff00')}\">{{ledStatus}}</b>\n </div>\n\n <div ng-style=\"{'background-color': (ledStatus === 'OFF' ? '#ff4d4d' : '#00ff00'), 'box-shadow': '0 0 20px ' + (ledStatus === 'OFF' ? '#ff4d4d' : '#00ff00')}\"\n style=\"width: 60px; height: 60px; border-radius: 50%; margin: 0 auto; transition: 0.4s ease-in-out; border: 3px solid #444;\">\n </div>\n </div>\n\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 12px;\">\n <button ng-click=\"send({topic: 'alex9ufo/ex7/ledcon', payload: 'on'})\"\n style=\"background: #2ecc71; border: none; color: white; padding: 20px; border-radius: 8px; cursor: pointer; font-weight: bold; font-size: 1.1em;\">\n ON\n </button>\n\n <button ng-click=\"send({topic: 'alex9ufo/ex7/ledcon', payload: 'off'})\"\n style=\"background: #e74c3c; border: none; color: white; padding: 20px; border-radius: 8px; cursor: pointer; font-weight: bold; font-size: 1.1em;\">\n OFF\n </button>\n\n <button ng-click=\"send({topic: 'alex9ufo/ex7/ledcon', payload: 'flash'})\"\n style=\"background: #f1c40f; border: none; color: black; padding: 20px; border-radius: 8px; cursor: pointer; font-weight: bold; font-size: 1.1em;\">\n FLASH\n </button>\n\n <button ng-click=\"send({topic: 'alex9ufo/ex7/ledcon', payload: 'timer'})\"\n style=\"background: #3498db; border: none; color: white; padding: 20px; border-radius: 8px; cursor: pointer; font-weight: bold; font-size: 1.1em;\">\n TIMER (5s)\n </button>\n </div>\n</div>\n\n<script>\n (function(scope) {\n scope.$watch('msg', function(msg) {\n if (!msg) return;\n\n // 監聽來自 MQTT 的 LED 狀態更新 (例如: ON, OFF, Flash, Timer)\n // 來源主題需為: alex9ufo/ex7/ledst\n if (msg.topic === \"alex9ufo/ex7/ledst\") {\n scope.ledStatus = msg.payload;\n }\n });\n})(scope);\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":380,"y":160,"wires":[["0a5d2681d434f50b"]]},{"id":"4c0408017879ba16","type":"mqtt in","z":"25666aa01ece9150","name":"","topic":"alex9ufo/ex7/ledst","qos":"1","datatype":"auto-detect","broker":"b283845a8722b420","nl":false,"rap":false,"rh":0,"inputs":0,"x":150,"y":160,"wires":[["72a50948abab5c44"]]},{"id":"48edbb37c788666f","type":"inject","z":"25666aa01ece9150","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"2","crontab":"","once":true,"onceDelay":0.1,"topic":"SELECT * FROM data_logs ORDER BY id DESC LIMIT 25","payload":"","payloadType":"date","x":140,"y":380,"wires":[["7d0e0155adb1a9e3"]]},{"id":"0317d60d4d4e23f4","type":"ui_group","name":"UID","tab":"0b9280dac9c490f1","order":4,"disp":true,"width":"6","collapse":false,"className":""},{"id":"b283845a8722b420","type":"mqtt-broker","name":"","broker":"broker.emqx.io","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"0c6e9b903744a9ef","type":"sqlitedb","db":"2026ex7.db","mode":"RWC"},{"id":"93ff3ebbc526669a","type":"ui_group","name":"database command","tab":"0b9280dac9c490f1","order":2,"disp":true,"width":"6","collapse":false,"className":""},{"id":"b8bea6bc6ecd9b16","type":"ui_group","name":"Show Data","tab":"0b9280dac9c490f1","order":3,"disp":true,"width":"6","collapse":false,"className":""},{"id":"9cc17ea5438167fa","type":"ui_group","name":"control","tab":"0b9280dac9c490f1","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"0b9280dac9c490f1","type":"ui_tab","name":"2026-ex7","icon":"dashboard","disabled":false,"hidden":false}]
這份 Node-RED 程式碼構建了一個完整的物聯網(IoT)儀表板,專門用於監控 RFID 掃描、控制 ESP32 的 LED 模式,並將所有事件記錄到 SQLite 資料庫 中。
以下是程式碼各個區塊的詳細說明:
1. 訊息接收與資料記錄 (上層流程)
這部分負責處理從 ESP32 傳回的資訊(UID 與 LED 狀態)。
MQTT In (
ledst&UID): 分別訂閱兩個主題。當 ESP32 掃描到卡片或 LED 狀態改變時,訊息會進入此處。Function (
0d74265fb42ae932):負責將目前的日期(Date)與時間(Time)封裝進訊息。
產生 SQL 語法:
INSERT INTO data_logs ...。
SQLite (
2026-ex7): 將訊息存入名為2026ex7.db的資料庫檔案中。Debug (
378,379): 在 Node-RED 側邊欄顯示訊息,幫助你確認資料是否正確存入。
2. LED 即時監控與控制 (中層流程)
這部分是與使用者互動的人機介面(Dashboard)。
MQTT In (
ledst): 接收 ESP32 目前的燈號狀態(ON, OFF, Flash...)。UI Template (control):
顯示區域:上方有一個圓形指示燈,透過顏色變化同步顯示 ESP32 的 LED 狀態。
控制按鈕:提供四個按鈕(ON, OFF, FLASH, TIMER),點擊後會透過 MQTT 發送指令給 ESP32。
MQTT Out (
ledcon): 將按鈕指令傳送回 ESP32。UI Template (UID): 專屬的顯示視窗,用藍色大字體顯示最後一次掃描到的 RFID UID 號碼。
3. 資料庫維護管理 (下層流程)
這部分用於系統管理與資料清理。
UI Template (database command): 提供兩個維護按鈕。
初始化 / 建立資料表:若
data_logs表格不存在則建立它。清空紀錄:刪除資料表內所有資料(帶有彈出視窗確認)。
Function (
126e9374abfdfcf8): 作為管理過濾器,確保只有點擊「維護按鈕」產生的 SQL 指令會進入資料庫執行,防止資料寫入錯誤。
4. 歷史紀錄顯示 (查詢流程)
這部分負責在儀表板上顯示過去的事件紀錄。
Inject (每 2 秒): 定時器,每隔 2 秒自動觸發一次資料庫查詢,確保表格資料是新的。
SQLite (
2026-ex7): 執行SELECT * FROM data_logs ORDER BY id DESC LIMIT 25,從資料庫撈出最後 25 筆資料。UI Template (Show Data):
將撈出的資料陣列(Array)渲染成 HTML 表格。
內建邏輯:自動判斷訊息內容,如果是
UID:則顯示藍色卡片圖標,如果是ON/OFF則顯示設定圖標。
整體架構總結
這是一個 「閉環控制 + 數據存儲」 的架構:
控制環:網頁按鈕 -> MQTT -> ESP32 動作 -> 狀態回傳 MQTT -> 網頁顯示燈號。
記錄環:ESP32 數據 -> MQTT -> Node-RED 格式化 -> SQLite 存檔。
顯示環:Inject 定時觸發 -> SQLite 查詢 -> UI 表格更新。
特別建議: 你的 Inject 節點目前設定為每 2 秒刷新一次,這在模擬器中可能會造成資料庫稍微忙碌,如果資料量大,可以考慮將頻率降為 5 秒或改為「寫入後觸發」。

沒有留言:
張貼留言