#include <MFRC522.h>
#include <SPI.h>
#include <PubSubClient.h>
#include <WiFi.h>
#include <freertos/task.h> // For FreeRTOS tasks
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2); // I2C address 0x27, 16 column and 2 rows
// MFRC522 PINs (Although simulated, kept to conform to actual hardware configuration)
#define SS_PIN 5
#define RST_PIN 27
// Onboard LED GPIO pin (usually GPIO 2)
#define LED_PIN 2
MFRC522 mfrc522(SS_PIN, RST_PIN);
// WiFi credentials
const char* ssid = "Wokwi-GUEST"; // Replace with your Wi-Fi name (e.g., Wokwi's "Wokwi-GUEST")
const char* password = ""; // Replace with your Wi-Fi password (e.g., Wokwi's empty string "")
// MQTT Broker details
const char* mqtt_server = "broker.mqttgo.io";
const int mqtt_port = 1883;
const char* mqtt_topic = "alex9ufo/rfidUID";
const char* mqtt_client_id = "ESP32_RFID_DualCore_LED_Simulator"; // Updated Client ID
WiFiClient espClient;
PubSubClient client(espClient);
// Queue for passing UID between cores
QueueHandle_t uidQueue;
// --- Core 0 (MQTT and WiFi Core) ---
void mqttTask(void *pvParameters) {
// Serial.begin() only needs to be initialized once on one core, usually in setup() or a main task
// SPI.begin() and mfrc522.PCD_Init() are here because they are related to the main core
Serial.begin(115200); // Ensure Serial is available in this task
SPI.begin();
mfrc522.PCD_Init(); // Initialize MFRC522 (kept even for simulation)
pinMode(LED_PIN, OUTPUT); // Set LED pin as output
Serial.println("Initializing WiFi on Core 0...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.print(".");
}
Serial.println("\nWiFi connected on Core 0");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
client.setServer(mqtt_server, mqtt_port);
Serial.println("MQTT client initialized on Core 0.");
String receivedUid;
while (true) {
if (!client.connected()) {
reconnectMQTT();
}
client.loop(); // Maintain MQTT connection
// Receive UID from Queue
// Use portMAX_DELAY to make the task wait indefinitely until data is available
if (xQueueReceive(uidQueue, &receivedUid, portMAX_DELAY) == pdPASS) {
Serial.print("Received UID from Core 1: ");
Serial.println(receivedUid);
if (client.publish(mqtt_topic, receivedUid.c_str())) {
Serial.println("UID published successfully to MQTT. Turning LED ON.");
digitalWrite(LED_PIN, HIGH); // Turn LED ON on successful publish
vTaskDelay(500 / portTICK_PERIOD_MS); // Stay on for 500 milliseconds
digitalWrite(LED_PIN, LOW); // Turn LED OFF
Serial.println("LED OFF.");
lcd.clear(); // clear display
lcd.setCursor(5, 0); // move cursor to (0, 0)
lcd.print("RFID IOT"); // print message at (0, 0)
} else {
Serial.println("Failed to publish UID to MQTT.");
// Option to blink LED here to indicate failure
}
}
// Short delay to allow other tasks to run, even if no data is received
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void reconnectMQTT() {
while (!client.connected()) {
Serial.println("Attempting MQTT connection on Core 0...");
// Attempt to connect with a Client ID
if (client.connect(mqtt_client_id)) {
Serial.println("MQTT connected on Core 0.");
} else {
Serial.print("MQTT connect failed on Core 0, rc=");
Serial.print(client.state());
Serial.println(" trying again in 5 seconds.");
delay(5000); // Use delay() because this is a blocking operation for MQTT connection
}
}
}
// --- Core 1 (RFID Simulation Core) ---
void rfidSimTask(void *pvParameters) {
Serial.println("Press ENTER in the serial monitor to simulate RFID scan on Core 1.");
while (true) {
if (Serial.available()) {
char c = Serial.read(); // Read the character
if (c == '\n' || c == '\r') { // Detect ENTER key press (both LF and CR)
// Consume any remaining newline/carriage return characters
while(Serial.available() && (Serial.peek() == '\n' || Serial.peek() == '\r')) {
Serial.read();
}
String uid = generateSpecificRangeUID(); // Call the new UID generation function
Serial.print("Simulating RFID scan on Core 1. Generated UID: ");
Serial.println(uid);
lcd.setCursor(5, 1); // move cursor to (2, 1)
lcd.print(uid); // print message at (2, 1)
// Send UID to Queue
// portMAX_DELAY means it will wait indefinitely if the queue is full until successfully sent
if (xQueueSend(uidQueue, &uid, portMAX_DELAY) != pdPASS) {
Serial.println("Failed to send UID to queue from Core 1.");
}
}
}
vTaskDelay(100 / portTICK_PERIOD_MS); // Avoid busy-waiting
}
}
// --- NEW UID GENERATION FUNCTION ---
String generateSpecificRangeUID() {
// Define the start and end values as unsigned long
unsigned long start_val = 0x8EED7100;
unsigned long end_val = 0x8EED71FF;
// Generate a random unsigned long within the specified range
unsigned long random_val = random(start_val, end_val + 1); // +1 because random(min, max) excludes max
// Convert the unsigned long to an 8-character hexadecimal string
char hex_uid[9]; // 8 characters + null terminator
sprintf(hex_uid, "%08lX", random_val); // %08lX for 8 uppercase hex digits of a long
return String(hex_uid);
}
// --- Main Program Setup ---
void setup() {
lcd.init(); // initialize the lcd
lcd.backlight();
Serial.begin(115200);
// Create Queue for String type, depth of 5
// Note: Copying Strings has overhead, but is acceptable for small amounts of data
uidQueue = xQueueCreate(5, sizeof(String));
if (uidQueue == NULL) {
Serial.println("Failed to create UID queue. System Halted!");
while(true); // If queue creation fails, halt
}
Serial.println("Enter any character (then press ENTER) in the serial monitor to simulate an RFID scan.");
Serial.println("This will generate a UID within the range 8EED7100 to 8EED71FF.");
Serial.println("The generated UID will be published to broker.mqttgo.io topic alex9ufo/rfidUID.");
Serial.println("The onboard LED (GPIO 2) will briefly light up upon successful MQTT publication.");
// Create MQTT task, run on Core 0
// Increase stack size as it handles WiFi, MQTT, and Serial output
xTaskCreatePinnedToCore(
mqttTask, // Task function
"MQTT_Task", // Task name
10000, // Stack size (bytes)
NULL, // Task parameters
1, // Task priority (0 lowest, ESP32 core tasks usually higher)
NULL, // Task handle
0 // Run on Core 0
);
// Create RFID simulation task, run on Core 1
xTaskCreatePinnedToCore(
rfidSimTask, // Task function
"RFID_Sim_Task", // Task name
4000, // Stack size (bytes)
NULL, // Task parameters
1, // Task priority
NULL, // Task handle
1 // Run on Core 1
);
// After setup(), the FreeRTOS scheduler will automatically start these tasks
}
// loop() can be empty as all logic is within FreeRTOS tasks
void loop() {
// All logic is executed within FreeRTOS tasks, so loop() can be empty
// Add a brief delay to avoid compilation warnings
vTaskDelay(1);
}
這個 Arduino ESP32 程式展示了一個多執行緒 (FreeRTOS) 應用程式,它模擬了 RFID 讀取器,將生成的 RFID UID(唯一識別碼)發佈到 MQTT 代理,並透過板載 LED 和 I2C LCD 顯示提供視覺回饋。它有效地將 RFID 模擬和 MQTT 通訊分離到 ESP32 兩個不同的 CPU 核心上運行的獨立任務中。
1. 程式概述
該程式設定 ESP32 以執行以下操作:
模擬 RFID 掃描: 當使用者在序列埠監控器中按下 ENTER 鍵時,會生成一個在特定範圍內的隨機 8 位元十六進位 UID。
顯示 UID: 模擬的 UID 會顯示在 16x2 I2C LCD 上。
WiFi 連線: 連接到指定的 Wi-Fi 網路。
MQTT 通訊: 將生成的 UID 發佈到公共 MQTT 代理上的 MQTT 主題。
LED 指示: 成功發佈 MQTT 後,板載 LED (GPIO 2) 會短暫閃爍。
FreeRTOS 多任務處理: 利用 ESP32 的雙核心架構,在核心 0 上運行 WiFi/MQTT 操作,在核心 1 上運行 RFID 模擬,並透過 FreeRTOS 佇列在它們之間進行通訊。
2. 包含的函式庫
該程式使用了幾個關鍵函式庫:
<MFRC522.h>:用於與 MFRC522 RFID 讀取器互動。儘管程式碼模擬了 RFID,但此函式庫的初始化 (mfrc522.PCD_Init()) 仍然存在,以符合潛在的硬體設定。
<SPI.h>:用於 SPI 通訊的標準函式庫,通常由 MFRC522 使用。
<PubSubClient.h>:用於 MQTT 用戶端功能,啟用與 MQTT 代理的通訊。
<WiFi.h>:用於管理 ESP32 上的 Wi-Fi 連線。
<freertos/task.h>:ESP32 上 FreeRTOS 功能的必備函式庫,允許創建和管理併發運行的任務(執行緒)。
<LiquidCrystal_I2C.h>:用於控制透過 I2C 連接的 LCD 顯示器。
3. 硬體配置和引腳定義
LiquidCrystal_I2C lcd(0x27, 16, 2);:初始化位址為 0x27、16 列 2 行的 I2C LCD。
#define SS_PIN 5:MFRC522 的從屬選擇引腳 (GPIO 5)。
#define RST_PIN 27:MFRC522 的重置引腳 (GPIO 27)。
#define LED_PIN 2:將板載 LED 引腳定義為 GPIO 2(ESP32 開發板常用)。
4. WiFi 和 MQTT 憑證
const char* ssid = "Wokwi-GUEST";:您的 Wi-Fi 網路名稱。
const char* password = "";:您的 Wi-Fi 密碼。(注意:Wokwi 通常為訪客網路使用空字串)。
const char* mqtt_server = "broker.mqttgo.io";:MQTT 代理的位址。mqttgo.io 是一個適合測試的公共代理。
const int mqtt_port = 1883;:標準的非加密 MQTT 埠。
const char* mqtt_topic = "alex9ufo/rfidUID";:將發佈 RFID UID 的 MQTT 主題。
const char* mqtt_client_id = "ESP32_RFID_DualCore_LED_Simulator";:MQTT 連線的唯一用戶端 ID。
5. FreeRTOS 任務管理和核心間通訊
該程式的並發核心在於 FreeRTOS:
QueueHandle_t uidQueue;:全域宣告一個 FreeRTOS 佇列。此佇列充當在不同 CPU 核心上運行的兩個任務之間的通訊通道(訊息緩衝區)。它旨在傳遞 String 物件(RFID UID)。
mqttTask(void *pvParameters) (核心 0):
此任務處理 Wi-Fi 連線、MQTT 用戶端初始化和 UID 發佈。
Serial.begin(115200);:初始化序列埠通訊(儘管 setup() 也會呼叫它)。
SPI.begin(); 和 mfrc522.PCD_Init();:初始化 SPI 和 MFRC522 模組。
使用 WiFi.begin() 連接到 Wi-Fi。
使用 client.setServer() 設定 MQTT 伺服器詳細資訊。
while(true) 迴圈不斷檢查 MQTT 連線 (reconnectMQTT()) 並維護它 (client.loop())。
xQueueReceive(uidQueue, &receivedUid, portMAX_DELAY) == pdPASS:這很關鍵。它嘗試從 uidQueue 中讀取 UID。portMAX_DELAY 表示任務將無限期阻塞(等待),直到佇列中可用資料。
收到 UID 後,它嘗試將其發佈到 mqtt_topic。
如果發佈成功,它會將 LED_PIN 設為 HIGH 500 毫秒,然後設為 LOW,提供視覺回饋。它還會清除 LCD 並列印「RFID IOT」。
vTaskDelay(10 / portTICK_PERIOD_MS);:一個很小的延遲,用於讓出 CPU 時間,防止此任務在沒有資料可用時佔用核心 0。
reconnectMQTT():
rfidSimTask(void *pvParameters) (核心 1):
此任務模擬 RFID 讀取器。它等待序列埠監控器中的使用者輸入。
if (Serial.available()) { ... }:檢查序列緩衝區中是否有任何資料可用。
char c = Serial.read(); if (c == '\n' || c == '\r') { ... }:檢測 ENTER 鍵按下(換行或回車字元)。
generateSpecificRangeUID():呼叫一個函式來創建模擬 UID。
生成的 UID 會列印到序列埠監控器。
lcd.setCursor(5, 1); lcd.print(uid);:在 LCD 的第二行顯示生成的 UID。
xQueueSend(uidQueue, &uid, portMAX_DELAY) != pdPASS:將生成的 UID 發送到 uidQueue。這裡的 portMAX_DELAY 表示如果佇列已滿,此任務也會阻塞,直到空間可用。
vTaskDelay(100 / portTICK_PERIOD_MS);:一個延遲,以避免序列埠上的忙等待,允許其他任務運行。
6. UID 生成
7. setup() 函式
setup() 函式在程式開始時執行一次:
lcd.init(); lcd.backlight();:初始化 LCD 並打開其背光。
Serial.begin(115200);:初始化序列埠通訊以進行除錯輸出。
uidQueue = xQueueCreate(5, sizeof(String));:創建一個容量為 5 個 String 物件的 FreeRTOS 佇列。它包含錯誤處理,如果佇列創建失敗則停止系統。
將操作說明列印到序列埠監控器,以便與模擬互動。
xTaskCreatePinnedToCore(...):
8. loop() 函式
9. 程式整體流程
初始化 (setup()):
mqttTask (核心 0):
rfidSimTask (核心 1):
通訊: uidQueue 作為橋樑,允許 rfidSimTask(生產者)將 UID 非同步且安全地發送給 mqttTask(消費者),利用 ESP32 的雙核心能力進行並行執行。
Node-Red程式
程式分成2個
[
{
"id": "c42cb358a4ec6272",
"type": "mqtt in",
"z": "86587a5248252f2d",
"name": "MQTT RFID UID Input ",
"topic": "alex9ufo/rfidUID",
"qos": "2",
"datatype": "auto",
"broker": "06d79adf7cfe46f5",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 140,
"y": 80,
"wires": [
[
"87813834088d7985",
"67e319dd036b422a",
"352becac094fd387"
]
]
},
{
"id": "87813834088d7985",
"type": "function",
"z": "86587a5248252f2d",
"name": "整理 MQTT 數據",
"func": "const uid = msg.payload;\nconst now = new Date();\nconst date = now.toISOString().split('T')[0];\nconst time = now.toTimeString().split(' ')[0];\n\nconst mode = flow.get('operatingMode') || '比對模式'; // 預設為比對模式\n\n// 將整理好的數據和模式儲存到 msg 物件中,供後續節點使用\nmsg.uid = uid;\nmsg.date = date;\nmsg.time = time;\nmsg.mode = mode; // 模式也作為 msg 屬性傳遞,方便 Switch 節點判斷\n\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 360,
"y": 80,
"wires": [
[
"5cc2e0ca97eed330",
"22f574cc87dbf420"
]
]
},
{
"id": "5cc2e0ca97eed330",
"type": "switch",
"z": "86587a5248252f2d",
"name": "根據操作模式",
"property": "mode",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "新增模式",
"vt": "str"
},
{
"t": "eq",
"v": "比對模式",
"vt": "str"
}
],
"checkall": "false",
"repair": false,
"outputs": 3,
"x": 400,
"y": 180,
"wires": [
[
"fa1619eb7b9480c2"
],
[
"20a62781eb2627aa"
],
[
"e9544c7443d9798e"
]
]
},
{
"id": "fa1619eb7b9480c2",
"type": "function",
"z": "86587a5248252f2d",
"name": "準備自動新增 SQL",
"func": "const uid = msg.uid;\nconst date = msg.date;\nconst time = msg.time;\n\nif (!uid || uid.trim() === '') {\n node.warn(\"MQTT 接收到空的 UID,在新增模式下不處理。\");\n msg.payload = \"MQTT 接收到空的 UID,已忽略\";\n return [null, msg]; \n}\n\nmsg.topic = \"INSERT INTO rfid_uids (uid, date, time) VALUES ($uid, $date, $time)\";\n//msg.payload = [ $uid: uid, $date: date, $time: time ];\nmsg.payload = [ uid,date,time ]\nreturn [msg, null];\n",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 630,
"y": 120,
"wires": [
[
"4c350b937a47dc28",
"060045734fceb2fc"
],
[
"cfeb88c2fee440a4"
]
]
},
{
"id": "20a62781eb2627aa",
"type": "function",
"z": "86587a5248252f2d",
"name": "準備自動比對 SQL",
"func": "const uid = msg.uid;\n\nif (!uid || uid.trim() === '') {\n node.warn(\"MQTT 接收到空的 UID,在比對模式下不處理。\");\n msg.payload = { led: 'grey', text: '接收到空 UID,請檢查' };\n return [null, msg];\n}\n\nmsg.topic = \"SELECT * FROM rfid_uids WHERE uid = $uid\";\n//msg.payload = { $uid: uid };\nmsg.payload = [ uid ]\nflow.set('currentUID', uid); // 儲存當前 UID 供後續比對使用\n\nreturn [msg, null];\n",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 630,
"y": 180,
"wires": [
[
"4c350b937a47dc28"
],
[
"cfeb88c2fee440a4"
]
]
},
{
"id": "e9544c7443d9798e",
"type": "function",
"z": "86587a5248252f2d",
"name": "處理無效 MQTT 輸入",
"func": "const uid = msg.uid;\nconst mode = msg.mode;\n\nlet text = `當前模式: ${mode},不處理 MQTT 訊息。`;\nif (!uid || uid.trim() === '') {\n text = `MQTT 接收到空的 UID (模式: ${mode}),已忽略。`;\n}\n\nmsg.payload = { led: 'grey', text: text };\nreturn msg;\n",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 640,
"y": 240,
"wires": [
[
"cfeb88c2fee440a4"
]
]
},
{
"id": "4c350b937a47dc28",
"type": "sqlite",
"z": "86587a5248252f2d",
"mydb": "rfidDB",
"sqlquery": "msg.topic",
"sql": "msg.topic",
"name": "RFID UID 資料庫 (自動)",
"x": 890,
"y": 120,
"wires": [
[
"b8967e0218a84006"
]
]
},
{
"id": "b8967e0218a84006",
"type": "function",
"z": "86587a5248252f2d",
"name": "處理資料庫結果",
"func": "const mode = flow.get('operatingMode');\nconst currentActionType = msg.dbActionType; \n\nlet outputMsg = {};\n\nif (mode === '新增模式' && !currentActionType) { // 來自 MQTT 自動新增\n outputMsg.payload = \"UID 新增成功!\";\n node.status({ fill: \"green\", shape: \"dot\", text: \"UID 已新增\" });\n // 自動觸發表格更新\n node.send([outputMsg, { topic: \"SELECT * FROM rfid_uids ORDER BY id DESC LIMIT 50\" }]);\n return; \n} else if (mode === '比對模式' && !currentActionType) { // 來自 MQTT 自動比對\n const currentUID = flow.get('currentUID');\n if (msg.payload && msg.payload.length > 0) {\n outputMsg.payload = { led: 'green', text: `通過:UID ${currentUID} 已存在` };\n node.status({ fill: \"green\", shape: \"dot\", text: \"通過\" });\n } else {\n outputMsg.payload = { led: 'red', text: `錯誤:UID ${currentUID} 不存在` };\n node.status({ fill: \"red\", shape: \"dot\", text: \"錯誤\" });\n }\n} else { // 手動操作的結果 (由 currentActionType 判斷)\n switch (currentActionType) {\n case 'add_manual':\n outputMsg.payload = \"手動新增成功!\";\n node.status({ fill: \"green\", shape: \"dot\", text: \"手動新增成功\" });\n // 自動觸發表格更新\n node.send([outputMsg, { payload: \"SELECT * FROM rfid_uids ORDER BY id DESC LIMIT 50\" }]);\n return;\n case 'update_manual':\n outputMsg.payload = \"更正成功!\";\n node.status({ fill: \"green\", shape: \"dot\", text: \"更正成功\" });\n node.send([outputMsg, { payload: \"SELECT * FROM rfid_uids ORDER BY id DESC LIMIT 50\" }]);\n return;\n case 'delete_manual':\n outputMsg.payload = \"刪除成功!\";\n node.status({ fill: \"green\", shape: \"dot\", text: \"刪除成功\" });\n node.send([outputMsg, { payload: \"SELECT * FROM rfid_uids ORDER BY id DESC LIMIT 50\" }]);\n return;\n case 'query_manual':\n if (msg.payload && msg.payload.length > 0) {\n outputMsg.payload = \"查詢結果:\" + JSON.stringify(msg.payload);\n node.status({ fill: \"blue\", shape: \"dot\", text: \"查詢成功\" });\n // 如果是手動查詢,第二個輸出端口連接到表格,直接顯示查詢結果\n return [outputMsg, { payload: msg.payload }]; \n } else {\n outputMsg.payload = \"未找到符合條件的資料。\";\n node.status({ fill: \"yellow\", shape: \"dot\", text: \"未找到\" });\n }\n break;\n default:\n outputMsg.payload = \"資料庫操作完成。\";\n node.status({ fill: \"grey\", shape: \"dot\", text: \"操作完成\" });\n break;\n }\n}\n\n// 對於非新增、更正、刪除的結果,或是 MQTT 比對的結果,走這裡\nreturn [outputMsg, null]; \n",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1120,
"y": 120,
"wires": [
[
"fb1dc5cc0dc41ba4"
],
[
"0bca09d23f357815",
"a8ca45c5f2937486"
]
]
},
{
"id": "933e6c5206e27d71",
"type": "ui_switch",
"z": "86587a5248252f2d",
"name": "新增/比對模式",
"label": "模式切換",
"tooltip": "切換自動新增或自動比對模式",
"group": "q8r7s6t5.u4v3w2",
"order": 3,
"width": 3,
"height": 1,
"passthru": true,
"decouple": "false",
"topic": "mode",
"topicType": "str",
"style": "",
"onvalue": "新增模式",
"onvalueType": "str",
"onicon": "",
"oncolor": "",
"offvalue": "比對模式",
"offvalueType": "str",
"officon": "",
"offcolor": "",
"animate": true,
"className": "",
"x": 160,
"y": 300,
"wires": [
[
"1d716d2303cde764"
]
]
},
{
"id": "1d716d2303cde764",
"type": "change",
"z": "86587a5248252f2d",
"name": "儲存模式",
"rules": [
{
"t": "set",
"p": "operatingMode",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 340,
"y": 300,
"wires": [
[
"666a12327dafb890"
]
]
},
{
"id": "666a12327dafb890",
"type": "ui_text",
"z": "86587a5248252f2d",
"group": "q8r7s6t5.u4v3w2",
"order": 4,
"width": 3,
"height": 1,
"name": "目前模式",
"label": "目前:",
"format": "{{msg.payload}}",
"layout": "row-spread",
"className": "",
"x": 540,
"y": 300,
"wires": []
},
{
"id": "a8ca45c5f2937486",
"type": "sqlite",
"z": "86587a5248252f2d",
"mydb": "rfidDB",
"sqlquery": "msg.topic",
"sql": "msg.payload",
"name": "查詢所有資料",
"x": 1220,
"y": 260,
"wires": [
[
"de435e0837ac94a8"
]
]
},
{
"id": "67e319dd036b422a",
"type": "debug",
"z": "86587a5248252f2d",
"name": "debug 331",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 390,
"y": 20,
"wires": []
},
{
"id": "22f574cc87dbf420",
"type": "debug",
"z": "86587a5248252f2d",
"name": "debug 332",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 570,
"y": 60,
"wires": []
},
{
"id": "060045734fceb2fc",
"type": "debug",
"z": "86587a5248252f2d",
"name": "debug 333",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 850,
"y": 80,
"wires": []
},
{
"id": "352becac094fd387",
"type": "link out",
"z": "86587a5248252f2d",
"name": "link out 68",
"mode": "link",
"links": [],
"x": 265,
"y": 120,
"wires": []
},
{
"id": "0bca09d23f357815",
"type": "debug",
"z": "86587a5248252f2d",
"name": "debug 336",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 1390,
"y": 200,
"wires": []
},
{
"id": "fb1dc5cc0dc41ba4",
"type": "link out",
"z": "86587a5248252f2d",
"name": "link out 72",
"mode": "link",
"links": [
"a176cedec6a2972c"
],
"x": 1295,
"y": 100,
"wires": []
},
{
"id": "cfeb88c2fee440a4",
"type": "link out",
"z": "86587a5248252f2d",
"name": "link out 73",
"mode": "link",
"links": [
"a176cedec6a2972c"
],
"x": 895,
"y": 220,
"wires": []
},
{
"id": "de435e0837ac94a8",
"type": "link out",
"z": "86587a5248252f2d",
"name": "link out 74",
"mode": "link",
"links": [
"6dd18624c6038143"
],
"x": 1375,
"y": 260,
"wires": []
},
{
"id": "06d79adf7cfe46f5",
"type": "mqtt-broker",
"name": "broker.mqttgo.io",
"broker": "broker.mqttgo.io",
"port": "1883",
"clientid": "",
"autoConnect": true,
"usetls": false,
"compatmode": false,
"keepalive": "60"
},
{
"id": "rfidDB",
"type": "sqlitedb",
"db": "rfidlog.db",
"mode": "RWC"
},
{
"id": "q8r7s6t5.u4v3w2",
"type": "ui_group",
"name": "操作面板",
"tab": "o2p1q0r9.s8t7u6",
"order": 2,
"disp": true,
"width": 6,
"collapse": false,
"className": ""
},
{
"id": "o2p1q0r9.s8t7u6",
"type": "ui_tab",
"name": "RFID UID 管理",
"icon": "dashboard",
"disabled": false,
"hidden": false
}
]
第二個
[
{
"id": "4e533e285a4e2edf",
"type": "ui_button",
"z": "8803570705a2bb98",
"name": "刪除資料表",
"group": "q8r7s6t5.u4v3w2",
"order": 2,
"width": 0,
"height": 0,
"passthru": false,
"label": "刪除資料表 (危險操作)",
"tooltip": "刪除 rfid_uids 表格 (所有資料將遺失)",
"color": "#FF0000",
"bgcolor": "",
"className": "",
"icon": "",
"payload": "DROP TABLE IF EXISTS rfid_uids",
"payloadType": "str",
"topic": "db_init",
"topicType": "str",
"x": 130,
"y": 100,
"wires": [
[
"f7ed826245e17e14",
"a07984ad118c1ade"
]
]
},
{
"id": "ef2b49c2466a5551",
"type": "ui_text_input",
"z": "8803570705a2bb98",
"name": "UID Input",
"label": "UID:",
"tooltip": "",
"group": "q8r7s6t5.u4v3w2",
"order": 6,
"width": 0,
"height": 0,
"passthru": true,
"mode": "text",
"delay": 300,
"topic": "uid_input",
"sendOnBlur": true,
"className": "",
"topicType": "str",
"x": 130,
"y": 220,
"wires": [
[
"f104df67aa429342",
"35907121e5ad7825"
]
]
},
{
"id": "f104df67aa429342",
"type": "change",
"z": "8803570705a2bb98",
"name": "儲存 UID",
"rules": [
{
"t": "set",
"p": "manualUid",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 300,
"y": 220,
"wires": [
[]
]
},
{
"id": "343af340b74fe7ea",
"type": "ui_text_input",
"z": "8803570705a2bb98",
"name": "ID Input",
"label": "ID:",
"tooltip": "",
"group": "q8r7s6t5.u4v3w2",
"order": 5,
"width": 0,
"height": 0,
"passthru": true,
"mode": "number",
"delay": 300,
"topic": "id_input",
"sendOnBlur": true,
"className": "",
"topicType": "str",
"x": 130,
"y": 280,
"wires": [
[
"944ba091ba0fb373",
"35907121e5ad7825"
]
]
},
{
"id": "944ba091ba0fb373",
"type": "change",
"z": "8803570705a2bb98",
"name": "儲存 ID",
"rules": [
{
"t": "set",
"p": "manualId",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 300,
"y": 280,
"wires": [
[]
]
},
{
"id": "4fe3ea9311296549",
"type": "change",
"z": "8803570705a2bb98",
"name": "儲存 Date",
"rules": [
{
"t": "set",
"p": "manualDate",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 520,
"y": 340,
"wires": [
[]
]
},
{
"id": "5ee6e7c00f87f288",
"type": "change",
"z": "8803570705a2bb98",
"name": "儲存 Time",
"rules": [
{
"t": "set",
"p": "manualTime",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 520,
"y": 420,
"wires": [
[]
]
},
{
"id": "4756e6d8176cde4e",
"type": "ui_button",
"z": "8803570705a2bb98",
"name": "新增一筆",
"group": "q8r7s6t5.u4v3w2",
"order": 9,
"width": 3,
"height": 1,
"passthru": false,
"label": "新增一筆",
"tooltip": "根據 UID, Date, Time 欄位新增資料",
"color": "",
"bgcolor": "",
"icon": "",
"payload": "add",
"payloadType": "str",
"topic": "db_action",
"x": 120,
"y": 480,
"wires": [
[
"181d7959c95eb88f",
"ebb453a2fd57b137",
"45e90f0c672cb5cc"
]
]
},
{
"id": "2c54998dab61501e",
"type": "ui_button",
"z": "8803570705a2bb98",
"name": "查詢一筆",
"group": "q8r7s6t5.u4v3w2",
"order": 10,
"width": 3,
"height": 1,
"passthru": false,
"label": "查詢一筆",
"tooltip": "根據 UID 或 ID 欄位查詢資料",
"color": "",
"bgcolor": "",
"icon": "",
"payload": "query",
"payloadType": "str",
"topic": "db_action",
"x": 120,
"y": 540,
"wires": [
[
"181d7959c95eb88f",
"ebb453a2fd57b137"
]
]
},
{
"id": "7bbc093d5f1d7165",
"type": "ui_button",
"z": "8803570705a2bb98",
"name": "更正一筆",
"group": "q8r7s6t5.u4v3w2",
"order": 11,
"width": 3,
"height": 1,
"passthru": false,
"label": "更正一筆",
"tooltip": "根據 ID 欄位更正 UID, Date, Time",
"color": "",
"bgcolor": "",
"icon": "",
"payload": "update",
"payloadType": "str",
"topic": "db_action",
"x": 120,
"y": 600,
"wires": [
[
"181d7959c95eb88f",
"ebb453a2fd57b137"
]
]
},
{
"id": "d14dcf78e91fc657",
"type": "ui_button",
"z": "8803570705a2bb98",
"name": "刪除一筆",
"group": "q8r7s6t5.u4v3w2",
"order": 12,
"width": 3,
"height": 1,
"passthru": false,
"label": "刪除一筆",
"tooltip": "根據 ID 欄位刪除資料",
"color": "",
"bgcolor": "",
"icon": "",
"payload": "delete",
"payloadType": "str",
"topic": "db_action",
"x": 120,
"y": 660,
"wires": [
[
"181d7959c95eb88f",
"ebb453a2fd57b137"
]
]
},
{
"id": "181d7959c95eb88f",
"type": "function",
"z": "8803570705a2bb98",
"name": "準備資料庫查詢",
"func": "const action = msg.payload;\nconst uid = flow.get('manualUid') || '';\nconst id = flow.get('manualId') || '';\nlet date = flow.get('manualDate') || '';\nlet time = flow.get('manualTime') || '';\n\nconst now = new Date();\nif (!date) date = now.toISOString().split('T')[0];\nif (!time) time = now.toTimeString().split(' ')[0].substring(0, 8); // 確保格式為 HH:MM:SS\n\nmsg.dbAction = action; // 儲存當前操作類型\n\nswitch (action) {\n case 'add':\n if (!uid.trim()) {\n msg.payload = \"新增失敗:UID 不能為空!\";\n return [null, msg];\n }\n msg.topic = \"INSERT INTO rfid_uids (uid, date, time) VALUES ($uid, $date, $time)\";\n //msg.payload = { $uid: uid, $date: date, $time: time };\n msg.payload = [uid, date, time ]\n\n break;\n case 'query':\n if (!uid.trim() && !id) {\n msg.payload = \"查詢失敗:UID 或 ID 至少需要一個!\";\n return [null, msg];\n }\n if (id) {\n if (isNaN(parseInt(id))) {\n msg.payload = \"查詢失敗:ID 必須是數字!\";\n return [null, msg];\n }\n msg.topic = `SELECT * FROM rfid_uids WHERE id = ${parseInt(id)}`;\n } else {\n msg.topic = `SELECT * FROM rfid_uids WHERE uid = '${uid}'`;\n }\n msg.payload = {}; // 清空payload,避免影響查詢結果\n break;\n case 'update':\n if (!id || isNaN(parseInt(id))) {\n msg.payload = \"更正失敗:ID 必須是數字且不能為空!\";\n return [null, msg];\n }\n if (!uid.trim()) {\n msg.payload = \"更正失敗:UID 不能為空!\";\n return [null, msg];\n }\n msg.topic = \"UPDATE rfid_uids SET uid = $uid, date = $date, time = $time WHERE id = $id\";\n //msg.payload = { $uid: uid, $date: date, $time: time, $id: parseInt(id) };\n msg.payload = [uid, date, time, parseInt(id) ] \n break;\n case 'delete':\n if (!id || isNaN(parseInt(id))) {\n msg.payload = \"刪除失敗:ID 必須是數字且不能為空!\";\n return [null, msg];\n }\n msg.topic = `DELETE FROM rfid_uids WHERE id = ${parseInt(id)}`;\n msg.payload = {};\n break;\n default:\n msg.payload = \"未知操作類型。\";\n return [null, msg];\n}\nreturn [msg, null];\n",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 380,
"y": 560,
"wires": [
[
"ca9d667ecac97891"
],
[
"e5071cf3e004e695"
]
]
},
{
"id": "ca9d667ecac97891",
"type": "sqlite",
"z": "8803570705a2bb98",
"mydb": "rfidDB",
"sqlquery": "msg.topic",
"sql": "msg.topic",
"name": "執行資料庫查詢",
"x": 620,
"y": 560,
"wires": [
[
"f369f5255955abc5"
]
]
},
{
"id": "f369f5255955abc5",
"type": "function",
"z": "8803570705a2bb98",
"name": "處理資料庫結果",
"func": "const action = msg.dbAction;\nlet outputMsg = {};\n\nswitch (action) {\n case 'add':\n outputMsg.payload = \"新增成功!\";\n node.status({ fill: \"green\", shape: \"dot\", text: \"新增成功\" });\n // 新增後自動刷新表格\n node.send([outputMsg, { payload: \"SELECT * FROM rfid_uids ORDER BY id DESC LIMIT 50\" }]);\n return;\n case 'query':\n if (msg.payload && msg.payload.length > 0) {\n outputMsg.payload = `查詢成功:找到 ${msg.payload.length} 筆資料。`;\n node.status({ fill: \"blue\", shape: \"dot\", text: \"查詢成功\" });\n // 將查詢結果直接送往表格\n return [outputMsg, { payload: msg.payload }];\n } else {\n outputMsg.payload = \"查詢結果:未找到符合條件的資料。\";\n node.status({ fill: \"yellow\", shape: \"dot\", text: \"未找到\" });\n }\n break;\n case 'update':\n if (msg.payload.changes > 0) {\n outputMsg.payload = \"更正成功!\";\n node.status({ fill: \"green\", shape: \"dot\", text: \"更正成功\" });\n } else {\n outputMsg.payload = \"更正失敗:未找到符合 ID 的資料或資料無變動。\";\n node.status({ fill: \"yellow\", shape: \"dot\", text: \"更正失敗\" });\n }\n // 更正後自動刷新表格\n node.send([outputMsg, { payload: \"SELECT * FROM rfid_uids ORDER BY id DESC LIMIT 50\" }]);\n return;\n case 'delete':\n if (msg.payload.changes > 0) {\n outputMsg.payload = \"刪除成功!\";\n node.status({ fill: \"green\", shape: \"dot\", text: \"刪除成功\" });\n } else {\n outputMsg.payload = \"刪除失敗:未找到符合 ID 的資料。\";\n node.status({ fill: \"yellow\", shape: \"dot\", text: \"刪除失敗\" });\n }\n // 刪除後自動刷新表格\n node.send([outputMsg, { payload: \"SELECT * FROM rfid_uids ORDER BY id DESC LIMIT 50\" }]);\n return;\n default:\n outputMsg.payload = \"資料庫操作完成。\";\n node.status({ fill: \"grey\", shape: \"dot\", text: \"操作完成\" });\n break;\n}\n\nreturn [outputMsg, null]; // 如果沒有自動發送表格更新,則這裡只發送狀態訊息\n",
"outputs": 2,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 820,
"y": 560,
"wires": [
[
"6787d2155f172ff6"
],
[
"ae368b828cd9f887"
]
]
},
{
"id": "6787d2155f172ff6",
"type": "ui_text",
"z": "8803570705a2bb98",
"group": "j1k2l3m4.n5o6p7",
"order": 1,
"width": 0,
"height": 0,
"name": "操作狀態/訊息",
"label": "狀態:",
"format": "<b>{{msg.payload}}</b>",
"layout": "row-spread",
"x": 1120,
"y": 620,
"wires": []
},
{
"id": "4e921892d5a2abe3",
"type": "sqlite",
"z": "8803570705a2bb98",
"mydb": "rfidDB",
"sqlquery": "msg.topic",
"sql": "msg.payload",
"name": "初始化資料庫",
"x": 620,
"y": 80,
"wires": [
[
"82afc45e6184ec2c",
"93f0621d7f1859bd",
"6e3aef0a03e52a08"
]
]
},
{
"id": "82afc45e6184ec2c",
"type": "function",
"z": "8803570705a2bb98",
"name": "處理初始化結果",
"func": "if (msg.topic === \"DROP TABLE IF EXISTS rfid_uids\") {\n msg.payload = \"資料表已刪除!\";\n node.status({ fill: \"red\", shape: \"dot\", text: \"表已刪除\" });\n} else if (msg.topic === \"CREATE TABLE IF NOT EXISTS rfid_uids (id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, date TEXT, time TEXT)\") {\n msg.payload = \"資料表已建立或已存在。\";\n node.status({ fill: \"green\", shape: \"dot\", text: \"表已建立\" });\n}\n// 初始化完成後自動查詢顯示所有資料\nnode.send({ payload: \"SELECT * FROM rfid_uids ORDER BY id DESC LIMIT 50\", topic: \"query_all_auto\" });\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 760,
"y": 120,
"wires": [
[
"97aca1aa6fc231e3"
]
]
},
{
"id": "b6d037ea4ba2d032",
"type": "ui_button",
"z": "8803570705a2bb98",
"name": "顯示所有資料",
"group": "q8r7s6t5.u4v3w2",
"order": 13,
"width": 3,
"height": 1,
"passthru": false,
"label": "顯示所有資料 (最新50筆)",
"tooltip": "顯示資料庫中最新的50筆資料",
"color": "",
"bgcolor": "",
"className": "",
"icon": "",
"payload": "SELECT * FROM rfid_uids ORDER BY id DESC LIMIT 50",
"payloadType": "str",
"topic": "query_all",
"topicType": "str",
"x": 140,
"y": 720,
"wires": [
[
"2eb63f271bc6546b"
]
]
},
{
"id": "18c02dcd71baace3",
"type": "sqlite",
"z": "8803570705a2bb98",
"mydb": "rfidDB",
"sqlquery": "msg.topic",
"sql": "msg.payload",
"name": "查詢所有資料",
"x": 460,
"y": 720,
"wires": [
[
"ae368b828cd9f887"
]
]
},
{
"id": "ae368b828cd9f887",
"type": "ui_table",
"z": "8803570705a2bb98",
"group": "j1k2l3m4.n5o6p7",
"name": "資料庫內容",
"order": 4,
"width": 10,
"height": 8,
"columns": [
{
"field": "id",
"title": "ID",
"width": "10%",
"align": "left",
"formatter": "plaintext",
"formatterParams": {
"target": "_blank"
}
},
{
"field": "uid",
"title": "UID",
"width": "30%",
"align": "left",
"formatter": "plaintext",
"formatterParams": {
"target": "_blank"
}
},
{
"field": "date",
"title": "Date",
"width": "20%",
"align": "left",
"formatter": "plaintext",
"formatterParams": {
"target": "_blank"
}
},
{
"field": "time",
"title": "Time",
"width": "20%",
"align": "left",
"formatter": "plaintext",
"formatterParams": {
"target": "_blank"
}
}
],
"outputs": 0,
"cts": false,
"x": 690,
"y": 720,
"wires": []
},
{
"id": "a17b8870977175db",
"type": "function",
"z": "8803570705a2bb98",
"name": "function ",
"func": "//CREATE TABLE IF NOT EXISTS rfid_uids(id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, date TEXT, time TEXT)\nmsg.topic =\"CREATE TABLE IF NOT EXISTS rfid_uids (id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, date TEXT, time TEXT)\";\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 300,
"y": 20,
"wires": [
[
"4e921892d5a2abe3"
]
]
},
{
"id": "51ee77407fe794b7",
"type": "function",
"z": "8803570705a2bb98",
"name": "function ",
"func": "//CREATE TABLE IF NOT EXISTS rfid_uids(id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, date TEXT, time TEXT)\nmsg.topic =\"DROP TABLE IF EXISTS rfid_uids\";\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 420,
"y": 160,
"wires": [
[
"4e921892d5a2abe3"
]
]
},
{
"id": "f7ed826245e17e14",
"type": "ui_toast",
"z": "8803570705a2bb98",
"position": "prompt",
"displayTime": "3",
"highlight": "",
"sendall": true,
"outputs": 1,
"ok": "OK",
"cancel": "Cancel",
"raw": true,
"className": "",
"topic": "",
"name": "",
"x": 290,
"y": 100,
"wires": [
[
"265fccece79dc666"
]
]
},
{
"id": "265fccece79dc666",
"type": "function",
"z": "8803570705a2bb98",
"name": "function 確認",
"func": "var topic=msg.payload;\nif (topic==\"\"){\n return [msg,null];\n \n}\nif (topic==\"Cancel\"){\n return [null,msg];\n \n}\nreturn msg;",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 270,
"y": 160,
"wires": [
[
"51ee77407fe794b7"
],
[]
]
},
{
"id": "a116f6518754dc4a",
"type": "link in",
"z": "8803570705a2bb98",
"name": "link in 65",
"links": [
"97aca1aa6fc231e3",
"a07984ad118c1ade",
"e5071cf3e004e695"
],
"x": 945,
"y": 460,
"wires": [
[
"6787d2155f172ff6"
]
]
},
{
"id": "97aca1aa6fc231e3",
"type": "link out",
"z": "8803570705a2bb98",
"name": "link out 69",
"mode": "link",
"links": [
"a116f6518754dc4a",
"2a46e6024057b38e"
],
"x": 845,
"y": 180,
"wires": []
},
{
"id": "a07984ad118c1ade",
"type": "link out",
"z": "8803570705a2bb98",
"name": "link out 70",
"mode": "link",
"links": [
"a116f6518754dc4a"
],
"x": 285,
"y": 60,
"wires": []
},
{
"id": "ff12f4e3b2a6de15",
"type": "inject",
"z": "8803570705a2bb98",
"name": "定時/手動更新日期時間",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "30",
"crontab": "",
"once": true,
"onceDelay": "0.1",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 380,
"wires": [
[
"214a40dcd76d832d",
"c8f57918489126df"
]
]
},
{
"id": "214a40dcd76d832d",
"type": "function",
"z": "8803570705a2bb98",
"name": "獲取日期時間字串",
"func": "const now = new Date(msg.payload);\nconst date = now.toISOString().split('T')[0];\nconst time = now.toTimeString().split(' ')[0].substring(0, 8); // 確保格式為 HH:MM:SS\n\n// 分別輸出給 Date Input 和 Time Input UI 節點\nreturn [\n { payload: date, topic: \"date_ui_update\" },\n { payload: time, topic: \"time_ui_update\" }\n];",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 330,
"y": 380,
"wires": [
[
"b6e1d2c99473cdd7"
],
[
"e6865cac23f1af10"
]
]
},
{
"id": "b6e1d2c99473cdd7",
"type": "ui_text_input",
"z": "8803570705a2bb98",
"name": "Date Input",
"label": "Date (YYYY-MM-DD):",
"tooltip": "",
"group": "q8r7s6t5.u4v3w2",
"order": 7,
"width": 0,
"height": 0,
"passthru": true,
"mode": "text",
"delay": 300,
"topic": "date_input",
"sendOnBlur": true,
"className": "",
"topicType": "str",
"x": 350,
"y": 340,
"wires": [
[
"4fe3ea9311296549"
]
]
},
{
"id": "e6865cac23f1af10",
"type": "ui_text_input",
"z": "8803570705a2bb98",
"name": "Time Input",
"label": "Time (HH:MM:SS):",
"group": "q8r7s6t5.u4v3w2",
"order": 8,
"width": 0,
"height": 0,
"passthru": true,
"mode": "text",
"delay": 300,
"topic": "time_input",
"sendOnBlur": true,
"className": "",
"x": 350,
"y": 420,
"wires": [
[
"5ee6e7c00f87f288"
]
]
},
{
"id": "e5071cf3e004e695",
"type": "link out",
"z": "8803570705a2bb98",
"name": "link out 71",
"mode": "link",
"links": [
"a116f6518754dc4a"
],
"x": 505,
"y": 600,
"wires": []
},
{
"id": "2a46e6024057b38e",
"type": "link in",
"z": "8803570705a2bb98",
"name": "link in 66",
"links": [
"97aca1aa6fc231e3"
],
"x": 495,
"y": 680,
"wires": [
[
"ae368b828cd9f887"
]
]
},
{
"id": "2eb63f271bc6546b",
"type": "function",
"z": "8803570705a2bb98",
"name": "function ",
"func": "//CREATE TABLE IF NOT EXISTS rfid_uids(id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, date TEXT, time TEXT)\nmsg.topic =\"SELECT * FROM rfid_uids ORDER BY id DESC LIMIT 50\";\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 300,
"y": 720,
"wires": [
[
"18c02dcd71baace3"
]
]
},
{
"id": "e3f1c921e9db94a0",
"type": "debug",
"z": "8803570705a2bb98",
"name": "debug 338",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 730,
"y": 460,
"wires": []
},
{
"id": "09e4495b878ebfd0",
"type": "ui_button",
"z": "8803570705a2bb98",
"name": "刪除所有資料",
"group": "q8r7s6t5.u4v3w2",
"order": 14,
"width": 3,
"height": 1,
"passthru": false,
"label": "刪除所有資料 (危險操作)",
"tooltip": "清除 rfid_uids 表格中所有資料",
"color": "#FF0000",
"bgcolor": "",
"icon": "",
"payload": "DELETE FROM rfid_uids",
"payloadType": "str",
"topic": "delete_all_data",
"x": 140,
"y": 800,
"wires": [
[
"d1820e3c6d032ab5"
]
]
},
{
"id": "7661df635dca548a",
"type": "sqlite",
"z": "8803570705a2bb98",
"mydb": "rfidDB",
"sqlquery": "msg.topic",
"sql": "msg.payload",
"name": "執行刪除所有資料",
"x": 470,
"y": 820,
"wires": [
[
"0a4d673ceba264a8"
]
]
},
{
"id": "0a4d673ceba264a8",
"type": "function",
"z": "8803570705a2bb98",
"name": "處理刪除所有結果",
"func": "let outputMsg = {};\nif (msg.payload.changes !== undefined) {\n outputMsg.payload = `成功刪除所有 ${msg.payload.changes} 筆資料!`;\n node.status({ fill: \"red\", shape: \"dot\", text: \"所有資料已刪除\" });\n} else {\n outputMsg.payload = \"刪除所有資料操作完成,但未返回變動數量。\";\n node.status({ fill: \"yellow\", shape: \"dot\", text: \"操作完成\" });\n}\n\n// 刪除後自動刷新表格,顯示空表格或最新資料\nnode.send([outputMsg, { payload: \"SELECT * FROM rfid_uids ORDER BY id DESC LIMIT 50\" }]);\n//return null; // 此節點只使用第二個輸出到表格和狀態\n//return [outputMsg, { payload: msg.payload }];\nreturn [outputMsg];",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 670,
"y": 820,
"wires": [
[
"6787d2155f172ff6"
],
[
"ae368b828cd9f887"
]
]
},
{
"id": "bb6d8a14c1552e60",
"type": "function",
"z": "8803570705a2bb98",
"name": "function ",
"func": "//CREATE TABLE IF NOT EXISTS rfid_uids(id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, date TEXT, time TEXT)\nmsg.topic =\"DELETE FROM rfid_uids\";\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 320,
"y": 900,
"wires": [
[
"7661df635dca548a"
]
]
},
{
"id": "d1820e3c6d032ab5",
"type": "ui_toast",
"z": "8803570705a2bb98",
"position": "prompt",
"displayTime": "3",
"highlight": "",
"sendall": true,
"outputs": 1,
"ok": "OK",
"cancel": "Cancel",
"raw": true,
"className": "",
"topic": "",
"name": "",
"x": 150,
"y": 860,
"wires": [
[
"aa10cbfc2a19a655"
]
]
},
{
"id": "aa10cbfc2a19a655",
"type": "function",
"z": "8803570705a2bb98",
"name": "function 確認",
"func": "var topic=msg.payload;\nif (topic==\"\"){\n return [msg,null];\n \n}\nif (topic==\"Cancel\"){\n return [null,msg];\n \n}\nreturn msg;",
"outputs": 2,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 150,
"y": 900,
"wires": [
[
"bb6d8a14c1552e60"
],
[]
]
},
{
"id": "ebb453a2fd57b137",
"type": "debug",
"z": "8803570705a2bb98",
"name": "debug 339",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 330,
"y": 480,
"wires": []
},
{
"id": "45e90f0c672cb5cc",
"type": "function",
"z": "8803570705a2bb98",
"name": "debug function {uid,id,date,time}",
"func": "const uid = flow.get('manualUid') || '';\nconst id = flow.get('manualId') || '';\nlet date = flow.get('manualDate') || '';\nlet time = flow.get('manualTime') || '';\nmsg.payload={uid,id,date,time};\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 610,
"y": 500,
"wires": [
[
"e3f1c921e9db94a0"
]
]
},
{
"id": "35907121e5ad7825",
"type": "debug",
"z": "8803570705a2bb98",
"name": "debug 340",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 450,
"y": 260,
"wires": []
},
{
"id": "c8f57918489126df",
"type": "function",
"z": "8803570705a2bb98",
"name": "get current id function ",
"func": "msg.payload = flow.get('manualId') || '';\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 140,
"y": 320,
"wires": [
[
"343af340b74fe7ea"
]
]
},
{
"id": "a176cedec6a2972c",
"type": "link in",
"z": "8803570705a2bb98",
"name": "link in 67",
"links": [
"fb1dc5cc0dc41ba4",
"cfeb88c2fee440a4"
],
"x": 1035,
"y": 560,
"wires": [
[
"6787d2155f172ff6"
]
]
},
{
"id": "16844919824c6623",
"type": "function",
"z": "8803570705a2bb98",
"name": "Blank function ",
"func": "msg.payload=\" <>\";\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1180,
"y": 760,
"wires": [
[
"6787d2155f172ff6",
"6864d78b5c31a066"
]
]
},
{
"id": "6864d78b5c31a066",
"type": "debug",
"z": "8803570705a2bb98",
"name": "debug 341",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 1170,
"y": 820,
"wires": []
},
{
"id": "36df1b2f74179889",
"type": "inject",
"z": "8803570705a2bb98",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "5",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 1020,
"y": 760,
"wires": [
[
"16844919824c6623"
]
]
},
{
"id": "6dd18624c6038143",
"type": "link in",
"z": "8803570705a2bb98",
"name": "link in 68",
"links": [
"de435e0837ac94a8"
],
"x": 275,
"y": 680,
"wires": [
[
"2eb63f271bc6546b"
]
]
},
{
"id": "fe28b77b4375d3d4",
"type": "ui_button",
"z": "8803570705a2bb98",
"name": "建立資料表",
"group": "q8r7s6t5.u4v3w2",
"order": 1,
"width": 0,
"height": 0,
"passthru": false,
"label": "建立資料表 ",
"tooltip": "建立 rfid_uids 表格,ID將從 1001 開始遞增。",
"color": "",
"bgcolor": "",
"className": "",
"icon": "",
"payload": "CREATE TABLE IF NOT EXISTS rfid_uids (id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT NOT NULL UNIQUE, date TEXT, time TEXT); INSERT OR IGNORE INTO rfid_uids (id, uid, date, time) ",
"payloadType": "str",
"topic": "db_init_with_start_id",
"topicType": "str",
"x": 130,
"y": 20,
"wires": [
[
"a17b8870977175db",
"a07984ad118c1ade"
]
]
},
{
"id": "93f0621d7f1859bd",
"type": "debug",
"z": "8803570705a2bb98",
"name": "debug 342",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 810,
"y": 60,
"wires": []
},
{
"id": "6e3aef0a03e52a08",
"type": "debug",
"z": "8803570705a2bb98",
"name": "debug 343",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "topic",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 810,
"y": 20,
"wires": []
},
{
"id": "q8r7s6t5.u4v3w2",
"type": "ui_group",
"name": "操作面板",
"tab": "o2p1q0r9.s8t7u6",
"order": 2,
"disp": true,
"width": 6,
"collapse": false,
"className": ""
},
{
"id": "rfidDB",
"type": "sqlitedb",
"db": "rfidlog.db",
"mode": "RWC"
},
{
"id": "j1k2l3m4.n5o6p7",
"type": "ui_group",
"name": "系統狀態與資料庫內容",
"tab": "o2p1q0r9.s8t7u6",
"order": 1,
"disp": true,
"width": 10,
"collapse": false,
"className": ""
},
{
"id": "o2p1q0r9.s8t7u6",
"type": "ui_tab",
"name": "RFID UID 管理",
"icon": "dashboard",
"disabled": false,
"hidden": false
}
]
這份 Node-RED 流程設計用於接收來自 ESP32 的 RFID UID 數據,根據預設的操作模式(新增或比對)將其處理並存儲到 SQLite 資料庫中,同時提供使用者介面以切換模式並顯示結果。
1. 程式概述
這個 Node-RED 流程的核心功能是:
接收 MQTT 訊息: 從 MQTT 代理訂閱特定的主題 (alex9ufo/rfidUID) 以接收 ESP32 發送的 RFID UID。
數據整理: 對接收到的 UID 進行預處理,加入時間戳記並獲取當前操作模式。
模式判斷與分流: 根據預設的操作模式(「新增模式」或「比對模式」)將訊息導向不同的處理路徑。
資料庫操作:
結果處理與反饋: 根據資料庫操作結果,生成相應的訊息和狀態提示。
使用者介面控制: 提供一個開關(UI Switch)讓使用者手動切換操作模式,並顯示當前模式。
2. 節點詳細分析
2.1 輸入節點
2.2 數據處理節點
2.3 資料庫操作準備節點
準備自動新增 SQL (id: fa1619eb7b9480c2)
準備自動比對 SQL (id: 20a62781eb2627aa)
處理無效 MQTT 輸入 (id: e9544c7443d9798e)
2.4 資料庫執行節點
2.5 資料庫結果處理和輸出
2.6 使用者介面與模式控制
新增/比對模式 (id: 933e6c5206e27d71)
儲存模式 (id: 1d716d2303cde764)
目前模式 (id: 666a12327dafb890)
2.7 其他輔助節點
查詢所有資料 (id: a8ca45c5f2937486)
Debug 節點 (67e319dd036b422a, 22f574cc87dbf420, 060045734fceb2fc, 0bca09d23f357815)
Link Out 節點 (352becac094fd387, fb1dc5cc0dc41ba4, cfeb88c2fee440a4, de435e0837ac94a8)
2.8 配置節點
broker.mqttgo.io (id: 06d79adf7cfe46f5)
rfidDB (id: rfidDB)
操作面板 (id: q8r7s6t5.u4v3w2)
RFID UID 管理 (id: o2p1q0r9.s8t7u6)
3. 流程整體運作
ESP32 發送 UID: 您的 Arduino ESP32 程式模擬 RFID 掃描後,會將生成的 UID 發佈到 MQTT 代理的 alex9ufo/rfidUID 主題。
Node-RED 接收: Node-RED 流程中的 MQTT RFID UID Input 節點接收到這個 UID 訊息。
數據整理: 訊息隨後傳遞給「整理 MQTT 數據」函式節點,此節點會從流程上下文(預設為「比對模式」)獲取當前操作模式,並將 UID、日期和時間整理到訊息物件中。
模式判斷:「根據操作模式」開關節點會檢查 msg.mode,決定將訊息導向「新增模式」或「比對模式」的處理路徑。
SQL 準備與執行:
結果處理與反饋:「處理資料庫結果」函式節點接收 SQLite 節點的執行結果。
使用者介面交互:
使用者可以透過「新增/比對模式」UI 開關來手動切換流程的操作模式。
「儲存模式」節點會將選擇的模式儲存到流程上下文中。
「目前模式」UI 文字節點會顯示當前設定的操作模式。
沒有留言:
張貼留言