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"
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: 這是程式的核心。當接收到新資料時:自動抓取目前的日期與時間。
將資料寫入對應的 SQLite 資料庫。
觸發語音:讀出狀態或卡號末四位。
即時更新介面:使用
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__)
多線程執行: 程式分為兩個主要部分同時跑:
MQTT Loop: 持續監控網路訊息,若斷線則每 5 秒嘗試重新連線。
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)安裝步驟詳解:
- 安裝 Node.js:
- 前往 Node.js 官網下載並安裝 LTS(長期支援)版本。
- 安裝 Node-RED:
- 打開終端機 (Terminal) 或命令提示字元 (cmd)。
- 輸入指令:
npm install -g --unsafe-perm node-red(Windows/Mac/Linux 均適用)。
- 啟動 Node-RED:
- 安裝完成後,輸入:
node-red。
- 安裝完成後,輸入:
- 開啟編輯器:
- 在瀏覽器輸入
http://localhost:1880即可開始使用。
- 打開 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聲音。當動作執行成功時,電腦/手機會發出語音提醒用戶。
這段程式碼定義了 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/DD與HH: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) 中,方便進行後續的考勤或門禁數據分析。



















沒有留言:
張貼留言