2025年6月24日 星期二

WOKWI模擬RFID感應卡片並發行至MQTT Broker , Node-Red訂閱WOKWI發行的主題並使用SQLite資料庫儲存或比對

WOKWI模擬RFID感應卡片並發行至MQTT Broker , Node-Red訂閱WOKWI發行的主題並使用SQLite資料庫儲存或比對



WOKWI程式





#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()

    • 一個幫助函式,用於確保 MQTT 用戶端已連接。如果連線斷開,它會嘗試重新連線,並在序列埠監控器中列印狀態訊息。它在這裡使用 delay(),因為這是一個阻塞操作,會等待連線。

  • 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 生成

  • String generateSpecificRangeUID()

    • 此函式生成一個隨機的 8 個字元的十六進位字串。

    • 它定義了 UID 的特定範圍:0x8EED71000x8EED71FF

    • random(start_val, end_val + 1):在此範圍內生成一個隨機的 unsigned long 整數。+1 是必要的,因為 random() 的上限是排他性的。

    • sprintf(hex_uid, "%08lX", random_val);:將 unsigned long 格式化為 8 個字元的大寫十六進位字串,如果需要則用前導零填充。

    • 將結果作為 Arduino String 物件返回。

7. setup() 函式

setup() 函式在程式開始時執行一次:

  • lcd.init(); lcd.backlight();:初始化 LCD 並打開其背光。

  • Serial.begin(115200);:初始化序列埠通訊以進行除錯輸出。

  • uidQueue = xQueueCreate(5, sizeof(String));:創建一個容量為 5 個 String 物件的 FreeRTOS 佇列。它包含錯誤處理,如果佇列創建失敗則停止系統。

  • 將操作說明列印到序列埠監控器,以便與模擬互動。

  • xTaskCreatePinnedToCore(...)

    • mqttTask:以 10000 位元組的堆疊大小和優先級 1 創建,並固定到 核心 0。這是網路操作和 MQTT 發生的地方。

    • rfidSimTask:以 4000 位元組的堆疊大小和優先級 1 創建,並固定到 核心 1。這處理 RFID 模擬和使用者輸入。

8. loop() 函式

  • void loop() { vTaskDelay(1); }loop() 函式有意地幾乎為空。在基於 FreeRTOS 的 ESP32 程式中,主要邏輯通常駐留在使用者定義的任務中。vTaskDelay(1) 是一個最小的延遲,用於防止關於空迴圈的編譯器警告,並確保 FreeRTOS 排程器可以有效管理其他任務。

9. 程式整體流程

  1. 初始化 (setup()):

    • 設定 LCD 和序列埠。

    • 創建用於任務間通訊的 FreeRTOS 佇列 (uidQueue)。

    • 創建兩個任務 (mqttTaskrfidSimTask) 並分別分配給核心 0 和核心 1。

  2. mqttTask (核心 0):

    • 連接到 Wi-Fi。

    • 連接到 MQTT 代理。

    • 持續等待 uidQueue 中出現 UID。

    • 收到 UID 後,將其發佈到 MQTT 並閃爍 LED。

  3. rfidSimTask (核心 1):

    • 等待序列埠監控器中的使用者輸入(ENTER 鍵按下)。

    • 檢測到後,生成一個隨機的 RFID UID。

    • 在 LCD 上顯示 UID。

    • 將生成的 UID 放入 uidQueue,供 mqttTask 處理。

  4. 通訊: 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 進行預處理,加入時間戳記並獲取當前操作模式。

  • 模式判斷與分流: 根據預設的操作模式(「新增模式」或「比對模式」)將訊息導向不同的處理路徑。

  • 資料庫操作:

    • 在「新增模式」下,將 UID、日期和時間插入 SQLite 資料庫。

    • 在「比對模式」下,查詢資料庫以檢查 UID 是否存在。

  • 結果處理與反饋: 根據資料庫操作結果,生成相應的訊息和狀態提示。

  • 使用者介面控制: 提供一個開關(UI Switch)讓使用者手動切換操作模式,並顯示當前模式。

2. 節點詳細分析

2.1 輸入節點

  • MQTT RFID UID Input (id: c42cb358a4ec6272)

    • 類型: mqtt in (MQTT 輸入)

    • 名稱: "MQTT RFID UID Input " (MQTT RFID UID 輸入)

    • 主題: alex9ufo/rfidUID (此主題與您 Arduino ESP32 程式中的 mqtt_topic 相符,確保了數據來源的一致性)

    • QoS: 2 (保證訊息只被傳遞一次)

    • 代理: broker.mqttgo.io (連接到公共 MQTT 代理,其配置在 06d79adf7cfe46f5 中定義)

    • 功能: 此節點是整個流程的起點,它監聽來自 ESP32 的 RFID UID 訊息。每當 ESP32 成功發佈一個 UID 到 alex9ufo/rfidUID 主題時,此節點就會接收到該訊息並將其傳遞到下游節點。

2.2 數據處理節點

  • 整理 MQTT 數據 (id: 87813834088d7985)

    • 類型: function (函式)

    • 名稱: "整理 MQTT 數據"

    • 功能: 此節點接收來自 MQTT 輸入節點的訊息 (msg),並對其進行處理:

      • 提取 MQTT 訊息的 payload 作為 uid

      • 獲取當前日期和時間,並格式化為 ISO 字串。

      • 從流程上下文 (flow context) 中讀取 operatingMode 變數。如果不存在,預設為「比對模式」。

      • uiddatetimemode 作為新的屬性添加到 msg 物件中,以便後續節點使用。

  • 根據操作模式 (id: 5cc2e0ca97eed330)

    • 類型: switch (開關)

    • 名稱: "根據操作模式"

    • 屬性: msg.mode

    • 規則:

      • 如果 msg.mode 等於「新增模式」,將訊息導向第一個輸出埠。

      • 如果 msg.mode 等於「比對模式」,將訊息導向第二個輸出埠。

    • 功能: 根據「整理 MQTT 數據」節點設定的 msg.mode 值,將訊息分流到不同的處理分支(新增、比對或無效)。

2.3 資料庫操作準備節點

  • 準備自動新增 SQL (id: fa1619eb7b9480c2)

    • 類型: function (函式)

    • 名稱: "準備自動新增 SQL"

    • 功能: 當模式為「新增模式」時,此節點將準備 SQLite 數據庫的插入語句:

      • 檢查 uid 是否為空。如果為空,則發出警告並將訊息導向第二個輸出埠(通常是錯誤處理或日誌記錄)。

      • 設置 msg.topic 為 SQL INSERT 語句 (INSERT INTO rfid_uids (uid, date, time) VALUES ($uid, $date, $time))。

      • 設置 msg.payload 為一個包含 uid, date, time 的陣列,這些值將用於填充 SQL 語句中的參數。

      • 將處理後的訊息導向第一個輸出埠,用於執行 SQL 語句。

  • 準備自動比對 SQL (id: 20a62781eb2627aa)

    • 類型: function (函式)

    • 名稱: "準備自動比對 SQL"

    • 功能: 當模式為「比對模式」時,此節點將準備 SQLite 數據庫的查詢語句:

      • 檢查 uid 是否為空。如果為空,則發出警告並將錯誤訊息導向第二個輸出埠。

      • 設置 msg.topic 為 SQL SELECT 語句 (SELECT * FROM rfid_uids WHERE uid = $uid)。

      • 設置 msg.payload 為一個包含 uid 的陣列。

      • 將當前的 uid 儲存到流程上下文 flow.get('currentUID'),以便在後續處理資料庫結果時使用。

      • 將處理後的訊息導向第一個輸出埠,用於執行 SQL 語句。

  • 處理無效 MQTT 輸入 (id: e9544c7443d9798e)

    • 類型: function (函式)

    • 名稱: "處理無效 MQTT 輸入"

    • 功能: 當「根據操作模式」節點判斷當前模式既不是「新增模式」也不是「比對模式」,或者 uid 為空時,此節點會生成一條相應的提示訊息。

      • 設置 msg.payload 為包含 led: 'grey' 和說明文字的物件,表示不處理當前 MQTT 訊息或接收到空 UID。

      • 將此訊息傳遞給 cfeb88c2fee440a4 (一個 link out 節點),可能用於在儀表板上顯示狀態。

2.4 資料庫執行節點

  • RFID UID 資料庫 (自動) (id: 4c350b937a47dc28)

    • 類型: sqlite (SQLite 資料庫)

    • 名稱: "RFID UID 資料庫 (自動)"

    • 資料庫: rfidDB (參考 rfidDB 配置節點,指定資料庫檔案為 rfidlog.db)

    • SQL 查詢: msg.topic (從上游函式節點獲取 SQL 語句)

    • SQL 參數: msg.payload (從上游函式節點獲取 SQL 參數)

    • 功能: 此節點執行由「準備自動新增 SQL」或「準備自動比對 SQL」節點傳入的 SQL 語句。執行結果會被傳遞給「處理資料庫結果」節點。

2.5 資料庫結果處理和輸出

  • 處理資料庫結果 (id: b8967e0218a84006)

    • 類型: function (函式)

    • 名稱: "處理資料庫結果"

    • 功能: 此節點處理從 RFID UID 資料庫 (自動) 節點返回的結果,並根據操作模式生成不同的輸出訊息:

      • 新增模式: 如果是自動新增操作,會顯示「UID 新增成功!」,並更新節點狀態。同時,它會觸發一個新的訊息,查詢資料庫中最新的 50 條記錄,以便更新儀表板上的表格。

      • 比對模式: 如果是自動比對操作,它會檢查查詢結果是否包含該 UID。

        • 如果 UID 存在 (msg.payload && msg.payload.length > 0),則輸出「通過」訊息 ({ led: 'green', text: '通過:UID 已存在' })。

        • 如果 UID 不存在,則輸出「錯誤」訊息 ({ led: 'red', text: '錯誤:UID 不存在' })。並更新節點狀態。

      • 手動操作(此流程中未直接顯示源頭,但程式碼支持): 根據 msg.dbActionType(例如 add_manual, update_manual, delete_manual, query_manual),處理手動資料庫操作的結果,並發送相應的成功或失敗訊息,並在成功時觸發表格更新。

      • 此節點有兩個輸出埠:第一個用於顯示操作結果訊息(例如,到儀表板上的文字節點),第二個則在某些情況下(如查詢所有資料)將結果直接傳遞給表格顯示節點。

2.6 使用者介面與模式控制

  • 新增/比對模式 (id: 933e6c5206e27d71)

    • 類型: ui_switch (使用者介面開關)

    • 名稱: "新增/比對模式"

    • 標籤: "模式切換"

    • 開啟值: "新增模式"

    • 關閉值: "比對模式"

    • 功能: 這是一個顯示在 Node-RED Dashboard 上的開關,允許使用者在「新增模式」和「比對模式」之間切換。當開關狀態改變時,它會發送一個訊息到下游節點。

  • 儲存模式 (id: 1d716d2303cde764)

    • 類型: change (變更)

    • 名稱: "儲存模式"

    • 功能: 此節點接收來自「新增/比對模式」開關的訊息,並將 msg.payload (即「新增模式」或「比對模式」字串) 儲存到 flow (流程) 上下文變數 operatingMode 中。這個變數可以在流程中的任何地方被訪問,例如被「整理 MQTT 數據」節點讀取。

  • 目前模式 (id: 666a12327dafb890)

    • 類型: ui_text (使用者介面文字)

    • 名稱: "目前模式"

    • 標籤: "目前:"

    • 格式: {{msg.payload}}

    • 功能: 此節點顯示從「儲存模式」節點傳遞過來的 msg.payload,即當前設置的操作模式,讓使用者在儀表板上直觀地看到當前的工作模式。

2.7 其他輔助節點

  • 查詢所有資料 (id: a8ca45c5f2937486)

    • 類型: sqlite (SQLite 資料庫)

    • 名稱: "查詢所有資料"

    • 資料庫: rfidDB

    • SQL 查詢: msg.topic (實際上在「處理資料庫結果」中會發送帶有查詢 SQL 的 msg.payload,但此處定義為 msg.topic 可能是筆誤或不同用途的設計,應是 sql: "msg.payload")

    • 功能: 此節點用於執行查詢所有或最近記錄的 SQL 語句,通常在資料庫有變更(如新增、刪除)後觸發,以更新儀表板上的表格顯示。

  • Debug 節點 (67e319dd036b422a, 22f574cc87dbf420, 060045734fceb2fc, 0bca09d23f357815)

    • 類型: debug (除錯)

    • 功能: 這些節點用於在 Node-RED 側邊欄的 Debug 視窗中顯示訊息,便於開發者追蹤訊息流和檢查數據內容,是除錯過程中非常重要的工具。

  • Link Out 節點 (352becac094fd387, fb1dc5cc0dc41ba4, cfeb88c2fee440a4, de435e0837ac94a8)

    • 類型: link out (連結輸出)

    • 功能: 這些節點用於將訊息從流程的一部分發送到流程的其他部分,或者發送到另一個流程。它們有助於使複雜的流程更整潔和模組化,避免過多的線路交叉。

2.8 配置節點

  • broker.mqttgo.io (id: 06d79adf7cfe46f5)

    • 類型: mqtt-broker (MQTT 代理)

    • 名稱: "broker.mqttgo.io"

    • 代理地址: broker.mqttgo.io

    • 埠: 1883

    • 功能: 這是 MQTT 輸入節點所連接的 MQTT 代理服務器配置。

  • rfidDB (id: rfidDB)

    • 類型: sqlitedb (SQLite 資料庫)

    • 資料庫檔案: rfidlog.db

    • 模式: RWC (讀、寫、創建)

    • 功能: 這是 SQLite 資料庫節點所使用的資料庫配置,它指定了 SQLite 資料庫檔案的名稱和存取模式。

  • 操作面板 (id: q8r7s6t5.u4v3w2)

    • 類型: ui_group (UI 群組)

    • 名稱: "操作面板"

    • 選項卡: RFID UID 管理 (o2p1q0r9.s8t7u6)

    • 功能: 這是 Node-RED Dashboard 中的一個面板群組,用於組織相關的使用者介面元素(如模式切換開關和模式顯示文字)。

  • RFID UID 管理 (id: o2p1q0r9.s8t7u6)

    • 類型: ui_tab (UI 選項卡)

    • 名稱: "RFID UID 管理"

    • 功能: 這是 Node-RED Dashboard 中的一個主要選項卡,所有相關的 UI 群組都將顯示在此選項卡下。

3. 流程整體運作

  1. ESP32 發送 UID: 您的 Arduino ESP32 程式模擬 RFID 掃描後,會將生成的 UID 發佈到 MQTT 代理的 alex9ufo/rfidUID 主題。

  2. Node-RED 接收: Node-RED 流程中的 MQTT RFID UID Input 節點接收到這個 UID 訊息。

  3. 數據整理: 訊息隨後傳遞給「整理 MQTT 數據」函式節點,此節點會從流程上下文(預設為「比對模式」)獲取當前操作模式,並將 UID、日期和時間整理到訊息物件中。

  4. 模式判斷:「根據操作模式」開關節點會檢查 msg.mode,決定將訊息導向「新增模式」或「比對模式」的處理路徑。

  5. SQL 準備與執行:

    • 如果是「新增模式」,「準備自動新增 SQL」節點會構建一個 INSERT SQL 語句,並將其傳遞給「RFID UID 資料庫 (自動)」SQLite 節點執行。

    • 如果是「比對模式」,「準備自動比對 SQL」節點會構建一個 SELECT SQL 語句,並將其傳遞給「RFID UID 資料庫 (自動)」SQLite 節點執行。

  6. 結果處理與反饋:「處理資料庫結果」函式節點接收 SQLite 節點的執行結果。

    • 在「新增模式」下,它會確認新增成功,並觸發一個查詢以更新儀表板上的 UID 列表。

    • 在「比對模式」下,它會根據查詢結果判斷 UID 是否已存在,並發送「通過」或「錯誤」訊息。

  7. 使用者介面交互:

    • 使用者可以透過「新增/比對模式」UI 開關來手動切換流程的操作模式。

    • 「儲存模式」節點會將選擇的模式儲存到流程上下文中。

    • 「目前模式」UI 文字節點會顯示當前設定的操作模式。

沒有留言:

張貼留言

ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite

 ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite  ESP32 VS Code 程式 ; PlatformIO Project Configuration File ; ;   Build op...