2026年9月13日 星期日

2026-09 MQTT + wokwi esp32 + Node-red + Python (作業2)

2026-09  MQTT + wokwi esp32 +  Node-red + Python (作業2)







在這套 ESP32 門禁系統中,MQTT 主題(Topic)扮演著「訊息傳遞通道/地址」的角色。MQTT 採用發行/訂閱(Publish/Subscribe)架構,ESP32 與後端(Python GUI / Node-RED)透過以下三個主題進行雙向資料交換:

一、 主題功能詳細解析

1. PubTopic3 (alex9ufo/2026/RFID/RFID_UID)

  • 方向: ESP32 發行 $\rightarrow$ 後端訂閱(TX)

  • 傳送內容: 8 碼或 14 碼的 RFID 實體卡片 UID(例如 AABBCCDD04112233445566)。

  • 功能說明: 當使用者在門口刷卡時,ESP32 讀取卡片號碼,並將 UID 發送至此主題。

  • 後端處理: Node-RED 或 Python 收到後,會加上 [UID] 前綴,搭配當前日期與時間寫入 mqtt.db 資料庫作為進出刷卡紀錄。

2. PubTopic4 (alex9ufo/2026/RFID/RFID_PICC)

  • 方向: ESP32 / 後端發行 $\rightarrow$ 後端訂閱(TX)

  • 傳送內容:

    • 4x4 Keypad 輸入的 8 位數密碼(例如 88888888)。

    • 工作證號碼(例如 #12345678)。

    • 實體按鈕門鈴觸發訊息(DOORBELL_RING)。

  • 功能說明: 作為「非卡片類」事件的回報通道。當使用者在門口按門鈴或使用鍵盤輸入資料時,ESP32 會將對應訊息發送至此主題。

  • 後端處理: 後端收到後會加上 [PICC/Keypad] 前綴並自動存入 SQLite 資料庫;若是門鈴訊息,亦可用於觸發遠端告警通知。

3. SubTopicCtrl (alex9ufo/2026/RFID/CONTROL)

  • 方向: 後端發行 $\rightarrow$ ESP32 訂閱(RX)

  • 傳送內容: 遠端開門指令字串(例如 "open""1""true")。

  • 功能說明: 作為「遠端控制門鎖」的專用指令通道。ESP32 啟動後會持續監聽此主題。

  • 執行動作: 當管理者在 Python GUI 或 Node-RED Dashboard 點擊「🔓 遠端開門」按鈕時,後端會向此主題發送 "open",ESP32 收到後便會立即驅動 Servo 馬達旋轉 90 度開門 3 秒。

二、 三大主題運作對照表

主題變數名稱主題名稱 (MQTT Topic)數據流向主要傳送內容/指令觸發時機與主要功能
PubTopic3alex9ufo/2026/RFID/RFID_UIDESP32 $\rightarrow$ 後端卡號 UID (如 AABBCCDD)刷卡時主動發行,用於出勤/刷卡紀錄
PubTopic4alex9ufo/2026/RFID/RFID_PICCESP32 $\rightarrow$ 後端密碼、工作證號或 DOORBELL_RING按門鈴或鍵盤輸入時發行,用於操作紀錄與通知
SubTopicCtrlalex9ufo/2026/RFID/CONTROL後端 $\rightarrow$ ESP32遠端開門指令 "open"點擊中控台「遠端開門」按鈕時發行,驅動 Servo 馬達

參考:

https://alex9ufoexploer.blogspot.com/2026/03/2026-2-rfidnode-redpythonsqlite.html





以下為本專案 ESP32 門禁與 IoT 監控系統 所使用的完整接腳(GPIO)配置清單:

ESP32 接腳分配表

模組 / 周邊元件訊號名稱ESP32 GPIO 腳位腳位功能與說明
MFRC522 RFID 讀卡器SDA (SS)GPIO 5SPI 晶片選擇 (VSPI SS)
SCKGPIO 18SPI 時脈線 (VSPI SCK)
MISOGPIO 19SPI 主機輸入 (VSPI MISO)
MOSIGPIO 23SPI 主機輸出 (VSPI MOSI)
RSTGPIO 4模組重置腳位
LCD 16x2 (I2C 介面)SDAGPIO 21I2C 數據線
SCLGPIO 22I2C 時脈線
HC-SR04 超音波感測器TRIGGPIO 16超音波觸發訊號輸出
ECHOGPIO 17超音波回波讀取輸入
4x4 Keypad 矩陣鍵盤R1 (Row 1)GPIO 26矩陣掃描行 1
R2 (Row 2)GPIO 25矩陣掃描行 2
R3 (Row 3)GPIO 33矩陣掃描行 3
R4 (Row 4)GPIO 32矩陣掃描行 4
C1 (Col 1)GPIO 13矩陣掃描列 1
C2 (Col 2)GPIO 12矩陣掃描列 2
C3 (Col 3)GPIO 14矩陣掃描列 3
C4 (Col 4)GPIO 27矩陣掃描列 4
Servo 馬達PWM ControlGPIO 2控制 Servo 旋轉 (0°關門 / 90°開門)
Buzzer 蜂鳴器Positive (+)GPIO 15發出高低音提示聲響
Pushbutton 門鈴按鈕SignalGPIO 34

純輸入腳位 (Input-Only)


⚠️ 須外接 10kΩ 電阻上拉至 3.3V,按鍵另一端接 GND

接線注意事項:

  1. GPIO 34(門鈴按鍵): 屬於 Input-Only 腳位,內部沒有上拉電阻。硬體線路上務必在 GPIO 34 與 3.3V 之間外接一顆 10kΩ 提升電阻,防止未按下按鈕時腳位懸空導致 MQTT 訊息無限重複發送。

  2. I2C 預設位址: LCD16x2 搭配 PCF8574 轉接板,I2C 位址預設為 0x27




這份 ESP32 程式整合了多個硬體模組與 MQTT 網路通訊,主要實現了「本機智慧門禁驗證」「遠端物聯網控門與回報」以及「智慧省電節能」三大核心功能。

一、 核心功能總覽

  • 多重驗證解鎖(控制 Servo 馬達):

    • RFID 刷卡驗證: 讀取 MFRC522 卡片 UID,若匹配本機白名單卡號(如 010203041122334455667788AABBCCDD04112233445566C0FFEE99),即驅動伺服馬達旋轉 90 度開門 3 秒。

    • Keypad 密碼驗證: 透過 4x4 鍵盤輸入 8 位數密碼,若為 88888888 則觸發開門 3 秒。

    • 工作證號輸入: 按下 # 鍵可進入工作證模式(格式為 # + 8 碼數字)。

  • MQTT 物聯網雙向通訊:

    • 即時發行數據 (TX):

      • 刷卡時將 UID 發送至 alex9ufo/2026/RFID/RFID_UID

      • 輸入密碼、工作證號或按下門鈴時,發送至 alex9ufo/2026/RFID/RFID_PICC(如門鈴觸發發送 DOORBELL_RING)。

    • 遠端訂閱控制 (RX): 訂閱 alex9ufo/2026/RFID/CONTROL 主題,當接收到 "open""1""true" 等指令時,可直接遠端驅動馬達開門。

  • HC-SR04 自動感應與省電:

    • 當偵測到前方的障礙物/人體距離小於 50 cm 時,自動點亮 16x2 LCD 背光並顯示操作選單。

    • 若超過 10 秒 無人靠近,自動關閉 LCD 背光以達省電與保護螢幕效果。

  • 門鈴與周邊警報回饋:

    • Pushbutton 門鈴: 按下接於 GPIO 34 的門鈴按鍵時,顯示提示文字並觸發 MQTT 門鈴通知(採用去彈跳與邊緣觸發機制,避免重複發送)。

    • Buzzer 聲響引導: 驗證成功發出高音短嗶聲,驗證失敗發出低音警告聲。


二、 硬體腳位配置表

模組元件功能說明ESP32 GPIO 腳位配置備註 / 特殊說明
MFRC522RFID 讀卡器 (SPI)SDA(SS)=5, SCK=18, MOSI=23, MISO=19, RST=4標準 VSPI 介面
LCD 16x2系統狀態顯示 (I2C)SDA=21, SCL=22I2C 預設位址 0x27
HC-SR04超音波人體感應TRIG=16, ECHO=17測距範圍 < 50 cm 自動喚醒
Servo Motor門鎖模擬馬達 (PWM)PWM=20 度關門 / 90 度開門 3 秒
4x4 Keypad密碼與工作證輸入R1~R4: 26, 25, 33, 32 / C1~C4: 13, 12, 14, 27矩陣式掃描鍵盤
Pushbutton實體門鈴按鈕GPIO 34Pure Input 腳位,須外接 10k 電阻上拉至 3.3V
Buzzer提示音蜂鳴器GPIO 15提供成功與失敗聲響回饋
#include <WiFi.h>
#include <PubSubClient.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include <ESP32Servo.h>

// ==========================
// 1. WiFi 與 MQTT 設定
// ==========================
const char* ssid        = "Wokwi-GUEST";
const char* password    = "";
const char* broker      = "broker.emqx.io";
const int   port        = 1883;

// MQTT 主題配置
const char* PubTopic3    = "alex9ufo/2026/RFID/RFID_UID";    // 發行 RFID UID
const char* PubTopic4    = "alex9ufo/2026/RFID/RFID_PICC";   // 發行 Keypad / 門鈴訊息
const char* SubTopicCtrl = "alex9ufo/2026/RFID/CONTROL";    // 訂閱遠端開門指令

WiFiClient espClient;
PubSubClient client(espClient);

// ==========================
// 2. MFRC522 (SDA=5, SCK=18, MOSI=23, MISO=19, RST=4)
// ==========================
#define SS_PIN  5
#define RST_PIN 4
MFRC522 rfid(SS_PIN, RST_PIN);

// ==========================
// 3. LCD I2C (SDA=21, SCL=22)
// ==========================
LiquidCrystal_I2C lcd(0x27, 16, 2);

// ==========================
// 4. 周邊腳位定義
// ==========================
#define BUZZER_PIN 15
#define BELL_PIN   34   // GPIO 34 (須外接 10k 電阻上拉至 3.3V)
#define SERVO_PIN  2    // PWM 控制腳位

Servo doorServo;

// ==========================
// 5. HC-SR04 超音波感測器 (TRIG=16, ECHO=17)
// ==========================
#define TRIG_PIN 16
#define ECHO_PIN 17
unsigned long lastPersonTime = 0;
bool lcdBacklightOn = true;

// ==========================
// 6. Keypad (R1=26, R2=25, R3=33, R4=32, C1=13, C2=12, C3=14, C4=27)
// ==========================
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
  {'1','2','3','A'},
  {'4','5','6','B'},
  {'7','8','9','C'},
  {'*','0','#','D'}
};
byte rowPins[ROWS] = {26, 25, 33, 32};
byte colPins[COLS] = {13, 12, 14, 27};

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

// ==========================
// 7. 驗證白名單與系統變數
// ==========================
String passwordInput = "";
bool lastBellState = HIGH;

// 白名單卡號清單 (大寫、無冒號)
const int VALID_UID_COUNT = 6;
const String validUIDs[VALID_UID_COUNT] = {
  "01020304",
  "11223344",
  "55667788",
  "AABBCCDD",
  "04112233445566",
  "C0FFEE99"
};

// 本機白名單開門密碼
const String VALID_PASSWORD = "88888888";

// 副程式宣告
void showMenu();
void openDoor();
void beepSuccess();
void beepError();

// 檢查卡號是否在授權清單內
bool isAuthorizedUID(String uid) {
  for (int i = 0; i < VALID_UID_COUNT; i++) {
    if (uid.equalsIgnoreCase(validUIDs[i])) {
      return true;
    }
  }
  return false;
}

// ==========================
// WiFi & MQTT 連線處理
// ==========================
void setup_wifi() {
  delay(10);
  Serial.println("\n[WiFi] Connecting...");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\n[WiFi] Connected!");
  Serial.print("[WiFi] IP: ");
  Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* payload, unsigned int length) {
  String message = "";
  for (unsigned int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  message.trim();
  message.toLowerCase();

  Serial.println("----------------------------------------");
  Serial.print("[MQTT RX] Topic: ");
  Serial.println(topic);
  Serial.print("[MQTT RX] Payload: ");
  Serial.println(message);

  // 遠端開門控制指令判斷
  if (String(topic) == SubTopicCtrl) {
    if (message == "open" || message == "1" || message == "true" || message.indexOf("true") != -1) {
      Serial.println("[Control] Remote Open Command Received!");
      openDoor();
    }
  }
  Serial.println("----------------------------------------");
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("[MQTT] Connecting to ");
    Serial.print(broker);
    Serial.print("...");
    String clientId = "ESP32RFID-" + String(random(0xffff), HEX);

    if (client.connect(clientId.c_str())) {
      Serial.println(" Connected!");
      client.subscribe(SubTopicCtrl);
      Serial.print("[MQTT] Subscribed to: ");
      Serial.println(SubTopicCtrl);
    } else {
      Serial.print(" Failed, rc=");
      Serial.print(client.state());
      Serial.println(" Retrying in 5s...");
      delay(5000);
    }
  }
}

// ==========================
// SETUP 初始化
// ==========================
void setup() {
  Serial.begin(115200);

  // 1. SPI 與 MFRC522 初始化
  SPI.begin();
  rfid.PCD_Init();

  // 2. LCD I2C 初始化 (SDA=21, SCL=22)
  Wire.begin(21, 22);
  lcd.init();
  lcd.backlight();

  // 3. GPIO 引腳設定 (GPIO 34 使用 INPUT 模式)
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(BELL_PIN, INPUT);

  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);

  // 4. Servo 馬達初始化
  ESP32PWM::allocateTimer(0);
  doorServo.setPeriodHertz(50);
  doorServo.attach(SERVO_PIN, 500, 2400);
  doorServo.write(0); // 初始狀態:關門 (0度)

  // 5. 網路與 MQTT 設定
  setup_wifi();
  client.setServer(broker, port);
  client.setCallback(callback);

  lastPersonTime = millis();
  showMenu();
}

// ==========================
// LOOP 主循環
// ==========================
void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  handleUltrasonic();
  handleRFID();
  handleKeypad();
  handleDoorBell();
}

// ==========================
// 主功能邏輯
// ==========================
void showMenu() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("1.Card/Password");
  lcd.setCursor(0, 1);
  lcd.print("2.ID:#00000000");
}

void openDoor() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Door Opening...");
 
  doorServo.write(90); // 旋轉 90 度開門
  beepSuccess();

  lcd.setCursor(0, 0);
  lcd.print("Door Open       ");
  lcd.setCursor(0, 1);
  lcd.print("Welcome!        ");

  delay(3000); // 保持開門 3 秒

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Door Closing...");
  doorServo.write(0);  // 歸位關門 (0度)
  delay(500);

  showMenu();
}

// 1. HC-SR04 超音波節能與背光控制
void handleUltrasonic() {
  static unsigned long lastCheckTime = 0;
  if (millis() - lastCheckTime < 200) return;
  lastCheckTime = millis();

  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  long duration = pulseIn(ECHO_PIN, HIGH, 30000);
  float distance = duration * 0.034 / 2;

  if (distance > 0 && distance < 50) {
    lastPersonTime = millis();
    if (!lcdBacklightOn) {
      lcd.backlight();
      lcdBacklightOn = true;
      showMenu();
    }
  } else if (lcdBacklightOn && (millis() - lastPersonTime > 10000)) {
    lcd.noBacklight();
    lcdBacklightOn = false;
  }
}

// 2. MFRC522 RFID 讀卡處理
void handleRFID() {
  if (!rfid.PICC_IsNewCardPresent()) return;
  if (!rfid.PICC_ReadCardSerial()) return;

  lastPersonTime = millis();
  if (!lcdBacklightOn) { lcd.backlight(); lcdBacklightOn = true; }

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

  Serial.println("----------------------------------------");
  Serial.print("[RFID Sensed] UID: ");
  Serial.println(uid);

  // 發行 UID 至 alex9ufo/2026/RFID/RFID_UID
  client.publish(PubTopic3, uid.c_str());
  Serial.print("[MQTT TX] ");
  Serial.print(PubTopic3);
  Serial.print(" -> ");
  Serial.println(uid);

  // 白名單開門判定
  if (isAuthorizedUID(uid)) {
    openDoor(); // 白名單卡號,旋轉 SERVO 3 秒
  } else {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Card Invalid!");
    lcd.setCursor(0, 1);
    lcd.print("UID: " + uid);
    beepError();
    delay(2000);
    showMenu();
  }

  rfid.PICC_HaltA();
}

// 3. Keypad 鍵盤輸入處理
void handleKeypad() {
  char key = keypad.getKey();
  if (!key) return;

  lastPersonTime = millis();
  if (!lcdBacklightOn) { lcd.backlight(); lcdBacklightOn = true; showMenu(); }

  Serial.print("[Keypad] Pressed: ");
  Serial.println(key);

  if (key == 'A') {
    passwordInput = "";
    showMenu();
    return;
  }

  // 工作證模式 (#)
  if (key == '#') {
    passwordInput = "#";
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Staff ID Mode:");
    lcd.setCursor(0, 1);
    lcd.print("#");
    return;
  }

  // 工作證號輸入中 (# + 8 碼數字)
  if (passwordInput.startsWith("#")) {
    if (key >= '0' && key <= '9') {
      passwordInput += key;
      lcd.setCursor(0, 1);
      lcd.print(passwordInput);

      if (passwordInput.length() == 9) {
        Serial.println("----------------------------------------");
        Serial.print("[Keypad TX] Staff ID: ");
        Serial.println(passwordInput);

        client.publish(PubTopic4, passwordInput.c_str());

        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Sending Staff ID");
        lcd.setCursor(0, 1);
        lcd.print(passwordInput);
        beepSuccess();
        delay(1500);

        passwordInput = "";
        showMenu();
      }
    }
    return;
  }

  // 一般數字密碼輸入
  if (key >= '0' && key <= '9') {
    passwordInput += key;

    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Password:");
    lcd.setCursor(0, 1);

    for (int i = 0; i < passwordInput.length(); i++) {
      lcd.print("*");
    }

    // 當密碼輸入滿 8 位數時進行 88888888 驗證
    if (passwordInput.length() == 8) {
      Serial.println("----------------------------------------");
      Serial.print("[Keypad TX] Password: ");
      Serial.println(passwordInput);

      // 發行至 alex9ufo/2026/RFID/RFID_PICC
      client.publish(PubTopic4, passwordInput.c_str());

      // 密碼比對驗證
      if (passwordInput == VALID_PASSWORD) {
        openDoor(); // 密碼正確 (88888888),旋轉 SERVO 3 秒
      } else {
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Password Wrong!");
        lcd.setCursor(0, 1);
        lcd.print("Try Again");
        beepError();
        delay(2000);
        showMenu();
      }

      passwordInput = "";
    }
  }
}

// 4. Pushbutton 門鈴按鈕處理 (GPIO 34 邊緣觸發)
void handleDoorBell() {
  bool currentBellState = digitalRead(BELL_PIN);

  // 邊緣觸發判定 (當按鈕由 HIGH 變為 LOW 瞬間)
  if (lastBellState == HIGH && currentBellState == LOW) {
    delay(30); // 按鍵防彈跳
    if (digitalRead(BELL_PIN) == LOW) {
      lastPersonTime = millis();
      if (!lcdBacklightOn) { lcd.backlight(); lcdBacklightOn = true; }

      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Please Wait...");
      lcd.setCursor(0, 1);
      lcd.print("Notifying Owner");

      beepSuccess();

      // 發送 MQTT 門鈴觸發訊息
      client.publish(PubTopic4, "DOORBELL_RING");
      Serial.println("----------------------------------------");
      Serial.print("[MQTT TX] Doorbell Pressed (GPIO 34) -> Published to: ");
      Serial.println(PubTopic4);
      Serial.println("----------------------------------------");

      delay(1000);
      showMenu();
    }
  }
  lastBellState = currentBellState;
}

// 5. 蜂鳴器聲音控制
void beepSuccess() {
  tone(BUZZER_PIN, 1000);
  delay(150);
  noTone(BUZZER_PIN);
}

void beepError() {
  tone(BUZZER_PIN, 300);
  delay(400);
  noTone(BUZZER_PIN);
}

MQTTX 是一款由 EMQ 團隊開發的開放原始碼、跨平台 MQTT 5.0 桌面與 CLI/Web 客戶端測試工具。在開發 ESP32、樹莓派或 Node-RED 等 IoT 物聯網專案時,MQTTX 常被用來模擬裝置、發送控制指令與監測 MQTT Broker 的訊息傳輸。

其主要核心功能如下:

  • 跨平台與多種型態支援

    • 桌面版 (Desktop): 提供跨平台 GUI 視窗介面(支援 Windows, macOS, Linux),方便視覺化操作。

    • Web 版 (MQTTX Web): 免安裝,直接在瀏覽器開啟(mqttx.app/web)即可測試通訊。

    • CLI 命令列版 (MQTTX CLI): 提供命令列工具,適合整合至自動化腳本或伺服器環境。

  • 完整支援 MQTT 協定版本

    • 完全相容 MQTT v3.1v3.1.1 以及 MQTT v5.0 協定。

    • 支援 WebSocket (WS / WSS) 及 TCP/TLS 安全加密連線。

  • 多連線同時管理

    • 支援同時建立並管理多個 MQTT 客戶端連線(Client ID)。

    • 可自由切換不同的連線分頁,方便模擬多台硬體裝置同時在線。

  • 訊息訂閱與發行 (Pub/Sub)

    • 訂閱主題 (Subscribe): 輸入如 alex9ufo/2026/RFID/#,可自訂 QoS 等級並接收即時數據訊息。

    • 發行主題 (Publish): 指定 Topic(如 alex9ufo/2026/RFID/CONTROL),隨時發送控制 Payload(如 open{"value":true})。

  • 多種 Payload 資料格式解析

    • 支援 JSONPlain TextHexBase64CBOR 等數據格式。

    • 具備 JSON 格式語法高亮與格式化驗證功能,避免發送錯誤的 JSON 封包。

  • 腳本與自動化測試 (Scripting)

    • 支援撰寫 JavaScript 腳本,可在發送或接收訊息時進行數據轉換(例如:將 RAW Data 自動格式化)或模擬大量自動化測試數據發送

  • 日誌與歷史紀錄

    • 自動保存連線與訂閱歷史,方便下次直接開啟測試。

    • 內建日誌檢視器,可輕鬆除錯連線失敗或被 Broker 中斷(Disconnect)的原因。










Node-red 程式

[{"id":"init_db_inject","type":"inject","z":"5d30d10850012aab","name":"啟動時建立資料表","props":[],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":270,"y":280,"wires":[["fn_init_db"]]},{"id":"fn_init_db","type":"function","z":"5d30d10850012aab","name":"CREATE TABLE SQL","func":"msg.topic = 'CREATE TABLE IF NOT EXISTS rfid_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, mqtt TEXT NOT NULL, date TEXT NOT NULL, time TEXT NOT NULL);';\nreturn msg;","outputs":1,"noerr":0,"x":500,"y":280,"wires":[["sqlite_node"]]},{"id":"mqtt_in_uid","type":"mqtt in","z":"5d30d10850012aab","name":"MQTT UID","topic":"alex9ufo/2026/RFID/RFID_UID","qos":"2","datatype":"utf8","broker":"mqtt_broker","nl":false,"rap":true,"rh":0,"inputs":0,"x":240,"y":380,"wires":[["fn_parse_mqtt"]]},{"id":"mqtt_in_picc","type":"mqtt in","z":"5d30d10850012aab","name":"MQTT PICC","topic":"alex9ufo/2026/RFID/RFID_PICC","qos":"2","datatype":"utf8","broker":"mqtt_broker","nl":false,"rap":true,"rh":0,"inputs":0,"x":250,"y":440,"wires":[["fn_parse_mqtt"]]},{"id":"fn_parse_mqtt","type":"function","z":"5d30d10850012aab","name":"格式化與 SQL 插入","func":"let topic = msg.topic;\nlet payload = msg.payload;\nlet formatted_msg = '';\nif (topic.includes('RFID_UID')) {\n    formatted_msg = '[UID] ' + payload;\n} else if (topic.includes('RFID_PICC')) {\n    formatted_msg = '[PICC/Keypad] ' + payload;\n} else {\n    formatted_msg = payload;\n}\nlet now = new Date();\nlet date_str = now.toISOString().split('T')[0];\nlet time_str = now.toTimeString().split(' ')[0];\nmsg.topic = `INSERT INTO rfid_logs (mqtt, date, time) VALUES ('${formatted_msg}', '${date_str}', '${time_str}');`;\nreturn msg;","outputs":1,"noerr":0,"x":500,"y":400,"wires":[["sqlite_node"]]},{"id":"ui_btn_open","type":"ui_button","z":"5d30d10850012aab","name":"","group":"rfid_group","order":1,"width":6,"height":1,"passthru":false,"label":"🔓 遠端開門 (Open Door)","tooltip":"","color":"#ffffff","bgcolor":"#4CAF50","icon":"","payload":"open","payloadType":"str","topic":"topic","topicType":"msg","x":270,"y":520,"wires":[["mqtt_out_ctrl","fn_log_remote_open"]]},{"id":"mqtt_out_ctrl","type":"mqtt out","z":"5d30d10850012aab","name":"MQTT Control Out","topic":"alex9ufo/2026/RFID/CONTROL","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"mqtt_broker","x":530,"y":500,"wires":[]},{"id":"fn_log_remote_open","type":"function","z":"5d30d10850012aab","name":"紀錄遠端開門至 SQL","func":"let now = new Date();\nlet date_str = now.toISOString().split('T')[0];\nlet time_str = now.toTimeString().split(' ')[0];\nmsg.topic = `INSERT INTO rfid_logs (mqtt, date, time) VALUES ('RFID/CONTROL', '${date_str}', '${time_str}');`;\nreturn msg;","outputs":1,"noerr":0,"x":530,"y":560,"wires":[["sqlite_node"]]},{"id":"fn_fetch_30","type":"function","z":"5d30d10850012aab","name":"查詢最近 30 筆 SQL","func":"msg.topic = 'SELECT id, mqtt, date, time FROM rfid_logs ORDER BY id DESC LIMIT 30;';\nreturn msg;","outputs":1,"noerr":0,"x":960,"y":400,"wires":[["sqlite_fetch_node"]]},{"id":"ui_table_logs","type":"ui_table","z":"5d30d10850012aab","group":"rfid_group","name":"歷史紀錄表 (最近 30 筆)","order":2,"width":"12","height":"8","columns":[{"field":"id","title":"ID (自動+1)","width":"15%","align":"center"},{"field":"mqtt","title":"MQTT 接收內容","width":"45%","align":"left"},{"field":"date","title":"日期 (Date)","width":"20%","align":"center"},{"field":"time","title":"時間 (Time)","width":"20%","align":"center"}],"outputs":0,"cts":false,"x":1030,"y":560,"wires":[]},{"id":"sqlite_node","type":"sqlite","z":"5d30d10850012aab","mydb":"sqlite_db","sqlquery":"msg.topic","sql":"","name":"mqtt.db 處理","x":750,"y":400,"wires":[["fn_fetch_30"]]},{"id":"sqlite_fetch_node","type":"sqlite","z":"5d30d10850012aab","mydb":"sqlite_db","sqlquery":"msg.topic","sql":"","name":"讀取最近 30 筆","x":960,"y":480,"wires":[["ui_table_logs"]]},{"id":"mqtt_broker","type":"mqtt-broker","name":"EMQX Broker","broker":"broker.emqx.io","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true},{"id":"rfid_group","type":"ui_group","name":"系統監控與控制","tab":"rfid_tab","order":1,"disp":true,"width":"12","collapse":false},{"id":"sqlite_db","type":"sqlitedb","db":"E:\\VScode\\ex5\\mqtt.db","mode":"RWC"},{"id":"rfid_tab","type":"ui_tab","name":"RFID 門禁中控台","icon":"dashboard","disabled":false,"hidden":false},{"id":"7daad3988aff5054","type":"global-config","env":[],"modules":{"node-red-dashboard":"3.6.6","node-red-node-ui-table":"0.4.5","node-red-node-sqlite":"2.0.1"}}]


Node-RED 門禁監控中控台主要實現了「MQTT 數據監聽」「SQLite 資料庫自動儲存」「Web UI 歷史紀錄呈現」以及「遠端開門指令控制」四大功能。

一、 核心功能與運作流程

  1. MQTT 數據自動監聽與格式化:

    • 自動連線至 MQTT Broker (broker.emqx.io:1883)。

    • 訂閱主題:

      • alex9ufo/2026/RFID/RFID_UID:接收 ESP32 傳來的刷卡 UID,自動格式化標示為 [UID] <卡號>

      • alex9ufo/2026/RFID/RFID_PICC:接收 Keypad 輸入密碼、工作證號或門鈴觸發訊息,格式化標示為 [PICC/Keypad] <內容>

  2. SQLite 資料庫儲存管理 (mqtt.db):

    • 自動初始化: 系統啟動時自動檢測並建立 rfid_logs 資料表(若不存在)。

    • 自動寫入機制: 每當收到新的 MQTT 數據,自動擷取當下的日期(date)與時間(time),寫入資料庫中。

    • 欄位結構:

      • id:主鍵,自動從 1 開始遞增(INTEGER PRIMARY KEY AUTOINCREMENT)。

      • mqtt:格式化後的 MQTT 訊息內容。

      • date:記錄日期(格式:YYYY-MM-DD)。

      • time:記錄時間(格式:HH:MM:SS)。

  3. Web Dashboard 介面與最近 30 筆歷史呈現:

    • 提供視覺化網頁介面(路徑為 /ui)。

    • 資料庫新增紀錄後,會立即執行 SQL 查詢 SELECT ... ORDER BY id DESC LIMIT 30,並將最新的 30 筆通行歷史紀錄即時更新顯示於網頁表格(ui_table)中。

  4. 遠端開門控制 (Remote Control):

    • Dashboard 提供一個 「🔓 遠端開門 (Open Door)」 按鈕。

    • 點擊按鈕時會同時觸發兩項動作:

      1. 向 MQTT 主題 alex9ufo/2026/RFID/CONTROL 發送指令 "open" 驅動 ESP32 Servo 馬達開門。

      2. 在 SQLite 資料庫中主動寫入一筆 mqtt 欄位為 "RFID/CONTROL" 的紀錄並同步更新至表格。

二、 Node-RED 節點功能對照表

節點名稱 / 類型節點功能說明
Inject (啟動時建立資料表)系統部署/啟動時自動發出觸發訊號,執行資料庫建表。
MQTT In (UID & PICC)訂閱 ESP32 發出的刷卡與鍵盤/門鈴訊息。
Function (格式化與 SQL 插入)將接收到的訊息加上前綴、取得目前日期時間,並組合為 INSERT INTO 的 SQL 語句。
UI Button (遠端開門按鈕)Dashboard 網頁上的控制按鈕,按下即發出開門動作訊號。
MQTT Out (Control Out)"open" 開門指令發布至 MQTT Broker 供 ESP32 接收。
Function (紀錄遠端開門)生成寫入 "RFID/CONTROL" 紀錄的 SQL 語句。
SQLite (mqtt.db 處理)執行 INSERT / CREATE TABLE 指令並連結至 SQLite 檔案 mqtt.db
Function & SQLite (查詢 30 筆)執行 SQL 查詢倒序取得 ID 前 30 筆的最新紀錄。
UI Table (歷史紀錄表)在網頁 Dashboard 上繪製並即時展示 4 個欄位 (ID, MQTT, Date, Time) 的動態表格。




DB Browser for SQLite (DB4S) 是一款開源免費的視覺化工具,專門用來建立、設計與編輯與 SQLite 相容的資料庫檔案(如專案中的 mqtt.db)。

主要功能特色

  • 資料庫與結構管理 (Database Structure)

    • 視覺化建表: 不需要輸入複雜的 SQL 指令,直接點選介面就能建立或修改資料表與欄位。

    • 欄位屬性設定: 輕鬆設定欄位型態(INTEGERTEXT 等)、主鍵(PRIMARY KEY)、自動遞增(AUTOINCREMENT)或預設值。

    • 索引與觸發器: 支援建立 Index 提升查詢速度,以及設定 Trigger 自動化邏輯。

  • 數據瀏覽與即時編輯 (Browse Data)

    • 類 Excel 操作: 以直觀的表格方式顯示資料,可直接雙擊儲存格修改數值。

    • 數據過濾與排序: 支援在各欄位頂部直接輸入關鍵字過濾,或點擊欄位標題進行升降冪排序。

    • 新增與刪除列: 點選按鈕即可手動新增或刪除資料筆數。

  • SQL 語法執行與測試 (Execute SQL)

    • 獨立 SQL 編輯器: 提供語法高亮與自動補全功能,方便撰寫並測試 SELECT 查詢指令。

    • 多分頁與歷史紀錄: 可同時開啟多個 SQL 分頁,並保存查詢過的歷史指令。

  • 資料匯入與匯出 (Import / Export)

    • CSV 格式支援: 支援將 SQLite 資料表匯出為 CSV 檔,或將現有的 CSV 檔案直接轉入為 SQLite 資料表。

    • SQL Dump 備份: 可將整個資料庫架構與數據資料匯出為文字格式的 .sql 備份檔。

  • 安全變更機制 (Write / Revert Changes)

    • 無痕預覽: 在介面上做的任何修改不會立即寫入硬碟。點擊「Write Changes」(儲存變更)才會正式寫入檔;若操作錯誤點擊「Revert Changes」即可安全還原。







Python程式

import sqlite3
import datetime
import tkinter as tk
from tkinter import ttk, messagebox
import paho.mqtt.client as mqtt

# ==========================
# 1. MQTT 設定
# ==========================
BROKER = "broker.emqx.io"
PORT = 1883

TOPIC_UID     = "alex9ufo/2026/RFID/RFID_UID"    # 接收卡號
TOPIC_PICC    = "alex9ufo/2026/RFID/RFID_PICC"   # 接收密碼/工作證/門鈴
TOPIC_CONTROL = "alex9ufo/2026/RFID/CONTROL"    # 發送開門指令

# ==========================
# 2. SQLite 資料庫管理類別
# ==========================
class DatabaseManager:
    def __init__(self, db_name="mqtt.db"):
        self.db_name = db_name
        self.init_db()

    def init_db(self):
        """ 初始化資料庫與資料表 """
        conn = sqlite3.connect(self.db_name)
        cursor = conn.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS rfid_logs (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                mqtt TEXT NOT NULL,
                date TEXT NOT NULL,
                time TEXT NOT NULL
            )
        ''')
        conn.commit()
        conn.close()

    def insert_log(self, message):
        """ 新增紀錄 """
        now = datetime.datetime.now()
        date_str = now.strftime("%Y-%m-%d")
        time_str = now.strftime("%H:%M:%S")

        conn = sqlite3.connect(self.db_name)
        cursor = conn.cursor()
        cursor.execute('''
            INSERT INTO rfid_logs (mqtt, date, time)
            VALUES (?, ?, ?)
        ''', (message, date_str, time_str))
        conn.commit()
        conn.close()

    def fetch_recent_30(self):
        """ 讀取最近 30 筆資料 (按 ID 倒序排列) """
        conn = sqlite3.connect(self.db_name)
        cursor = conn.cursor()
        cursor.execute('''
            SELECT id, mqtt, date, time FROM rfid_logs
            ORDER BY id DESC LIMIT 30
        ''')
        rows = cursor.fetchall()
        conn.close()
        return rows


# ==========================
# 3. GUI 主程式介面
# ==========================
class RFIDConsoleApp:
    def __init__(self, root):
        self.root = root
        self.root.title("RFID & MQTT 門禁控制中控台")
        self.root.geometry("680x520")  # 修正格式:不可有空格

        # 初始化 DB
        self.db = DatabaseManager("mqtt.db")

        # UI 元件建置
        self.create_widgets()

        # 初始化 MQTT Client
        self.client = mqtt.Client()
        self.client.on_connect = self.on_connect
        self.client.on_message = self.on_message

        try:
            self.client.connect_async(BROKER, PORT, 60)
            self.client.loop_start()
        except Exception as e:
            messagebox.showerror("MQTT 連線錯誤", f"無法連線至 Broker: {e}")

        # 初始載入資料庫內容
        self.refresh_table()

    def create_widgets(self):
        # 頂部狀態列與控制按鈕
        top_frame = tk.Frame(self.root, pady=10)
        top_frame.pack(fill=tk.X, padx=15)

        self.lbl_status = tk.Label(top_frame, text="MQTT 狀態: 連線中...", font=("Arial", 12), fg="orange")
        self.lbl_status.pack(side=tk.LEFT)

        btn_open = tk.Button(top_frame, text="🔓 遠端開門 (Open Door)", font=("Arial", 11, "bold"),
                             bg="#4CAF50", fg="white", padx=10, command=self.send_open_command)
        btn_open.pack(side=tk.RIGHT)

        # 表格區域 (顯示最近 30 筆)
        table_frame = tk.Frame(self.root)
        table_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=10)

        columns = ("id", "mqtt", "date", "time")
        self.tree = ttk.Treeview(table_frame, columns=columns, show="headings", height=15)

        # 設定表頭名稱
        self.tree.heading("id", text="ID (自動+1)")
        self.tree.heading("mqtt", text="MQTT 接收內容")
        self.tree.heading("date", text="日期 (Date)")
        self.tree.heading("time", text="時間 (Time)")

        # 設定欄位寬度與對齊
        self.tree.column("id", width=80, anchor=tk.CENTER)
        self.tree.column("mqtt", width=280, anchor=tk.W)
        self.tree.column("date", width=120, anchor=tk.CENTER)
        self.tree.column("time", width=120, anchor=tk.CENTER)

        # 垂直滾動條
        scrollbar = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscroll=scrollbar.set)

        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        # 底部說明列
        bottom_frame = tk.Frame(self.root, pady=5)
        bottom_frame.pack(fill=tk.X, padx=15)
        lbl_info = tk.Label(bottom_frame, text="註:資料庫檔名為 mqtt.db,表格會自動更新並維持顯示最近 30 筆歷史紀錄。",
                            font=("Arial", 9), fg="gray")
        lbl_info.pack(side=tk.LEFT)

    # ==========================
    # MQTT 邏輯處理
    # ==========================
    def on_connect(self, client, userdata, flags, rc):
        if rc == 0:
            self.lbl_status.config(text="MQTT 狀態: 已連線 (EMQX Broker)", fg="green")
            # 訂閱指定主題
            client.subscribe(TOPIC_UID)
            client.subscribe(TOPIC_PICC)
        else:
            self.lbl_status.config(text=f"MQTT 狀態: 連線失敗 (代碼 {rc})", fg="red")

    def on_message(self, client, userdata, msg):
        try:
            payload = msg.payload.decode("utf-8")
            topic = msg.topic
            
            # 格式化顯示訊息內容
            if topic == TOPIC_UID:
                formatted_msg = f"[UID] {payload}"
            elif topic == TOPIC_PICC:
                formatted_msg = f"[PICC/Keypad] {payload}"
            else:
                formatted_msg = f"[{topic}] {payload}"

            # 寫入 SQLite 資料庫
            self.db.insert_log(formatted_msg)

            # 在 GUI 主執行緒中更新表格
            self.root.after(0, self.refresh_table)

        except Exception as e:
            print(f"處理 MQTT 訊息錯誤: {e}")

    def send_open_command(self):
        """ 發送開門指令至 ESP32 並記錄至 SQLite """
        if self.client.is_connected():
            # 1. 發送 MQTT 指令至 ESP32
            self.client.publish(TOPIC_CONTROL, "open")
            
            # 2. 寫入 SQLite 資料庫 ( mqtt 欄位設為 "RFID/CONTROL" )
            self.db.insert_log("RFID/CONTROL")
            
            # 3. 即時刷新 GUI 30 筆歷史紀錄
            self.refresh_table()

            messagebox.showinfo("指令發送成功", "已發送 'open' 指令至 ESP32 並紀錄至資料庫!")
        else:
            messagebox.showwarning("連線失敗", "MQTT 目前未連線,無法發送指令!")

    def refresh_table(self):
        """ 清空並重新繪製最近 30 筆資料 """
        for item in self.tree.get_children():
            self.tree.delete(item)

        rows = self.db.fetch_recent_30()
        for row in rows:
            self.tree.insert("", tk.END, values=row)

    def on_closing(self):
        """ 關閉視窗處理 """
        self.client.loop_stop()
        self.client.disconnect()
        self.root.destroy()

# ==========================
# 4. 主程式啟動點
# ==========================
if __name__ == "__main__":
    root = tk.Tk()
    app = RFIDConsoleApp(root)
    root.protocol("WM_DELETE_WINDOW", app.on_closing)
    root.mainloop()

Python + Tkinter 視窗監控與控制程式主要實現了「MQTT 通訊監聽」「SQLite 本地資料庫自動儲存」「Tkinter GUI 視覺化表格呈現」以及「遠端開門指令觸發」四大功能。

一、 核心功能與運作流程

  1. MQTT 數據自動監聽與格式化:

    • 背景執行 paho-mqtt Client,連線至 broker.emqx.io:1883 免費 MQTT Broker。

    • 訂閱主題:

      • alex9ufo/2026/RFID/RFID_UID:接收 ESP32 傳來的刷卡卡號,自動加入 [UID] 前綴。

      • alex9ufo/2026/RFID/RFID_PICC:接收 Keypad 輸入密碼、工作證號或門鈴觸發訊息,自動加入 [PICC/Keypad] 前綴。

  2. SQLite 本地資料庫儲存 (mqtt.db):

    • 自動建立資料表: 程式啟動時會檢查並自動建立 rfid_logs 資料表(若檔案不存在則自動創建)。

    • 自動寫入機制: 當接收到 MQTT 訊息時,即時擷取系統當下的日期(date)與時間(time),寫入資料庫中。

    • 欄位結構:

      • id:主鍵,從 1 開始自動遞增(INTEGER PRIMARY KEY AUTOINCREMENT)。

      • mqtt:格式化後的 MQTT 訊息內容(如 [UID] AABBCCDDRFID/CONTROL)。

      • date:記錄日期(格式:YYYY-MM-DD)。

      • time:記錄時間(格式:HH:MM:SS)。

  3. Tkinter GUI 介面與最近 30 筆歷史呈現:

    • 使用 Tkinter 繪製 680x520 尺寸視窗,並附帶動態 Treeview 表格與垂直滾動條。

    • 每當有新資料寫入時,主程式會執行 SQL 查詢 SELECT ... ORDER BY id DESC LIMIT 30,將最新的 30 筆通行歷史紀錄即時更新繪製在視窗表格上。

  4. 遠端開門控制 (Remote Control):

    • 介面右上角提供 「🔓 遠端開門 (Open Door)」 按鈕。

    • 點擊按鈕時觸發兩項操作:

      1. 透過 MQTT 發送指令 "open" 至主題 alex9ufo/2026/RFID/CONTROL,遠端驅動 ESP32 Servo 馬達旋轉 90 度開門 3 秒。

      2. 在 SQLite 資料庫中寫入一筆 mqtt 欄位為 "RFID/CONTROL" 的操作紀錄,並即時刷新介面表格。

二、 程式類別與架構對照表

程式區塊 / 類別功能說明
DatabaseManager專責 SQLite 資料庫操作(建立 mqtt.db、寫入資料、讀取最近 30 筆紀錄)。
RFIDConsoleAppTkinter 主介面類別,負責 GUI 元件建置、MQTT 事件監聽與資料表格動態更新。
on_connect()MQTT 成功連線的回呼函數,負責訂閱 RFID_UIDRFID_PICC 主題。
on_message()接收 MQTT 訊息的回呼函數,負責解析 Payload、格式化字串並呼叫 DB 寫入與 GUI 刷新。
send_open_command()點擊「遠端開門」按鈕時觸發的函數,發送 open MQTT 指令並記錄 RFID/CONTROL 至 DB。
refresh_table()清空當前 Treeview 表格,並從 mqtt.db 讀取最近 30 筆最新資料填入表格中。






沒有留言:

張貼留言

2026-09 MQTT + wokwi esp32 + Node-red + Python (作業2)

2026-09  MQTT + wokwi esp32 +  Node-red + Python (作業2) 在這套 ESP32 門禁系統中,MQTT 主題(Topic)扮演著「訊息傳遞通道/地址」的角色。MQTT 採用發行/訂閱(Publish/Subscribe)架構,ESP...