2026年10月30日 星期五

Wokwi ESP32 MQTT 傳輸 + Python (Tkinter + SQLite) 門禁管理後台

 






 Wokwi ESP32 MQTT 傳輸 + Python (Tkinter + SQLite) 門禁管理後台 

ESP32 會連接至免費公共測試服務器 broker.hivemq.com,並在刷卡時將帶有 AA: (進) 與 BB: (出)
前綴的卡號發布至主題
wokwi/rfid/access



wokwi

程式
#include <WiFi.h>
#include <PubSubClient.h>
#include <SPI.h>
#include <MFRC522.h>

// Wi-Fi 與 MQTT 伺服器設定 (Wokwi 虛擬 Wi-Fi)
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* mqtt_server = "broker.hivemq.com";
const char* mqtt_topic = "wokwi/rfid/access";

WiFiClient espClient;
PubSubClient client(espClient);

// RFID 腳位定義
#define SS_1_PIN   5
#define RST_1_PIN 21
#define SS_2_PIN   4
#define RST_2_PIN 22

MFRC522 rfid1(SS_1_PIN, RST_1_PIN); // CARD1 (入口)
MFRC522 rfid2(SS_2_PIN, RST_2_PIN); // CARD2 (出口)

void setup_wifi() {
  delay(10);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi 已連線");
}

void reconnect() {
  while (!client.connected()) {
    String clientId = "ESP32-RFID-" + String(random(0xffff), HEX);
    if (client.connect(clientId.c_str())) {
      Serial.println("MQTT 已連線");
    } else {
      delay(2000);
    }
  }
}

void readAndPublish(MFRC522 &mfrc522, String prefix) {
  if (!mfrc522.PICC_IsNewCardPresent() || !mfrc522.PICC_ReadCardSerial()) {
    return;
  }

  String fullCode = prefix;
  for (byte i = 0; i < mfrc522.uid.size; i++) {
    if (i > 0) fullCode += ":";
    if (mfrc522.uid.uidByte[i] < 0x10) fullCode += "0";
    fullCode += String(mfrc522.uid.uidByte[i], HEX);
  }
  fullCode.toUpperCase();

  Serial.print("刷卡發送: ");
  Serial.println(fullCode);

  // 發送卡號至 MQTT 主題
  client.publish(mqtt_topic, fullCode.c_str());

  mfrc522.PICC_HaltA();
  mfrc522.PCD_StopCrypto1();
  delay(1000);
}

void setup() {
  Serial.begin(115200);
  SPI.begin();

  setup_wifi();
  client.setServer(mqtt_server, 1883);

  rfid1.PCD_Init();
  rfid2.PCD_Init();
  Serial.println("雙 MFRC522 + MQTT 系統已啟動");
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  readAndPublish(rfid1, "AA:"); // 入口
  readAndPublish(rfid2, "BB:"); // 出口

  delay(20);
}


執行此 Python 腳本後,會自動建立 access_control.db 資料庫,
訂閱 MQTT 主題,並透過視窗顯示即時進出日誌與當前場內的所有卡號與進場時間。





import sqlite3 import threading import tkinter as tk from datetime import datetime from tkinter import ttk import paho.mqtt.client as mqtt # --- 1. SQLite 資料庫初始化 --- def init_db(): conn = sqlite3.connect("access_control.db") cursor = conn.cursor() # 建立歷史刷卡日誌資料表 cursor.execute( """ CREATE TABLE IF NOT EXISTS access_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, card_uid TEXT, action TEXT, timestamp TEXT ) """ ) # 建立場內人員清單資料表 cursor.execute( """ CREATE TABLE IF NOT EXISTS inside_cards ( card_uid TEXT PRIMARY KEY, entry_time TEXT ) """ ) conn.commit() conn.close() init_db() # --- 2. GUI 主介面類別 --- class AccessControlApp: def __init__(self, root): self.root = root self.root.title("RFID 門禁與場內即時監控系統") self.root.geometry("750x450") # 頂部狀態列 self.status_label = tk.Label( root, text="場內當前總人數: 0 人", font=("Helvetica", 16, "bold"), bg="#2c3e50", fg="white", pady=10, ) self.status_label.pack(fill=tk.X) # 頁籤容器 notebook = ttk.Notebook(root) notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 分頁 1: 當前場內卡號 self.inside_frame = ttk.Frame(notebook) notebook.add(self.inside_frame, text="場內卡號清單") self.inside_tree = ttk.Treeview( self.inside_frame, columns=("UID", "EntryTime"), show="headings", ) self.inside_tree.heading("UID", text="卡片 UID") self.inside_tree.heading("EntryTime", text="入場時間") self.inside_tree.column("UID", width=250, anchor="center") self.inside_tree.column("EntryTime", width=350, anchor="center") self.inside_tree.pack(fill=tk.BOTH, expand=True) # 分頁 2: 歷史進出日誌 self.log_frame = ttk.Frame(notebook) notebook.add(self.log_frame, text="歷史進出紀錄") self.log_tree = ttk.Treeview( self.log_frame, columns=("ID", "UID", "Action", "Time"), show="headings", ) self.log_tree.heading("ID", text="編號") self.log_tree.heading("UID", text="卡片 UID") self.log_tree.heading("Action", text="動作") self.log_tree.heading("Time", text="記錄時間") self.log_tree.column("ID", width=60, anchor="center") self.log_tree.column("UID", width=200, anchor="center") self.log_tree.column("Action", width=120, anchor="center") self.log_tree.column("Time", width=250, anchor="center") self.log_tree.pack(fill=tk.BOTH, expand=True) # 載入初始資料 self.refresh_ui() # 處理卡片刷卡邏輯 def process_swipe(self, raw_data): now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") conn = sqlite3.connect("access_control.db") cursor = conn.cursor() if raw_data.startswith("AA:"): # 入口 uid = raw_data[3:] # 紀錄歷史 cursor.execute( "INSERT INTO access_logs (card_uid, action, timestamp) VALUES (?, ?, ?)", (uid, "IN (進場)", now_str), ) # 新增至場內清單 (若未在場內) cursor.execute( "INSERT OR REPLACE INTO inside_cards (card_uid, entry_time) VALUES (?, ?)", (uid, now_str), ) elif raw_data.startswith("BB:"): # 出口 uid = raw_data[3:] # 紀錄歷史 cursor.execute( "INSERT INTO access_logs (card_uid, action, timestamp) VALUES (?, ?, ?)", (uid, "OUT (出場)", now_str), ) # 從場內清單移除 cursor.execute( "DELETE FROM inside_cards WHERE card_uid = ?", (uid,) ) conn.commit() conn.close() # 更新 UI 畫面 self.root.after(0, self.refresh_ui) # 刷新表格與統計人數 def refresh_ui(self): conn = sqlite3.connect("access_control.db") cursor = conn.cursor() # 1. 刷新場內表格 for item in self.inside_tree.get_children(): self.inside_tree.delete(item) cursor.execute( "SELECT card_uid, entry_time FROM inside_cards ORDER BY entry_time DESC" ) inside_records = cursor.fetchall() for row in inside_records: self.inside_tree.insert("", tk.END, values=row) # 2. 刷新歷史紀錄 for item in self.log_tree.get_children(): self.log_tree.delete(item) cursor.execute( "SELECT id, card_uid, action, timestamp FROM access_logs ORDER BY id DESC LIMIT 50" ) for row in cursor.fetchall(): self.log_tree.insert("", tk.END, values=row) # 3. 更新計數 count = len(inside_records) self.status_label.config(text=f"場內當前總人數: {count} 人") conn.close() # --- 3. MQTT 接收執行緒 --- def start_mqtt(app): def on_connect(client, userdata, flags, rc): print("已成功連線至 MQTT Broker,開始監聽刷卡事件...") client.subscribe("wokwi/rfid/access") def on_message(client, userdata, msg): payload = msg.payload.decode("utf-8") print(f"收到 MQTT 訊息: {payload}") app.process_swipe(payload) client = mqtt.Client() client.on_connect = on_connect client.on_message = on_message client.connect("broker.hivemq.com", 1883, 60) client.loop_forever() if __name__ == "__main__": root = tk.Tk() app = AccessControlApp(root) # 將 MQTT 放在 background thread 避免主介面凍結 mqtt_thread = threading.Thread(target=start_mqtt, args=(app,), daemon=True) mqtt_thread.start() root.mainloop()


雙 MFRC522 + MQTT 系統已啟動 MQTT 已連線 刷卡發送: AA:AA:11:22:33 刷卡發送: BB:BB:44:55:66 刷卡發送: BB:11:22:33:44 刷卡發送: AA:11:22:33:44 刷卡發送: AA:11:22:33:44 刷卡發送: BB:11:22:33:44 刷卡發送: BB:11:22:33:44 刷卡發送: BB:11:22:33:44 刷卡發送: BB:BB:44:55:66 刷卡發送: BB:BB:44:55:66 刷卡發送: BB:BB:44:55:66 刷卡發送: AA:11:22:33:44 刷卡發送: AA:AA:11:22:33 刷卡發送: AA:55:66:77:88 刷卡發送: AA:AA:BB:CC:DD 刷卡發送: AA:04:11:22:33 刷卡發送: AA:C0:FF:EE:99 刷卡發送: BB:BB:44:55:66 刷卡發送: BB:11:22:33:44 刷卡發送: BB:11:22:33:44 刷卡發送: BB:11:22:33:44 刷卡發送: BB:55:66:77:88 刷卡發送: BB:55:66:77:88







沒有留言:

張貼留言

MQTT + SQLite + Tkinter + Telegram 雙向門禁管理後台系統 (作業3)

MQTT + SQLite + Tkinter + Telegram 雙向門禁管理後台系統 。 它透過多執行緒(Multithreading)同時處理 MQTT 訊息接收、GUI 視窗介面更新、SQLite 資料庫讀寫,以及 Telegram Bot 的推播與指令監聽。 一、 系...