2026年3月9日 星期一

2026 作業2 RFID+Node-Red+Python+SQLite 練習

2026 作業2  RFID+Node-Red+Python+SQLite 練習


# --- 設定區 ---

MQTT_SERVER = "broker.mqtt-dashboard.com"

TOPIC_RFID_UID = "alex9ufo/rfid/UID"

TOPIC_LED_CONTROL = "alex9ufo/led/control"

TOPIC_LED_STATUS = "alex9ufo/led/status"






參考下面連結



#define SS_PIN    5
#define RST_PIN   22
#define LED_PIN   2
#define I2C_SDA   17
#define I2C_SCL   16





#include <SPI.h>
#include <MFRC522.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// --- 硬體腳位 (您的指定) ---
#define SS_PIN    5
#define RST_PIN   22
#define LED_PIN   2
#define I2C_SDA   17
#define I2C_SCL   16

// --- 設定區 ---
const char* ssid = "Wokwi-GUEST";
const char* password = "";

// MQTT 設定 (與您的 Python Tkinter 程式對接)
const char* mqtt_server = "broker.mqtt-dashboard.com";
const char* TOPIC_RFID_UID = "alex9ufo/rfid/UID";
const char* TOPIC_LED_CONTROL = "alex9ufo/led/control";
const char* TOPIC_LED_STATUS = "alex9ufo/led/status";

// 硬體物件
LiquidCrystal_I2C lcd(0x27, 16, 2);
MFRC522 mfrc522(SS_PIN, RST_PIN);
WiFiClient espClient;
PubSubClient mqttClient(espClient);

// --- FreeRTOS 隊列 ---
QueueHandle_t rfidQueue;
struct RfidMsg { char uid[20]; };

// 全域變數
bool isFlashing = false;

// --- MQTT 接收處理 (來自 Python 控制台) ---
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  String message = "";
  for (int i = 0; i < length; i++) message += (char)payload[i];
 
  Serial.printf("\n[MQTT CMD]: %s\n", message.c_str());
 
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("MQTT Command:");
  lcd.setCursor(0, 1);

  isFlashing = false;
  if (message == "on") {
    digitalWrite(LED_PIN, HIGH);
    lcd.print("LED: ON");
    mqttClient.publish(TOPIC_LED_STATUS, "ON");
  }
  else if (message == "off") {
    digitalWrite(LED_PIN, LOW);
    lcd.print("LED: OFF");
    mqttClient.publish(TOPIC_LED_STATUS, "OFF");
  }
  else if (message == "flash") {
    isFlashing = true;
    lcd.print("MODE: FLASHING");
    mqttClient.publish(TOPIC_LED_STATUS, "FLASHING");
  }
  else if (message == "timer") {
    digitalWrite(LED_PIN, HIGH);
    lcd.print("TIMER: 5 SEC");
    mqttClient.publish(TOPIC_LED_STATUS, "TIMER_START");
    vTaskDelay(5000 / portTICK_PERIOD_MS); // 在 Task 中使用 vTaskDelay 不會卡死整個系統
    digitalWrite(LED_PIN, LOW);
    mqttClient.publish(TOPIC_LED_STATUS, "OFF");
  }
}

// --- Core 0: 負責 WiFi 與 MQTT 通訊 ---
void mqttTask(void *pvParameters) {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { vTaskDelay(500 / portTICK_PERIOD_MS); }
 
  mqttClient.setServer(mqtt_server, 1883);
  mqttClient.setCallback(mqttCallback);

  RfidMsg rMsg;
  while (true) {
    // 維護 MQTT 連線
    if (!mqttClient.connected()) {
      Serial.print("Connecting to MQTT...");
      if (mqttClient.connect("ESP32_RFID_Gate_NoTG")) {
        Serial.println("Connected");
        mqttClient.subscribe(TOPIC_LED_CONTROL);
      } else {
        vTaskDelay(5000 / portTICK_PERIOD_MS);
      }
    }
    mqttClient.loop();

    // 接收來自 Core 1 的 RFID 訊息並發送到 MQTT
    if (xQueueReceive(rfidQueue, &rMsg, 0) == pdPASS) {
      mqttClient.publish(TOPIC_RFID_UID, rMsg.uid);
      Serial.printf("Sent UID to Python: %s\n", rMsg.uid);
    }

    // 處理閃爍邏輯
    if (isFlashing) {
      digitalWrite(LED_PIN, !digitalRead(LED_PIN));
      vTaskDelay(300 / portTICK_PERIOD_MS);
    }
   
    vTaskDelay(10 / portTICK_PERIOD_MS);
  }
}

// --- Core 1: 專門負責 RFID 掃描 (不處理網路) ---
void rfidTask(void *pvParameters) {
  SPI.begin();
  mfrc522.PCD_Init();
  RfidMsg rMsg;
  while (true) {
    if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {
      String uidStr = "";
      for (byte i = 0; i < mfrc522.uid.size; i++) {
        uidStr += (mfrc522.uid.uidByte[i] < 0x10 ? "0" : "");
        uidStr += String(mfrc522.uid.uidByte[i], HEX);
      }
      uidStr.toUpperCase();
     
      // 更新 LCD 顯示
      lcd.clear();
      lcd.setCursor(0, 0); lcd.print("RFID Detected!");
      lcd.setCursor(0, 1); lcd.print("ID: " + uidStr);
     
      // 將卡號打包送入隊列,交給 Core 0 發送
      uidStr.toCharArray(rMsg.uid, 20);
      xQueueSend(rfidQueue, &rMsg, portMAX_DELAY);

      mfrc522.PICC_HaltA();
      mfrc522.PCD_StopCrypto1();
    }
    vTaskDelay(200 / portTICK_PERIOD_MS);
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
 
  // 初始化 I2C LCD
  Wire.begin(I2C_SDA, I2C_SCL);
  lcd.init();
  lcd.backlight();
  lcd.print("MQTT Connecting...");

  // 建立隊列
  rfidQueue = xQueueCreate(10, sizeof(RfidMsg));

  if (rfidQueue != NULL) {
    // 建立雙核心任務
    xTaskCreatePinnedToCore(mqttTask, "MQTT_Task", 8192, NULL, 1, NULL, 0);
    xTaskCreatePinnedToCore(rfidTask, "RFID_Task", 4096, NULL, 1, NULL, 1);
  }
}

void loop() {
  // FreeRTOS 架構下 loop 不需執行內容
  vTaskDelay(portMAX_DELAY);
}











import tkinter as tk

from tkinter import messagebox, ttk, simpledialog

import paho.mqtt.client as mqtt

import sqlite3

import pandas as pd

import pyttsx3

import threading

from datetime import datetime


# --- 設定區 ---

MQTT_SERVER = "broker.mqtt-dashboard.com"

TOPIC_RFID_UID = "alex9ufo/rfid/UID"

TOPIC_LED_CONTROL = "alex9ufo/led/control"

TOPIC_LED_STATUS = "alex9ufo/led/status"


DB_LED = "202603LED.db"

DB_RFID = "202603RFID.db"


# --- 語音功能 ---

def speak(text):

    def _run_speak():

        try:

            engine = pyttsx3.init()

            engine.setProperty('rate', 150)

            voices = engine.getProperty('voices')

            for voice in voices:

                if "Chinese" in voice.name or "ZH-TW" in voice.id:

                    engine.setProperty('voice', voice.id)

                    break

            engine.say(text)

            engine.runAndWait()

        except:

            pass

    threading.Thread(target=_run_speak, daemon=True).start()


# --- 資料庫初始化 ---

def init_dbs():

    conn_led = sqlite3.connect(DB_LED)

    conn_led.execute('''CREATE TABLE IF NOT EXISTS LED_LOG 

                     (id INTEGER PRIMARY KEY AUTOINCREMENT, status TEXT, date TEXT, time TEXT)''')

    conn_led.close()

    

    conn_rfid = sqlite3.connect(DB_RFID)

    conn_rfid.execute('''CREATE TABLE IF NOT EXISTS RFID_LOG 

                      (id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, date TEXT, time TEXT)''')

    conn_rfid.close()


# --- MQTT 邏輯 ---

def on_connect(client, userdata, flags, rc, properties=None):

    if rc == 0:

        app.set_connection_status(True)

        client.subscribe([(TOPIC_RFID_UID, 0), (TOPIC_LED_STATUS, 0)])

    else:

        app.set_connection_status(False)


def on_message(client, userdata, msg):

    topic = msg.topic

    payload = msg.payload.decode()

    now = datetime.now()

    

    if topic == TOPIC_LED_STATUS:

        conn = sqlite3.connect(DB_LED)

        conn.execute("INSERT INTO LED_LOG (status, date, time) VALUES (?, ?, ?)",

                     (payload, now.strftime("%Y-%m-%d"), now.strftime("%H:%M:%S")))

        conn.commit()

        conn.close()

        speak(f"LED 狀態更新為 {payload}")

        root.after(0, lambda: app.update_table("LED"))

        

    elif topic == TOPIC_RFID_UID:

        conn = sqlite3.connect(DB_RFID)

        conn.execute("INSERT INTO RFID_LOG (uid, date, time) VALUES (?, ?, ?)",

                      (payload, now.strftime("%Y-%m-%d"), now.strftime("%H:%M:%S")))

        conn.commit()

        conn.close()

        speak(f"偵測到卡片,末四位 {payload[-4:]}")

        root.after(0, lambda: app.update_table("RFID"))


# --- GUI 介面 ---

class MQTTApp:

    def __init__(self, root):

        self.root = root

        self.root.title("2026 MQTT RFID 專業監控系統")

        self.root.geometry("900x700")


        # 狀態燈區

        status_frame = tk.Frame(root)

        status_frame.pack(side="top", fill="x", padx=10, pady=5)

        self.canvas = tk.Canvas(status_frame, width=20, height=20)

        self.canvas.pack(side="left")

        self.status_light = self.canvas.create_oval(2, 2, 18, 18, fill="red")

        self.status_text = tk.Label(status_frame, text="嘗試連線中...", fg="red")

        self.status_text.pack(side="left", padx=5)


        # 控制面板

        ctrl_frame = tk.LabelFrame(root, text="遠端指令控制")

        ctrl_frame.pack(pady=5, padx=10, fill="x")

        btns = [("LED ON", "#4CAF50", "on"), ("LED OFF", "#F44336", "off"), 

                ("閃爍", "#FFEB3B", "flash"), ("計時", "#2196F3", "timer")]

        for i, (txt, clr, cmd) in enumerate(btns):

            tk.Button(ctrl_frame, text=txt, bg=clr, width=12, 

                      command=lambda c=cmd: self.send_cmd(c)).grid(row=0, column=i, padx=20, pady=10)


        # 分頁系統

        notebook = ttk.Notebook(root)

        notebook.pack(pady=5, padx=10, expand=True, fill="both")


        self.led_frame = ttk.Frame(notebook)

        self.rfid_frame = ttk.Frame(notebook)

        notebook.add(self.led_frame, text=" LED 歷史紀錄 ")

        notebook.add(self.rfid_frame, text=" RFID 刷卡資料庫 ")


        self.setup_db_ui(self.led_frame, "LED")

        self.setup_db_ui(self.rfid_frame, "RFID")


    def setup_db_ui(self, frame, db_type):

        cols = ('ID', '內容資料', '日期', '時間')

        tree = ttk.Treeview(frame, columns=cols, show='headings')

        for col in cols: tree.heading(col, text=col)

        tree.column('ID', width=50, anchor='center')

        

        scroll = ttk.Scrollbar(frame, orient="vertical", command=tree.yview)

        tree.configure(yscrollcommand=scroll.set)

        tree.pack(side="left", fill="both", expand=True)

        scroll.pack(side="right", fill="y")

        

        if db_type == "LED": 

            self.led_tree = tree

            tree.tag_configure('on_tag', background='#E8F5E9')

            tree.tag_configure('off_tag', background='#FFEBEE')

        else: 

            self.rfid_tree = tree

            tree.tag_configure('stripe', background='#F2F2F2')


        # 右側管理按鈕

        btn_frame = tk.Frame(frame)

        btn_frame.pack(side="right", padx=10)

        tk.Button(btn_frame, text="搜尋 ID", width=12, command=lambda: self.query_data(db_type)).pack(pady=5)

        tk.Button(btn_frame, text="匯出 Excel", bg="#2196F3", fg="white", width=12, command=lambda: self.export_excel(db_type)).pack(pady=5)

        tk.Button(btn_frame, text="清空資料", bg="#FF9800", fg="white", width=12, command=lambda: self.clear_all(db_type)).pack(pady=5)


    def set_connection_status(self, connected):

        color = "#4CAF50" if connected else "#F44336"

        self.canvas.itemconfig(self.status_light, fill=color)

        self.status_text.config(text="連線正常" if connected else "連線中斷", fg=color)


    def send_cmd(self, cmd):

        client.publish(TOPIC_LED_CONTROL, cmd)

        speak(f"發送{cmd}")


    def update_table(self, db_type):

        db_file, tree, table = (DB_LED, self.led_tree, "LED_LOG") if db_type == "LED" else (DB_RFID, self.rfid_tree, "RFID_LOG")

        for row in tree.get_children(): tree.delete(row)

        conn = sqlite3.connect(db_file)

        rows = conn.execute(f"SELECT * FROM {table} ORDER BY id DESC LIMIT 50").fetchall()

        for i, row in enumerate(rows):

            tag = ''

            if db_type == "LED":

                if "on" in str(row[1]).lower(): tag = 'on_tag'

                elif "off" in str(row[1]).lower(): tag = 'off_tag'

            else:

                if i % 2 == 0: tag = 'stripe'

            tree.insert("", "end", values=row, tags=(tag,))

        conn.close()


    def export_excel(self, db_type):

        db_file, table = (DB_LED, "LED_LOG") if db_type == "LED" else (DB_RFID, "RFID_LOG")

        conn = sqlite3.connect(db_file)

        df = pd.read_sql_query(f"SELECT * FROM {table}", conn)

        conn.close()

        if not df.empty:

            fname = f"{table}_{datetime.now().strftime('%m%d_%H%M')}.xlsx"

            df.to_excel(fname, index=False)

            messagebox.showinfo("成功", f"匯出至 {fname}")

            speak("匯出完成")

        else:

            messagebox.showwarning("提示", "無資料可匯出")


    def query_data(self, db_type):

        q_id = simpledialog.askinteger("搜尋", "輸入搜尋 ID:")

        if q_id:

            db_file, table = (DB_LED, "LED_LOG") if db_type == "LED" else (DB_RFID, "RFID_LOG")

            conn = sqlite3.connect(db_file)

            res = conn.execute(f"SELECT * FROM {table} WHERE id=?", (q_id,)).fetchone()

            conn.close()

            if res: messagebox.showinfo("查詢結果", f"內容: {res[1]}\n日期: {res[2]}\n時間: {res[3]}")

            else: messagebox.showwarning("失敗", "找不到此 ID")


    def clear_all(self, db_type):

        if messagebox.askyesno("警告", "確定要清空所有紀錄?"):

            db_file, table = (DB_LED, "LED_LOG") if db_type == "LED" else (DB_RFID, "RFID_LOG")

            conn = sqlite3.connect(db_file)

            conn.execute(f"DELETE FROM {table}")

            conn.commit()

            conn.close()

            self.update_table(db_type)

            speak("資料已清空")


# --- 啟動 ---

if __name__ == "__main__":

    init_dbs()

    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)

    client.on_connect = on_connect

    client.on_message = on_message

    

    def mqtt_loop():

        while True:

            try:

                client.connect(MQTT_SERVER, 1883, 60)

                client.loop_forever()

            except:

                threading.Event().wait(5)


    threading.Thread(target=mqtt_loop, daemon=True).start()

    root = tk.Tk()

    app = MQTTApp(root)

    app.update_table("LED")

    app.update_table("RFID")

    root.mainloop()


這段 Python 程式碼是一個功能完整的 IoT 監控桌面應用程式。它結合了即時通訊、資料庫儲存、語音回饋以及報表匯出功能。

以下為程式碼的逐行邏輯說明:


1. 模組匯入與基礎設定

  • tkinter 系列: 負責建立視窗 (tk)、顯示彈窗 (messagebox)、分頁與表格 (ttk) 以及輸入框 (simpledialog)。

  • paho.mqtt: 處理與 MQTT 伺服器的連線,讓程式能與遠端硬體(如 ESP32)溝通。

  • sqlite3 & pandas: sqlite3 負責本機資料庫儲存;pandas 用於處理數據結構並匯出成 Excel。

  • pyttsx3 & threading: pyttsx3 提供語音合成功能;threading 確保語音和連線在背景執行,不會讓介面當掉。


2. 語音功能 (speak)

  • 這段函數會啟動一個背景線程 (Thread)。

  • 它會尋找系統內建的「中文」語音包,並將傳入的文字轉為語音讀出來。這樣做的好處是當程式在說話時,視窗依然可以點擊,不會卡住。


3. 資料庫初始化 (init_dbs)

  • 程式啟動時會檢查是否存在 202603LED.db (儲存燈光狀態) 與 202603RFID.db (儲存刷卡紀錄)。

  • 如果檔案不存在,它會自動建立資料表,欄位包括:id (自動編號)、status/uid (內容)、date (日期)、time (時間)。


4. MQTT 通訊邏輯 (on_connect, on_message)

  • on_connect: 當成功連線到伺服器時,自動訂閱 RFID 和 LED 的主題。

  • on_message: 這是程式的核心。當接收到新資料時:

    1. 自動抓取目前的日期與時間。

    2. 將資料寫入對應的 SQLite 資料庫。

    3. 觸發語音:讀出狀態或卡號末四位。

    4. 即時更新介面:使用 root.after(0, ...) 通知主視窗重新讀取資料庫並顯示在表格上。


5. GUI 介面類別 (MQTTApp)

這是程式的視覺框架:

  • 狀態燈: 顯示一個圓點,紅色代表斷線,綠色代表連線正常。

  • 控制面板: 建立四個彩色按鈕 (ON, OFF, 閃爍, 計時)。點擊按鈕會透過 MQTT 發送指令,並語音告知「發送指令」。

  • 分頁系統 (Notebook): 區分「LED 紀錄」與「RFID 資料庫」兩個頁面,讓介面保持整潔。


6. 資料管理功能

  • update_table: 從資料庫抓取最新的 50 筆資料顯示在表格中。

    • 視覺美化: LED 狀態為 ON 時背景變淡綠色,OFF 為淡紅色;RFID 紀錄則使用灰白相間的斑馬線。

  • export_excel: 利用 pandas 將資料庫轉換成 Excel 檔 (.xlsx)。這是最實用的報表功能。

  • query_data: 彈出對話框讓使用者輸入 ID,快速從成千上萬筆資料中找到特定的那一筆。

  • clear_all: 清空資料庫。為了安全,會先跳出確認視窗。


7. 程式啟動 (__main__)

  • 多線程執行: 程式分為兩個主要部分同時跑:

    1. MQTT Loop: 持續監控網路訊息,若斷線則每 5 秒嘗試重新連線。

    2. Tkinter Mainloop: 維持視窗顯示並處理使用者的點擊操作。


總結

這段程式碼將「硬體資料」轉化為「可管理、可查詢、可報表化」的系統。它不僅是一個監控工具,更是一個迷你的考勤與裝置管理系統


Node-Red有3個分頁 Tab 

資料庫位置 需修改成您的路徑    原來路徑path=D:\2026 RFID\2026-ex3


D:\2026 RFID\2026-ex3\202603LED.db  LED的Sqlite資料庫

CREATE TABLE "LED_LOG" (

"id" INTEGER,

"status" TEXT,

"date" TEXT,

"time" TEXT,

PRIMARY KEY("id")

);


D:\2026 RFID\2026-ex3\202603RFID.db  RFID的Sqlite資料庫

CREATE TABLE "RFID_LOG" (

"id" INTEGER,

"uid" TEXT,

"date" TEXT,

"time" TEXT,

PRIMARY KEY("id")

);

https://sqlitebrowser.org/dl/   軟體下載



Node-RED 安裝步驟暨執行node-red程式  參考下方連結


https://alex9ufoexploer.blogspot.com/2025/07/node-red-node-red.html

https://sites.google.com/site/wenyunotify/12-node-red/01-%E5%85%A5%E9%96%80%E7%AF%87

https://ithelp.ithome.com.tw/articles/10282461



A)安裝步驟詳解:

  1. 安裝 Node.js:
    • 前往 Node.js 官網下載並安裝 LTS(長期支援)版本。
  2. 安裝 Node-RED:
    • 打開終端機 (Terminal) 或命令提示字元 (cmd)。
    • 輸入指令:npm install -g --unsafe-perm node-red (Windows/Mac/Linux 均適用)。
  3. 啟動 Node-RED:
    • 安裝完成後,輸入:node-red
  4. 開啟編輯器:
    • 在瀏覽器輸入 http://localhost:1880 即可開始使用。
B)Node-RED 節點安裝方法
1. 編輯器直接安裝(推薦)
  • 打開 Node-RED 介面 (http://localhost:1880)。
  • 點擊右上角「選單」(☰) -> 「管理節點 (Manage palette)」
  • 切換到 「安裝 (Install)」 頁籤。
  • 在搜尋框輸入想安裝的節點名稱(例如:node-red-dashboard)。
  • 找到後點擊 「install」







[{"id":"d927438ef7105fcb","type":"ui_button","z":"3897cf2f308c39ee","name":"","group":"a7f1622545f7e3c5","order":1,"width":0,"height":0,"passthru":false,"label":"至分頁1Tab  (LED  SQLITE)","tooltip":"","color":"","bgcolor":"blue","className":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":180,"y":60,"wires":[["fd1d07c6d633f24f"]]},{"id":"fd1d07c6d633f24f","type":"function","z":"3897cf2f308c39ee","name":"function  ","func":"msg.payload= {\"tab\": \"RFIDSubpage1\"}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":60,"wires":[["bd19f4282b1404ac"]]},{"id":"bd19f4282b1404ac","type":"ui_ui_control","z":"3897cf2f308c39ee","name":"","events":"all","x":560,"y":80,"wires":[[]]},{"id":"f9839196994e4e12","type":"ui_button","z":"3897cf2f308c39ee","name":"","group":"a7f1622545f7e3c5","order":2,"width":0,"height":0,"passthru":false,"label":"至分頁2Tab  (RFID SQLITE)","tooltip":"","color":"","bgcolor":"purple","className":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":180,"y":120,"wires":[["5f2de4fb03923e90"]]},{"id":"5f2de4fb03923e90","type":"function","z":"3897cf2f308c39ee","name":"function  ","func":"msg.payload= {\"tab\": \"RFIDSubpage2\"}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":120,"wires":[["bd19f4282b1404ac"]]},{"id":"e6a04ebb7a023bf4","type":"ui_button","z":"3897cf2f308c39ee","name":"","group":"a7f1622545f7e3c5","order":4,"width":3,"height":1,"passthru":false,"label":"ON","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"on","payloadType":"str","topic":"topic","topicType":"msg","x":110,"y":200,"wires":[["fbd45b6c63ee72c2","32737c9374bb2e01","38f9ca19329b8265"]]},{"id":"ec3ef8fbc2d10ffb","type":"ui_button","z":"3897cf2f308c39ee","name":"","group":"a7f1622545f7e3c5","order":5,"width":3,"height":1,"passthru":false,"label":"OFF","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"off","payloadType":"str","topic":"topic","topicType":"msg","x":110,"y":240,"wires":[["fbd45b6c63ee72c2","32737c9374bb2e01","38f9ca19329b8265"]]},{"id":"470bd8eb959d0c9c","type":"ui_button","z":"3897cf2f308c39ee","name":"","group":"a7f1622545f7e3c5","order":6,"width":3,"height":1,"passthru":false,"label":"TIMER","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"timer","payloadType":"str","topic":"topic","topicType":"msg","x":120,"y":280,"wires":[["fbd45b6c63ee72c2","32737c9374bb2e01","38f9ca19329b8265"]]},{"id":"0d2a7c475996f588","type":"ui_button","z":"3897cf2f308c39ee","name":"","group":"a7f1622545f7e3c5","order":7,"width":3,"height":1,"passthru":false,"label":"FLASH","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"flash","payloadType":"str","topic":"topic","topicType":"msg","x":120,"y":320,"wires":[["fbd45b6c63ee72c2","32737c9374bb2e01","38f9ca19329b8265"]]},{"id":"fbd45b6c63ee72c2","type":"ui_audio","z":"3897cf2f308c39ee","name":"","group":"a7f1622545f7e3c5","voice":"Microsoft Hanhan - Chinese (Traditional, Taiwan)","always":true,"x":285,"y":200,"wires":[],"l":false},{"id":"38f9ca19329b8265","type":"debug","z":"3897cf2f308c39ee","name":"debug ","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":330,"y":240,"wires":[]},{"id":"32737c9374bb2e01","type":"mqtt out","z":"3897cf2f308c39ee","name":"LED Control","topic":"alex9ufo/led/control","qos":"1","retain":"true","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"b9efc827e98bf7f9","x":350,"y":280,"wires":[]},{"id":"371fba0e98260ca7","type":"mqtt in","z":"3897cf2f308c39ee","name":"RFID in","topic":"alex9ufo/rfid/UID","qos":"1","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":110,"y":440,"wires":[["b8f297adcdfc7fad","a7bdb84c9fe6df8c"]]},{"id":"6ac927abd4f5995a","type":"comment","z":"3897cf2f308c39ee","name":" MQTT 設定","info":"// --- MQTT 設定 ---\nconst int mqtt_port = 1883;\nconst char* mqtt_server = \"broker.mqtt-dashboard.com\";\nconst char* TOPIC_RFID_UID = \"alex9ufo/rfid/UID\";\nconst char* TOPIC_LED_CONTROL = \"alex9ufo/led/control\";\nconst char* TOPIC_LED_STATUS = \"alex9ufo/led/status\";","x":410,"y":200,"wires":[]},{"id":"b8f297adcdfc7fad","type":"debug","z":"3897cf2f308c39ee","name":"debug  ","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":250,"y":400,"wires":[]},{"id":"a7bdb84c9fe6df8c","type":"ui_text","z":"3897cf2f308c39ee","group":"a7f1622545f7e3c5","order":9,"width":0,"height":0,"name":"","label":"RFID UID卡號","format":"{{msg.payload}}","layout":"row-left","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":300,"y":440,"wires":[]},{"id":"a7f1622545f7e3c5","type":"ui_group","name":"2026-03-RFID","tab":"f514574c3ff83395","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"b9efc827e98bf7f9","type":"mqtt-broker","name":"broker.mqtt-dashboard.com","broker":"broker.mqtt-dashboard.com","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"f514574c3ff83395","type":"ui_tab","name":"RFIDHome","icon":"dashboard","disabled":false,"hidden":false}]

Gemini 說了

這份 Node-RED 程式是一個典型的 IoT 監控儀表板,結合了 MQTT 通訊RFID 讀取LED 遠端控制 以及 Dashboard 多分頁切換 功能。

根據您提供的截圖與 JSON 代碼,我將程式邏輯分為四大區塊進行說明:

1. 頁面切換控制 (Multi-Page Navigation)

這部分負責在「主頁面」與「副頁面」之間切換。

  • 藍色按鈕 (分頁1):點擊後透過 Function 節點發送 {"tab": "RFIDSubpage1"}

  • 紫色按鈕 (分頁2):點擊後發送 {"tab": "RFIDSubpage2"}

  • ui_control 節點:接收上述指令,並將瀏覽器畫面導向對應的分頁。

    • 注意:您的程式碼中使用了 ui_ui_control,這是 Dashboard 1.0 的節點,請確保您的 Tab 名稱與 Payload 內容完全一致(包含大小寫)。


2. LED 遠端控制區 (LED Command)

這是畫面中綠色按鈕組的功能:

  • 按鈕 (ON, OFF, TIMER, FLASH)

    • 點擊時,會發送對應的字串(如 "on""off")。

    • MQTT Out:將指令發送到 alex9ufo/led/control 主題,供遠端硬體(如 ESP32)接收。

    • Audio Out:當按鈕按下時,系統會用中文語音(Microsoft Hanhan)唸出該動作,提供語音回饋。

    • Debug:在側邊欄顯示目前送出的指令,方便開發者除錯。


3. RFID 數據顯示 (RFID Monitoring)

這部分處理從硬體端傳回的資料:

  • MQTT In:持續監聽 alex9ufo/rfid/UID 主題。當有人刷卡時,硬體會將卡號透過網路上傳。

  • UI Text:將接收到的卡號實時顯示在介面上的「RFID UID卡號」欄位。如截圖所示,目前顯示為 0A1B2C3D


4. 程式配置細節 (Configuration)

  • MQTT Broker:連線至公用伺服器 broker.mqtt-dashboard.com,這是一個免費的測試伺服器。

  • UI Group (2026-03-RFID):將所有控制項整合成一個寬度為 6 的區塊,並放置在名為 RFIDHome 的分頁中。





[{"id":"ba494f3383512153","type":"ui_button","z":"0f1188ad13a17d4c","name":"","group":"d5b16dc328bf6208","order":1,"width":0,"height":0,"passthru":false,"label":"回 主Tab","tooltip":"","color":"","bgcolor":"orange","className":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":160,"y":40,"wires":[["62343d105edbbe8c"]]},{"id":"62343d105edbbe8c","type":"function","z":"0f1188ad13a17d4c","name":"function  ","func":"msg.payload= {\"tab\": \"RFIDHome\"}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":40,"wires":[["fb99e9146e02a787"]]},{"id":"fb99e9146e02a787","type":"ui_ui_control","z":"0f1188ad13a17d4c","name":"","events":"all","x":500,"y":40,"wires":[[]]},{"id":"19e7e6a2695599f7","type":"sqlite","z":"0f1188ad13a17d4c","mydb":"f5c97c74cc496505","sqlquery":"msg.topic","sql":"","name":"LED_STATUS","x":620,"y":300,"wires":[["9834cf11a5eda166"]]},{"id":"0e32f93897bd16f9","type":"function","z":"0f1188ad13a17d4c","name":"CREATE DATABASE","func":"//CREATE TABLE \"LED_LOG\"(\n//   \"id\"\tINTEGER,\n//   \"status\"\tTEXT,\n//   \"date\"\tTEXT,\n//   \"time\"\tTEXT,\n//   PRIMARY KEY(\"id\" AUTOINCREMENT)\n//);\n\nmsg.topic = \"CREATE TABLE LED_LOG (id INTEGER,status TEXT,date TEXT ,time TEXT,PRIMARY KEY (id))\";\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":300,"wires":[["19e7e6a2695599f7"]]},{"id":"54c13d7d4a52925f","type":"ui_button","z":"0f1188ad13a17d4c","name":"","group":"d5b16dc328bf6208","order":4,"width":2,"height":1,"passthru":false,"label":"建立資料庫","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"建立資料庫","payloadType":"str","topic":"topic","topicType":"msg","x":170,"y":300,"wires":[["0e32f93897bd16f9","81927735b7d6496c"]]},{"id":"81927735b7d6496c","type":"ui_audio","z":"0f1188ad13a17d4c","name":"","group":"d5b16dc328bf6208","voice":"Microsoft Hanhan - Chinese (Traditional, Taiwan)","always":true,"x":325,"y":260,"wires":[],"l":false},{"id":"e97fe0fe5c84e971","type":"function","z":"0f1188ad13a17d4c","name":"INSERT","func":"var Today = new Date();\nvar yyyy = Today.getFullYear(); //年\nvar MM = Today.getMonth()+1;    //月\nvar dd = Today.getDate();       //日\nvar h = Today.getHours();       //時\nvar m = Today.getMinutes();     //分\nvar s = Today.getSeconds();     //秒\nif(MM<10)\n{\n   MM = '0'+MM;\n}\n\nif(dd<10)\n{\n   dd = '0'+dd;\n}\n\nif(h<10)\n{\n   h = '0'+h;\n}\n\nif(m<10)\n{\n  m = '0' + m;\n}\n\nif(s<10)\n{\n  s = '0' + s;\n}\nvar var_date = yyyy+'/'+MM+'/'+dd;\nvar var_time = h+':'+m+':'+s;\n\nvar myLED = msg.payload;\n\n\nmsg.topic = \"INSERT INTO LED_LOG ( status , date , time ) VALUES ($myLED,  $var_date ,  $var_time ) \" ;\nmsg.payload = [myLED, var_date , var_time ]\nreturn msg;\n\n\n\n//CREATE TABLE \"LED_LOG\"(\n//   \"id\"\tINTEGER,\n//   \"status\"\tTEXT,\n//   \"date\"\tTEXT,\n//   \"time\"\tTEXT,\n//   PRIMARY KEY(\"id\" AUTOINCREMENT)\n//);\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":180,"wires":[["c37d9d5539f9f2e0"]]},{"id":"c37d9d5539f9f2e0","type":"sqlite","z":"0f1188ad13a17d4c","mydb":"f5c97c74cc496505","sqlquery":"msg.topic","sql":"","name":"LED_STATUS","x":520,"y":180,"wires":[["96b7c87459e3ad68","7801ebc0aed3c9ee"]]},{"id":"9834cf11a5eda166","type":"debug","z":"0f1188ad13a17d4c","name":"debug ","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":790,"y":300,"wires":[]},{"id":"96b7c87459e3ad68","type":"debug","z":"0f1188ad13a17d4c","name":"debug ","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":790,"y":180,"wires":[]},{"id":"96885e5113714e9d","type":"ui_button","z":"0f1188ad13a17d4c","name":"","group":"d5b16dc328bf6208","order":6,"width":6,"height":2,"passthru":false,"label":"檢視資料庫資料","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"檢視資料","payloadType":"str","topic":"topic","topicType":"msg","x":180,"y":380,"wires":[["7801ebc0aed3c9ee","7e4dd8e401263ad0"]]},{"id":"7801ebc0aed3c9ee","type":"function","z":"0f1188ad13a17d4c","name":"檢視資料","func":"//CREATE TABLE \"LED_LOG\"(\n//   \"id\"\tINTEGER,\n//   \"status\"\tTEXT,\n//   \"date\"\tTEXT,\n//   \"time\"\tTEXT,\n//   PRIMARY KEY(\"id\" AUTOINCREMENT)\n//);\n\n\n//SELECT * FROM LED_LOG ORDER BY  id DESC LIMIT 50;\n\nmsg.topic = \"SELECT * FROM LED_LOG ORDER BY id DESC LIMIT 50\";\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":460,"y":380,"wires":[["35dfd5921f4b2122"]]},{"id":"8b59b14b06441637","type":"ui_table","z":"0f1188ad13a17d4c","group":"d5b16dc328bf6208","name":"","order":5,"width":8,"height":10,"columns":[],"outputs":0,"cts":false,"x":790,"y":380,"wires":[]},{"id":"35dfd5921f4b2122","type":"sqlite","z":"0f1188ad13a17d4c","mydb":"f5c97c74cc496505","sqlquery":"msg.topic","sql":"","name":"LED_STATUS","x":640,"y":380,"wires":[["8b59b14b06441637"]]},{"id":"10130f36de31eff1","type":"ui_button","z":"0f1188ad13a17d4c","name":"","group":"d5b16dc328bf6208","order":2,"width":2,"height":1,"passthru":false,"label":"刪除資料庫 ","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"刪除資料庫 ","payloadType":"str","topic":"topic","topicType":"msg","x":170,"y":620,"wires":[["bfc1239c57b037a4","c74f9d7128142d13"]]},{"id":"94e05d0ec6d91a61","type":"function","z":"0f1188ad13a17d4c","name":"DROP DATABASE","func":"//CREATE TABLE \"LED_LOG\"(\n//   \"id\"\tINTEGER,\n//   \"status\"\tTEXT,\n//   \"date\"\tTEXT,\n//   \"time\"\tTEXT,\n//   PRIMARY KEY(\"id\" AUTOINCREMENT)\n//);\n\nmsg.topic = \"DROP TABLE LED_LOG\";\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":740,"y":620,"wires":[["667f54479f69b8d3"]]},{"id":"667f54479f69b8d3","type":"sqlite","z":"0f1188ad13a17d4c","mydb":"f5c97c74cc496505","sqlquery":"msg.topic","sql":"","name":"LED_STATUS","x":940,"y":580,"wires":[["b96061ad75d52090"]]},{"id":"57e7dea749bb9cd5","type":"ui_button","z":"0f1188ad13a17d4c","name":"","group":"d5b16dc328bf6208","order":3,"width":2,"height":1,"passthru":false,"label":"刪除所有資料 ","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"刪除所有資料 ","payloadType":"str","topic":"topic","topicType":"msg","x":180,"y":520,"wires":[["c74f9d7128142d13","78c75ddb507cd1a2"]]},{"id":"076b9f6f23644794","type":"function","z":"0f1188ad13a17d4c","name":"DELETE ALL DATA","func":"//CREATE TABLE \"LED_LOG\"(\n//   \"id\"\tINTEGER,\n//   \"status\"\tTEXT,\n//   \"date\"\tTEXT,\n//   \"time\"\tTEXT,\n//   PRIMARY KEY(\"id\" AUTOINCREMENT)\n//);\n\nmsg.topic = \"DELETE from LED_LOG\";\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":750,"y":520,"wires":[["667f54479f69b8d3"]]},{"id":"bfc1239c57b037a4","type":"ui_toast","z":"0f1188ad13a17d4c","position":"prompt","displayTime":"3","highlight":"","sendall":true,"outputs":1,"ok":"OK","cancel":"Cancel","raw":true,"className":"","topic":"","name":"","x":370,"y":620,"wires":[["ba297107fa7c3756"]]},{"id":"ba297107fa7c3756","type":"function","z":"0f1188ad13a17d4c","name":"OK or Cancel","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,"noerr":0,"initialize":"","finalize":"","libs":[],"x":540,"y":620,"wires":[["94e05d0ec6d91a61"],[]]},{"id":"c74f9d7128142d13","type":"ui_audio","z":"0f1188ad13a17d4c","name":"","group":"d5b16dc328bf6208","voice":"Microsoft Hanhan - Chinese (Traditional, Taiwan)","always":true,"x":305,"y":580,"wires":[],"l":false},{"id":"78c75ddb507cd1a2","type":"ui_toast","z":"0f1188ad13a17d4c","position":"prompt","displayTime":"3","highlight":"","sendall":true,"outputs":1,"ok":"OK","cancel":"Cancel","raw":true,"className":"","topic":"","name":"","x":370,"y":520,"wires":[["af80c6b6ecae5773"]]},{"id":"af80c6b6ecae5773","type":"function","z":"0f1188ad13a17d4c","name":"OK or Cancel","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,"noerr":0,"initialize":"","finalize":"","libs":[],"x":540,"y":520,"wires":[["076b9f6f23644794"],[]]},{"id":"7e4dd8e401263ad0","type":"ui_audio","z":"0f1188ad13a17d4c","name":"","group":"d5b16dc328bf6208","voice":"Microsoft Hanhan - Chinese (Traditional, Taiwan)","always":true,"x":325,"y":340,"wires":[],"l":false},{"id":"b96061ad75d52090","type":"link out","z":"0f1188ad13a17d4c","name":"link out 79","mode":"link","links":["0afd9e681b69d254"],"x":995,"y":460,"wires":[]},{"id":"0afd9e681b69d254","type":"link in","z":"0f1188ad13a17d4c","name":"link in 73","links":["b96061ad75d52090"],"x":315,"y":420,"wires":[["7801ebc0aed3c9ee"]]},{"id":"279859c1419f28c6","type":"mqtt in","z":"0f1188ad13a17d4c","name":"LED Status","topic":"alex9ufo/led/status","qos":"1","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":170,"y":180,"wires":[["e97fe0fe5c84e971"]]},{"id":"ee74fadc9d2df075","type":"comment","z":"0f1188ad13a17d4c","name":"202603LED.db","info":"DB_LED = \"202603LED.db\"\nDB_RFID = \"202603RFID.db\"\n\n# --- 資料庫初始化 ---\ndef init_dbs():\n    conn_led = sqlite3.connect(DB_LED)\n    conn_led.execute('''CREATE TABLE IF NOT EXISTS LED_LOG \n                     (id INTEGER PRIMARY KEY AUTOINCREMENT, status TEXT, date TEXT, time TEXT)''')\n    conn_led.close()\n    \n    conn_rfid = sqlite3.connect(DB_RFID)\n    conn_rfid.execute('''CREATE TABLE IF NOT EXISTS RFID_LOG \n                      (id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT, date TEXT, time TEXT)''')\n    conn_rfid.close()\n","x":180,"y":100,"wires":[]},{"id":"d5b16dc328bf6208","type":"ui_group","name":"2026-03-RFID","tab":"b7a57e5038dab5a2","order":1,"disp":true,"width":14,"collapse":false,"className":""},{"id":"f5c97c74cc496505","type":"sqlitedb","db":"D:\\2026 RFID\\2026-ex3\\202603LED.db","mode":"RWC"},{"id":"b9efc827e98bf7f9","type":"mqtt-broker","name":"broker.mqtt-dashboard.com","broker":"broker.mqtt-dashboard.com","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"b7a57e5038dab5a2","type":"ui_tab","name":"RFIDSubpage1","icon":"dashboard","disabled":false,"hidden":false}]

這段程式碼展示了 Node-RED Dashboard 1.0 中,「副頁面 (RFIDSubpage1)」如何處理 SQLite 資料庫 的增刪查改,以及如何切回主頁面

以下是針對您提供的 JSON 與流程圖進行的逐行功能說明:


1. 頁面切換:回主頁面

這部分邏輯位於流程圖的最上方(nd4.png)。

  • ui_button (回主Tab): 橘色按鈕,當點擊時觸發流程。

  • function (切換指令): 程式碼為 msg.payload= {"tab": "RFIDHome"}。這會告訴下一個節點要跳轉到名為 RFIDHome 的分頁。

  • ui_ui_control: 執行導航動作,將用戶的瀏覽器畫面從目前的 RFIDSubpage1 切換回 RFIDHome


2. 資料自動寫入 (INSERT)

這部分監控硬體狀態並存檔。

  • mqtt in (LED Status): 監聽 MQTT 主題 alex9ufo/led/status。當硬體(如 ESP32)回報目前的 LED 狀態時,觸發此流程。

  • function (INSERT):

    • 自動抓取目前的系統時間(年/月/日 與 時:分:秒)。

    • 將時間與接收到的 msg.payload(LED 狀態)組合。

    • 產生 SQL 指令:INSERT INTO LED_LOG (status, date, time) VALUES (...)

  • sqlite (LED_STATUS): 將上述指令寫入位於 D:\2026 RFID\2026-ex3\202603LED.db 的資料庫檔案中。


3. 資料庫管理與顯示

對應介面上的綠色按鈕群(nd3.png)。

  • 建立資料庫 (CREATE DATABASE):

    • 點擊「建立資料庫」按鈕。

    • 發送 SQL 指令 CREATE TABLE LED_LOG (...)。如果表單不存在,則會建立一個包含 id, status, date, time 的資料表。

  • 檢視資料 (SELECT):

    • 點擊「檢視資料庫資料」大按鈕。

    • 執行 SELECT * FROM LED_LOG ORDER BY id DESC LIMIT 50

    • ui_table: 將查詢到的最後 50 筆紀錄顯示在網頁的表格中(如 nd3.png 右側所示)。


4. 刪除邏輯與安全確認 (DELETE/DROP)

為了防止誤刪,這裡加入了彈窗確認。

  • 刪除所有資料/資料庫: 點擊按鈕後,先觸發 ui_toast (prompt)

  • ui_toast: 在網頁彈出一個對話框,詢問用戶是否確定(OK 或 Cancel)。

  • function (OK or Cancel): 判斷用戶點擊了什麼。如果用戶按「OK」,才將流程傳遞下去;按「Cancel」則終止動作。

  • sqlite (DELETE/DROP):

    • DELETE FROM LED_LOG: 清空表內所有資料,但保留表結構。

    • DROP TABLE LED_LOG: 直接將整個資料表刪除。


5. 語音回饋 (Audio Out)

  • ui_audio: 在多個動作(建立、檢視、刪除)後方都連接了語音節點,使用 Microsoft Hanhan 聲音。當動作執行成功時,電腦/手機會發出語音提醒用戶。










[{"id":"ff743a144e801c11","type":"ui_button","z":"8716e8bacd81f09b","name":"","group":"9ddc322f04225a3c","order":1,"width":0,"height":0,"passthru":false,"label":"回 主Tab","tooltip":"","color":"","bgcolor":"orange","className":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":220,"y":120,"wires":[["b4473b2920b6ecef"]]},{"id":"b4473b2920b6ecef","type":"function","z":"8716e8bacd81f09b","name":"function  ","func":"msg.payload= {\"tab\": \"RFIDHome\"}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":120,"wires":[["bc114ae12b97b49f"]]},{"id":"bc114ae12b97b49f","type":"ui_ui_control","z":"8716e8bacd81f09b","name":"","events":"all","x":540,"y":120,"wires":[[]]},{"id":"df6df7550054f3af","type":"comment","z":"8716e8bacd81f09b","name":"TABLE \"RFID_LOG\"","info":"CREATE TABLE \"RFID_LOG\" (\n\t\"id\"\tINTEGER,\n\t\"uid\"\tTEXT,\n\t\"date\"\tTEXT,\n\t\"time\"\tTEXT,\n\tPRIMARY KEY(\"id\" AUTOINCREMENT)\n);","x":250,"y":60,"wires":[]},{"id":"0748435db05607a9","type":"sqlite","z":"8716e8bacd81f09b","mydb":"374401a176cf2d52","sqlquery":"msg.topic","sql":"","name":"RFID database","x":680,"y":300,"wires":[["a90ed08d57ae3c13"]]},{"id":"293a00c16bf04336","type":"function","z":"8716e8bacd81f09b","name":"CREATE DATABASE","func":"//CREATE TABLE \"RFID_LOG\" (\n//\t\"id\"\tINTEGER,\n//\t\"uid\"\tTEXT,\n//\t\"date\"\tTEXT,\n//\t\"time\"\tTEXT,\n//\tPRIMARY KEY(\"id\" AUTOINCREMENT)\n//);\nmsg.topic = \"CREATE TABLE RFID_LOG (id INTEGER,uid TEXT,date TEXT ,time TEXT,PRIMARY KEY (id))\";\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":300,"wires":[["0748435db05607a9"]]},{"id":"02743927399324ec","type":"ui_button","z":"8716e8bacd81f09b","name":"","group":"9ddc322f04225a3c","order":4,"width":2,"height":1,"passthru":false,"label":"建立資料庫","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"建立資料庫","payloadType":"str","topic":"topic","topicType":"msg","x":230,"y":300,"wires":[["293a00c16bf04336","e3297d3b76670741"]]},{"id":"e3297d3b76670741","type":"ui_audio","z":"8716e8bacd81f09b","name":"","group":"9ddc322f04225a3c","voice":"Microsoft Hanhan - Chinese (Traditional, Taiwan)","always":true,"x":385,"y":260,"wires":[],"l":false},{"id":"6c289518e882e786","type":"function","z":"8716e8bacd81f09b","name":"INSERT","func":"var Today = new Date();\nvar yyyy = Today.getFullYear(); //年\nvar MM = Today.getMonth()+1;    //月\nvar dd = Today.getDate();       //日\nvar h = Today.getHours();       //時\nvar m = Today.getMinutes();     //分\nvar s = Today.getSeconds();     //秒\nif(MM<10)\n{\n   MM = '0'+MM;\n}\n\nif(dd<10)\n{\n   dd = '0'+dd;\n}\n\nif(h<10)\n{\n   h = '0'+h;\n}\n\nif(m<10)\n{\n  m = '0' + m;\n}\n\nif(s<10)\n{\n  s = '0' + s;\n}\nvar var_date = yyyy+'/'+MM+'/'+dd;\nvar var_time = h+':'+m+':'+s;\n\nvar myLED = msg.payload;\n\n\nmsg.topic = \"INSERT INTO RFID_LOG ( uid , date , time ) VALUES ($myLED,  $var_date ,  $var_time ) \" ;\nmsg.payload = [myLED, var_date , var_time ]\nreturn msg;\n\n\n\n//CREATE TABLE \"RFID_LOG\" (\n//\t\"id\"\tINTEGER,\n//\t\"uid\"\tTEXT,\n//\t\"date\"\tTEXT,\n//\t\"time\"\tTEXT,\n//\tPRIMARY KEY(\"id\" AUTOINCREMENT)\n//);","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":180,"wires":[["cd4071495e70961e"]]},{"id":"cd4071495e70961e","type":"sqlite","z":"8716e8bacd81f09b","mydb":"374401a176cf2d52","sqlquery":"msg.topic","sql":"","name":"RFID database","x":580,"y":180,"wires":[["7dfd058865b98b40","c49f1e99cfe2d26a"]]},{"id":"a90ed08d57ae3c13","type":"debug","z":"8716e8bacd81f09b","name":"debug ","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":850,"y":300,"wires":[]},{"id":"7dfd058865b98b40","type":"debug","z":"8716e8bacd81f09b","name":"debug ","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":850,"y":180,"wires":[]},{"id":"53b5266828bf4059","type":"ui_button","z":"8716e8bacd81f09b","name":"","group":"9ddc322f04225a3c","order":7,"width":6,"height":2,"passthru":false,"label":"檢視資料庫資料","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"檢視資料","payloadType":"str","topic":"topic","topicType":"msg","x":240,"y":380,"wires":[["c49f1e99cfe2d26a","e322baf1296ee5d2"]]},{"id":"c49f1e99cfe2d26a","type":"function","z":"8716e8bacd81f09b","name":"檢視資料","func":"//CREATE TABLE \"RFID_LOG\" (\n//\t\"id\"\tINTEGER,\n//\t\"uid\"\tTEXT,\n//\t\"date\"\tTEXT,\n//\t\"time\"\tTEXT,\n//\tPRIMARY KEY(\"id\" AUTOINCREMENT)\n//);\n\n\n//SELECT * FROM RFID_LOG ORDER BY  id DESC LIMIT 50;\n\nmsg.topic = \"SELECT * FROM RFID_LOG ORDER BY id DESC LIMIT 50\";\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":380,"wires":[["562cbf8b650284be"]]},{"id":"f25bb3d7541a1213","type":"ui_table","z":"8716e8bacd81f09b","group":"9ddc322f04225a3c","name":"","order":5,"width":8,"height":9,"columns":[],"outputs":0,"cts":false,"x":850,"y":380,"wires":[]},{"id":"562cbf8b650284be","type":"sqlite","z":"8716e8bacd81f09b","mydb":"374401a176cf2d52","sqlquery":"msg.topic","sql":"","name":"RFID database","x":700,"y":380,"wires":[["f25bb3d7541a1213"]]},{"id":"5a318f812ec2b0c9","type":"ui_button","z":"8716e8bacd81f09b","name":"","group":"9ddc322f04225a3c","order":2,"width":2,"height":1,"passthru":false,"label":"刪除資料庫 ","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"刪除資料庫 ","payloadType":"str","topic":"topic","topicType":"msg","x":230,"y":620,"wires":[["4a2fcccd7dae4931","f4ce24e05da9e84a"]]},{"id":"68e19c9cfb4b8f9b","type":"function","z":"8716e8bacd81f09b","name":"DROP DATABASE","func":"//CREATE TABLE \"RFID_LOG\" (\n//\t\"id\"\tINTEGER,\n//\t\"uid\"\tTEXT,\n//\t\"date\"\tTEXT,\n//\t\"time\"\tTEXT,\n//\tPRIMARY KEY(\"id\" AUTOINCREMENT)\n//);\n\nmsg.topic = \"DROP TABLE RFID_LOG\";\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":800,"y":620,"wires":[["f8960e2dfe1f3c9a"]]},{"id":"f8960e2dfe1f3c9a","type":"sqlite","z":"8716e8bacd81f09b","mydb":"374401a176cf2d52","sqlquery":"msg.topic","sql":"","name":"RFID database","x":1000,"y":580,"wires":[["835fbe581d0acfd6"]]},{"id":"7df1a4c52b1ec8b1","type":"ui_button","z":"8716e8bacd81f09b","name":"","group":"9ddc322f04225a3c","order":3,"width":2,"height":1,"passthru":false,"label":"刪除所有資料 ","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"刪除所有資料 ","payloadType":"str","topic":"topic","topicType":"msg","x":240,"y":520,"wires":[["f4ce24e05da9e84a","4b38a92e2ff110c9"]]},{"id":"e5b24ff82f21c90f","type":"function","z":"8716e8bacd81f09b","name":"DELETE ALL DATA","func":"//CREATE TABLE \"RFID_LOG\" (\n//\t\"id\"\tINTEGER,\n//\t\"uid\"\tTEXT,\n//\t\"date\"\tTEXT,\n//\t\"time\"\tTEXT,\n//\tPRIMARY KEY(\"id\" AUTOINCREMENT)\n//);\n\n\nmsg.topic = \"DELETE from RFID_LOG\";\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":810,"y":520,"wires":[["f8960e2dfe1f3c9a"]]},{"id":"4a2fcccd7dae4931","type":"ui_toast","z":"8716e8bacd81f09b","position":"prompt","displayTime":"3","highlight":"","sendall":true,"outputs":1,"ok":"OK","cancel":"Cancel","raw":true,"className":"","topic":"","name":"","x":430,"y":620,"wires":[["488e9acdd25aa190"]]},{"id":"488e9acdd25aa190","type":"function","z":"8716e8bacd81f09b","name":"OK or Cancel","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,"noerr":0,"initialize":"","finalize":"","libs":[],"x":600,"y":620,"wires":[["68e19c9cfb4b8f9b"],[]]},{"id":"f4ce24e05da9e84a","type":"ui_audio","z":"8716e8bacd81f09b","name":"","group":"9ddc322f04225a3c","voice":"Microsoft Hanhan - Chinese (Traditional, Taiwan)","always":true,"x":365,"y":580,"wires":[],"l":false},{"id":"4b38a92e2ff110c9","type":"ui_toast","z":"8716e8bacd81f09b","position":"prompt","displayTime":"3","highlight":"","sendall":true,"outputs":1,"ok":"OK","cancel":"Cancel","raw":true,"className":"","topic":"","name":"","x":430,"y":520,"wires":[["5261be5ceac8b950"]]},{"id":"5261be5ceac8b950","type":"function","z":"8716e8bacd81f09b","name":"OK or Cancel","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,"noerr":0,"initialize":"","finalize":"","libs":[],"x":600,"y":520,"wires":[["e5b24ff82f21c90f"],[]]},{"id":"e322baf1296ee5d2","type":"ui_audio","z":"8716e8bacd81f09b","name":"","group":"9ddc322f04225a3c","voice":"Microsoft Hanhan - Chinese (Traditional, Taiwan)","always":true,"x":385,"y":340,"wires":[],"l":false},{"id":"835fbe581d0acfd6","type":"link out","z":"8716e8bacd81f09b","name":"link out 80","mode":"link","links":["a73daaa05aee25e6"],"x":1055,"y":460,"wires":[]},{"id":"a73daaa05aee25e6","type":"link in","z":"8716e8bacd81f09b","name":"link in 74","links":["835fbe581d0acfd6"],"x":375,"y":420,"wires":[["c49f1e99cfe2d26a"]]},{"id":"d5f295913269b6fc","type":"mqtt in","z":"8716e8bacd81f09b","name":"RFID UID in","topic":"alex9ufo/rfid/UID","qos":"1","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":230,"y":180,"wires":[["6c289518e882e786"]]},{"id":"9ddc322f04225a3c","type":"ui_group","name":"2026-03-RFID","tab":"0b311119db4c2faa","order":1,"disp":true,"width":14,"collapse":false,"className":""},{"id":"374401a176cf2d52","type":"sqlitedb","db":"D:\\2026 RFID\\2026-ex3\\202603RFID.db","mode":"RWC"},{"id":"b9efc827e98bf7f9","type":"mqtt-broker","name":"broker.mqtt-dashboard.com","broker":"broker.mqtt-dashboard.com","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"0b311119db4c2faa","type":"ui_tab","name":"RFIDSubpage2","icon":"dashboard","disabled":false,"hidden":false}]

這段程式碼定義了 Node-RED Dashboard 1.0 中的另一個副頁面 RFIDSubpage2。與前一個處理 LED 狀態的頁面不同,此頁面專門用於處理 RFID 卡號 (UID) 的資料庫紀錄與管理。

以下是針對您提供的 JSON 代碼進行的逐行功能說明:


1. 頁面導航:返回主頁面

位於流程的最上方,負責導回儀表板首頁。

  • ui_button (回主Tab):一個橘色的按鈕,標籤為「回 主Tab」。

  • function (切換指令):當按鈕被按下,執行 msg.payload = {"tab": "RFIDHome"}。這指定了目標分頁的名稱。

  • ui_ui_control:接收指令後,將用戶的瀏覽器畫面切換至 RFIDHome 分頁。


2. RFID 資料自動寫入 (INSERT)

這部分負責將感測器讀取的卡號存入資料庫。

  • mqtt in (RFID UID in):訂閱 MQTT 主題 alex9ufo/rfid/UID。當 RFID 讀卡機掃描到卡片並上傳 UID 時,觸發此流程。

  • function (INSERT)

    • 獲取時間:利用 JavaScript Date() 物件獲取目前的年、月、日、時、分、秒,並格式化為 YYYY/MM/DDHH:MM:SS

    • 準備 SQL:將接收到的 RFID UID(msg.payload)與時間存入數組。

    • 指令:生成 SQL 語句 INSERT INTO RFID_LOG (uid, date, time) VALUES ($myLED, $var_date, $var_time)

  • sqlite (RFID database):連接至實體路徑 D:\2026 RFID\2026-ex3\202603RFID.db,將數據永久保存。


3. 資料庫結構管理 (CREATE)

  • ui_button (建立資料庫):點擊後觸發資料表初始化。

  • function (CREATE DATABASE):發送 SQL 指令 CREATE TABLE RFID_LOG (id INTEGER, uid TEXT, date TEXT, time TEXT, PRIMARY KEY (id))

    • 註:id 設定為自動遞增(AUTOINCREMENT),作為每筆紀錄的唯一識別標籤。

  • ui_audio:動作執行時,透過語音合成 (TTS) 發出「建立資料庫」的中文語音回饋。


4. 資料檢視與顯示 (SELECT)

  • ui_button (檢視資料庫資料):寬度為 6、高度為 2 的大型按鈕。

  • function (檢視資料):發送查詢語句 SELECT * FROM RFID_LOG ORDER BY id DESC LIMIT 50

    • 這表示只抓取最新的 50 筆紀錄,並按 id 倒序排列(最新的在最上面)。

  • ui_table:將查詢結果以表格形式呈現在 Dashboard 上(如圖 nd6.png 所示)。


5. 安全刪除機制 (DROP / DELETE)

為了防止重要數據被誤刪,此處使用了彈出式確認視窗。

  • ui_toast (prompt):點擊「刪除」按鈕後,螢幕會彈出「OK」與「Cancel」的確認視窗。

  • function (OK or Cancel)

    • 如果用戶點擊 Cancel,流程終止。

    • 如果用戶點擊 OK,則允許發送 DELETE from RFID_LOG(清空紀錄)或 DROP TABLE RFID_LOG(刪除整個表)的指令給 SQLite 節點。


6. 系統回饋與連結

  • ui_audio:所有關鍵操作(刪除、建立、查詢)都配有語音提示,增加操作者的沉浸感與確認感。

  • link in / link out:用於跨區域連接流程。例如,當資料庫被清空或刪除後,透過 link 節點自動觸發「重新檢視資料」,使前端表格立刻更新為空白狀態,保持數據同步。


總結

這個副頁面 (RFIDSubpage2) 是一個完整的 RFID 紀錄管理系統。它與 RFIDSubpage1 共享相同的設計邏輯,但讀取的資料來源是 RFID 卡號,並且儲存在獨立的資料庫檔案 (202603RFID.db) 中,方便進行後續的考勤或門禁數據分析。


沒有留言:

張貼留言

2026 作業2 RFID+Node-Red+Python+SQLite 練習

2026 作業2  RFID+Node-Red+Python+SQLite 練習 # --- 設定區 --- MQTT_SERVER = "broker.mqtt-dashboard.com" TOPIC_RFID_UID = "alex9ufo/r...