2026年1月1日 星期四

Telegram +ESP32自動發報機

 

Telegram +ESP32自動發報機











這套系統是一個典型的 IoT(物聯網)架構,結合了遠端配置(Python)、通訊中介(MQTT)與硬體執行(ESP32)。

以下我為您拆解這兩支程式的核心運作原理:


一、 系統架構流程

  1. Python 端 (控制台):使用者輸入 Telegram 的金鑰 (Token) 與警報文字,透過 MQTT 發送到雲端。

  2. MQTT Broker (中轉站):負責在 Python 與 ESP32 之間傳遞訊息,兩者不需要在同一個區域網路。

  3. ESP32 端 (執行器)

    • 接收 MQTT 傳來的設定值。

    • 每 5 秒掃描一次硬體輸入(光耦)。

    • 若有觸發,執行輸出(Relay)並透過 Telegram API 發送通知。


二、 Python Tkinter 程式詳細解釋

這支程式的角色是 「遠端遙控器」

  • MQTT 連線與 retain=True: 程式使用 paho-mqtt 函式庫。在發送訊息時設置了 retain=True(保留訊息)。這非常重要,因為即使 ESP32 暫時斷電,重新連線後 MQTT Broker 會立刻把最後一次設定好的 Token 發送給 ESP32,不需要人手動重新設定。

  • 介面佈局 (GUI): 使用 tk.Entry 建立輸入框。程式將 6 個 Relay 的訊息儲存在一個列表(List)中,並透過迴圈產生介面,方便擴展與維護。

  • 非同步循環 loop_start(): 這讓 Python 介面在等待 MQTT 連線時不會「凍結」,使用者可以一邊修改文字,程式一邊在背景維持與 Server 的連線。


三、 ESP32 Wokwi 程式詳細解釋

這支程式的角色是 「感測與通訊終端」

  • 定時器邏輯 (millis): 程式沒有使用 delay(5000),而是使用 millis() 檢查時間差。

    • 為什麼? 如果用 delay,ESP32 在這 5 秒內會完全斷訊,無法接收 MQTT 的新訊息。使用 millis() 可以讓 MQTT 接收(mqttClient.loop())與硬體掃描並行。

  • 光耦輸入與 Relay 輸出

    • 光耦 (Photo Coupler):通常用於隔離高電壓雜訊,程式設定為 INPUT_PULLDOWN,代表預設是低電位,接收到信號時變為高電位。

    • Relay (繼電器):對應 6 個輸出,當輸入為 HIGH 時,digitalWrite(relayPins[i], HIGH) 點亮 LED 或驅動繼電器。

  • Telegram 通訊

    • 使用 WiFiClientSecure:因為 Telegram 的伺服器強制要求 HTTPS 加密連線。

    • setInsecure():在 Wokwi 模擬環境中,我們跳過複雜的憑證驗證,直接建立安全通道。

  • 動態初始化 bot 物件: 這是本程式最精巧的地方。ESP32 啟動時並沒有 Token,它會一直等到 MQTT 接收到 alex9ufo/Token 後,才在 callback 函式裡執行 new UniversalTelegramBot。這就是為什麼介面上設定完後,ESP32 的 Pin 2 LED 會亮起。

python tkinter 程式
1) 畫面顯示 連接到 broker.mqtt-dashboard.com (連上綠色)
2) 畫面顯示 主題: alex9ufo/Token 內定 payload:8022701986:AAGymymK9_d12HcTGJWle3mtqHmilxB64_5Zw  可以變更
3) 畫面顯示 主題: alex9ufo/chatID  內定 payload:749652818469   可以變更
4) 畫面顯示 主題: alex9ufo/relay1 內定 payload: 輸入<1> 被觸動   可以變更
5) 畫面顯示 主題: alex9ufo/relay2 內定 payload: 輸入<2> 被觸動   可以變更
6) 畫面顯示 主題: alex9ufo/relay3 內定 payload: 輸入<3> 被觸動   可以變更
7) 畫面顯示 主題: alex9ufo/relay4 內定 payload: 輸入<4> 被觸動   可以變更
8) 畫面顯示 主題: alex9ufo/relay5 內定 payload: 輸入<5> 被觸動   可以變更
9) 畫面顯示 主題: alex9ufo/relay6 內定 payload: 輸入<6> 被觸動   可以變更



input  phtot coupler pin 34,35,32,33,25,26
output relay pin    23,22,21,19,18,5


1) input  phtot coupler pin 34,35,32,33,25,26 , output relay pin 23,22,21,19,18,5  LED pin 2
2) 透過 mqtt  broker.mqtt-dashboard.com 接收主題 : alex9ufo/Token的內容 為telegram bot token 
   接收主題 : alex9ufo/chatID的內容 為telegram chat id 若成功 則 pin 2 輸出 hi LED亮
3) 檢查 input  phtot coupler pin 34,35,32,33,25,26  若是輸入為 hi 則對應 output relay pin    23,22,21,19,18,5 輸出Hi  並 透過 telegram  Token chatID , 輸出對應的內容 輸入<1> .....<6> 被觸動 到 telegram 上通知使用者 
 

Wokwi程式
#include <WiFi.h>
#include <PubSubClient.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>

// WiFi & MQTT 設定
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* mqtt_server = "broker.mqtt-dashboard.com";

// 引腳定義
const int inputPins[] = {34, 35, 32, 33, 25, 26};
const int relayPins[] = {23, 22, 21, 19, 18, 5};
const int ledPin = 2; // 系統狀態燈 (MQTT/Telegram 配置成功)

// 設定變數
String telegramToken = "";
String telegramChatID = "";
String relayMessages[6] = {"輸入<1> 被觸動", "輸入<2> 被觸動", "輸入<3> 被觸動",
                           "輸入<4> 被觸動", "輸入<5> 被觸動", "輸入<6> 被觸動"};
bool botInitialized = false;

WiFiClient espClient;
PubSubClient mqttClient(espClient);
WiFiClientSecure secured_client;
UniversalTelegramBot *bot = nullptr;

unsigned long lastCheckTime = 0;
const unsigned long checkInterval = 5000; // 5秒檢查一次

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  for (int i = 0; i < 6; i++) {
    pinMode(inputPins[i], INPUT_PULLDOWN);
    pinMode(relayPins[i], OUTPUT);
    digitalWrite(relayPins[i], LOW);
  }

  setup_wifi();
  mqttClient.setServer(mqtt_server, 1883);
  mqttClient.setCallback(callback);
}

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

void callback(char* topic, byte* payload, unsigned int length) {
  String msg = "";
  for (int i = 0; i < length; i++) msg += (char)payload[i];
  String strTopic = String(topic);

  if (strTopic == "alex9ufo/Token") telegramToken = msg;
  else if (strTopic == "alex9ufo/chatID") telegramChatID = msg;
  else {
    for (int i = 1; i <= 6; i++) {
      if (strTopic == "alex9ufo/relay" + String(i)) relayMessages[i-1] = msg;
    }
  }

  // 配置成功檢查
  if (telegramToken.length() > 20 && telegramChatID.length() > 5) {
    digitalWrite(ledPin, HIGH);
    secured_client.setInsecure();
    if (bot != nullptr) delete bot;
    bot = new UniversalTelegramBot(telegramToken, secured_client);
    botInitialized = true;
    Serial.println("Telegram Bot Initialized");
  }
}

void reconnect() {
  while (!mqttClient.connected()) {
    if (mqttClient.connect("ESP32_Alex_Monitor")) {
      mqttClient.subscribe("alex9ufo/Token");
      mqttClient.subscribe("alex9ufo/chatID");
      for(int i=1; i<=6; i++) mqttClient.subscribe(("alex9ufo/relay" + String(i)).c_str());
    } else { delay(5000); }
  }
}

void loop() {
  if (!mqttClient.connected()) reconnect();
  mqttClient.loop();

  // 每 5 秒執行一次檢查
  if (millis() - lastCheckTime > checkInterval) {
    lastCheckTime = millis();
    Serial.println("--- 5秒自動檢查中 ---");

    for (int i = 0; i < 6; i++) {
      if (digitalRead(inputPins[i]) == HIGH) {
        // 1. 點亮對應 Relay/LED
        digitalWrite(relayPins[i], HIGH);
        Serial.printf("Pin %d is HIGH. Relay %d ON.\n", inputPins[i], i+1);

        // 2. 如果 Telegram 已配置,發送對應訊息
        if (botInitialized) {
          if (bot->sendMessage(telegramChatID, relayMessages[i], "")) {
            Serial.println("Telegram sent: " + relayMessages[i]);
          } else {
            Serial.println("Telegram send failed.");
          }
        }
      } else {
        // 如果輸入為 LOW,則關閉 Relay
        digitalWrite(relayPins[i], LOW);
      }
    }
  }
}

Python程式

import tkinter as tk
from tkinter import messagebox
import paho.mqtt.client as mqtt

# MQTT 設定
MQTT_BROKER = "broker.mqtt-dashboard.com"
MQTT_PORT = 1883

class MqttGui:
    def __init__(self, root):
        self.root = root
        self.root.title("MQTT Telegram 遠端配置工具")
        self.root.geometry("550x680")

        # --- 連線狀態區域 ---
        self.status_frame = tk.Frame(root)
        self.status_frame.pack(pady=10)
        self.status_label = tk.Label(self.status_frame, text="MQTT 狀態: 嘗試中...", fg="black")
        self.status_label.pack(side=tk.LEFT)
        self.canvas = tk.Canvas(self.status_frame, width=20, height=20)
        self.status_light = self.canvas.create_oval(5, 5, 15, 15, fill="red")
        self.canvas.pack(side=tk.LEFT)

        # --- Telegram 基本設定 ---
        tk.Label(root, text="[Step 1] 設定 Telegram 憑證", font=('Arial', 10, 'bold')).pack(pady=5)
        
        tk.Label(root, text="主題: alex9ufo/Token").pack()
        self.token_entry = tk.Entry(root, width=60)
        self.token_entry.insert(0, "80122700986:AAGym1ymK9_rd1eHcTGJWl3mtqHmilxB64_5Zw")
        self.token_entry.pack()

        tk.Label(root, text="主題: alex9ufo/chatID").pack(pady=(5,0))
        self.chatid_entry = tk.Entry(root, width=60)
        self.chatid_entry.insert(0, "7916522184369")
        self.chatid_entry.pack()

        # 修正後的分隔線 (使用 Frame 代替 Separator)
        line = tk.Frame(root, height=2, bd=1, relief=tk.SUNKEN)
        line.pack(fill='x', padx=10, pady=15)

        # --- Relay 訊息設定 ---
        tk.Label(root, text="[Step 2] 設定觸發訊息 (可變更)", font=('Arial', 10, 'bold')).pack(pady=5)
        
        self.relay_entries = []
        for i in range(1, 7):
            frame = tk.Frame(root)
            frame.pack(pady=2, anchor="w", padx=40)
            tk.Label(frame, text=f"Relay {i} 訊息:", width=12, anchor="e").pack(side=tk.LEFT)
            entry = tk.Entry(frame, width=35)
            entry.insert(0, f"輸入<{i}> 被觸動")
            entry.pack(side=tk.LEFT, padx=5)
            self.relay_entries.append(entry)

        # --- 按鈕 ---
        self.send_btn = tk.Button(root, text="發送所有設定到 ESP32", 
                                  bg="#4CAF50", fg="white", font=('Arial', 12, 'bold'),
                                  command=self.publish_all_config)
        self.send_btn.pack(pady=25)

        # 初始化 MQTT
        self.client = mqtt.Client()
        self.client.on_connect = self.on_connect
        try:
            # 這裡增加了一個 try-except 避免沒網路時程式崩潰
            self.client.connect(MQTT_BROKER, MQTT_PORT, 60)
            self.client.loop_start()
        except Exception as e:
            print(f"Connect error: {e}")

    def on_connect(self, client, userdata, flags, rc):
        if rc == 0:
            self.status_label.config(text="MQTT 狀態: 已連線", fg="green")
            self.canvas.itemconfig(self.status_light, fill="green")
        else:
            self.status_label.config(text="MQTT 狀態: 連線失敗", fg="red")

    def publish_all_config(self):
        try:
            # 發送 Token 與 ID (保留 retain 讓 ESP32 斷線回來也能抓到)
            self.client.publish("alex9ufo/Token", self.token_entry.get(), retain=True)
            self.client.publish("alex9ufo/chatID", self.chatid_entry.get(), retain=True)
            
            # 發送 6 個 Relay 的自定義訊息
            for i, entry in enumerate(self.relay_entries):
                topic = f"alex9ufo/relay{i+1}"
                self.client.publish(topic, entry.get(), retain=True)
                
            messagebox.showinfo("發送成功", "所有設定已透過 MQTT 更新!")
        except Exception as e:
            messagebox.showerror("錯誤", f"發送失敗: {e}")

if __name__ == "__main__":
    root = tk.Tk()
    app = MqttGui(root)
    root.mainloop()



2025年12月31日 星期三

Telegram 送出命令 -->Node-Red+ SQLite -->MQTT -->WOKWI ESP32 RFID + LED

  Telegram 送出命令 -->Node-Red+ SQLite -->MQTT -->WOKWI ESP32 RFID + LED








wokwi程式

#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/rfid/led";
const char* TOPIC_LED_STATUS = "alex9ufo/rfid/ledStatus";

// 硬體物件
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);
}

Node-Red程式

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

Node-RED安裝與啟動 與 節點安裝 範例


Node-Red程式

[{"id":"ca7e01ca8654162f","type":"telegram receiver","z":"448157ebc5541371","name":"","bot":"457874f8aa8a857a","saveDataDir":"","filterCommands":true,"x":110,"y":40,"wires":[["9e865f658c3f6af9"],[]]},{"id":"dc882fef65c76133","type":"function","z":"448157ebc5541371","name":"根據指令發佈 MQTT 訊息","func":"if (msg.payload === \"/on\") {\n    msg.topic = \"alex9ufo/rfid/led\";\n    msg.payload = \"on\";  // 開啟綠色 LED\n} else if (msg.payload === \"/off\") {\n    msg.topic = \"alex9ufo/rfid/led\";\n    msg.payload = \"off\"; // 關閉 LED\n} else if (msg.payload === \"/flash\") {\n    msg.topic = \"alex9ufo/rfid/led\";\n    msg.payload = \"flash\"; // LED 交替閃爍\n} else if (msg.payload === \"/timer\") {\n    msg.topic = \"alex9ufo/rfid/led\";\n    msg.payload = \"timer\"; // LED 定時 10 秒後關閉\n}\nreturn msg;\n//function 根據指令來發佈 MQTT 訊息","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":40,"wires":[["7ec09b4c30e9b6cf"]]},{"id":"7ec09b4c30e9b6cf","type":"mqtt out","z":"448157ebc5541371","name":"","topic":"alex9ufo/rfid/led","qos":"1","retain":"true","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"b9efc827e98bf7f9","x":680,"y":40,"wires":[]},{"id":"13a8d28ef336440f","type":"mqtt in","z":"448157ebc5541371","name":"","topic":"alex9ufo/rfid/led","qos":"2","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":100,"y":160,"wires":[["6610796333c1f034","67311733e039e4b1","e9e6a74a585f3b6c"]]},{"id":"9e865f658c3f6af9","type":"function","z":"448157ebc5541371","name":"function","func":"var content= msg.payload.content;\nmsg.payload=content;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":40,"wires":[["dc882fef65c76133"]]},{"id":"2adb7ad76def92ca","type":"ui_led","z":"448157ebc5541371","order":1,"group":"5ed27e92f756bcf4","width":3,"height":2,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"","x":750,"y":160,"wires":[]},{"id":"6610796333c1f034","type":"function","z":"448157ebc5541371","name":"function","func":"var content = msg.payload;\n\nif (content === 'on') {\n    msg.payload = true;\n    // Route to output 1\n    return [msg, null, null, null];\n}\n\nif (content === 'off') {\n    msg.payload = false;\n    // Route to output 2\n    return [null, msg, null, null];\n}\n\nif (content === 'flash') {\n    msg.payload = true;\n    // Route to output 3\n    return [null, null, msg, null];\n}\n\nif (content === 'timer') {\n    msg.payload = true;\n    // Route to output 4\n    return [null, null, null, msg]; // Changed to route 'timer' to the 4th output\n}\n\n// Optional: If none of the conditions are met, you can return 'null' to discard the message\n// or return [msg] to pass the original message through the first output.\n// For this example, we'll return null to stop the message.\nreturn null;","outputs":4,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":180,"wires":[["2adb7ad76def92ca","48b63713aaa717b1"],["2adb7ad76def92ca","48b63713aaa717b1"],["8e4c7be415272248","48b63713aaa717b1"],["b340bf7e95798e33","2adb7ad76def92ca","48b63713aaa717b1"]]},{"id":"b340bf7e95798e33","type":"delay","z":"448157ebc5541371","name":"","pauseType":"delay","timeout":"10","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":400,"y":320,"wires":[["3f72babf6b7bde78"]]},{"id":"8e4c7be415272248","type":"delay","z":"448157ebc5541371","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":450,"y":220,"wires":[["2adb7ad76def92ca","7c61b065db9a43b5"]]},{"id":"3f72babf6b7bde78","type":"function","z":"448157ebc5541371","name":"function","func":"msg.payload=false;\nreturn msg;\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":540,"y":320,"wires":[["2adb7ad76def92ca"]]},{"id":"48b63713aaa717b1","type":"debug","z":"448157ebc5541371","name":"debug 363","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":510,"y":140,"wires":[]},{"id":"ce3cb5da10fc8fde","type":"function","z":"448157ebc5541371","name":"function","func":"msg.payload = !msg.payload;\n\nreturn msg;\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":540,"y":280,"wires":[["8e4c7be415272248"]]},{"id":"66070cb0acfb18ce","type":"mqtt in","z":"448157ebc5541371","name":"ledStatus","topic":"alex9ufo/rfid/ledStatus","qos":"1","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":80,"y":420,"wires":[["a2a05e599a1c2803"]]},{"id":"a2a05e599a1c2803","type":"function","z":"448157ebc5541371","name":"根據指令發佈 MQTT 訊息","func":"\nif (msg.payload === \"ON\") {\n    msg.payload = \"LED Status: on\";  // 開啟綠色 LED\n} else if (msg.payload === \"OFF\") {\n    msg.payload = \"LED Status: off\"; // 關閉 LED\n} else if (msg.payload === \"FLASH\") {\n    msg.payload = \"LED Status: flash\"; // LED 交替閃爍\n} else if (msg.payload === \"TIMER\") {\n    msg.payload = \"LED Status: timer\"; // LED 定時 10 秒後關閉\n}\nreturn msg;\n//function 根據指令來發佈 MQTT 訊息","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":420,"wires":[["cf9ed47acf13328f","ccd143a24325d0cb"]]},{"id":"3b246f02576fb2c4","type":"telegram sender","z":"448157ebc5541371","name":"","bot":"457874f8aa8a857a","haserroroutput":false,"outputs":1,"x":710,"y":420,"wires":[[]]},{"id":"8623eccb1262afa5","type":"mqtt out","z":"448157ebc5541371","name":"ledStatus","topic":"alex9ufo/rfid/ledStatus","qos":"1","retain":"true","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"b9efc827e98bf7f9","x":540,"y":360,"wires":[]},{"id":"67311733e039e4b1","type":"function","z":"448157ebc5541371","name":"根據指令發佈 MQTT 訊息","func":"if (msg.payload === \"on\") {\n    msg.topic = \"alex9ufo/rfid/ledStatus\";\n    msg.payload = \"ON\";  // 開啟綠色 LED\n} else if (msg.payload === \"off\") {\n    msg.topic = \"alex9ufo/rfid/ledStatus\";\n    msg.payload = \"OFF\"; // 關閉 LED\n} else if (msg.payload === \"flash\") {\n    msg.topic = \"alex9ufo/rfid/ledStatus\";\n    msg.payload = \"FLASH\"; // LED 交替閃爍\n} else if (msg.payload === \"timer\") {\n    msg.topic = \"alex9ufo/rfid/ledStatus\";\n    msg.payload = \"TIMER\"; // LED 定時 10 秒後關閉\n}\nreturn msg;\n//function 根據指令來發佈 MQTT 訊息","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":360,"wires":[["8623eccb1262afa5"]]},{"id":"cf9ed47acf13328f","type":"debug","z":"448157ebc5541371","name":"debug 365","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":490,"y":460,"wires":[]},{"id":"ccd143a24325d0cb","type":"template","z":"448157ebc5541371","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\"chatId\": 7965218469,\n\"type\":\"message\",\n\"content\":\"{{payload}}\"}","output":"json","x":550,"y":420,"wires":[["3b246f02576fb2c4"]]},{"id":"e9e6a74a585f3b6c","type":"function","z":"448157ebc5541371","name":"global.set","func":"var ledstatus = msg.payload;\n\n// 2. 將值設定為 Global 變數\nglobal.set(\"myledStatus\", ledstatus);\n\n// 3. 返回訊息,以便流程繼續\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":120,"wires":[[]]},{"id":"7c61b065db9a43b5","type":"function","z":"448157ebc5541371","name":"global.get","func":"// 1. 取得 Global 變數的值\n//global.set(\"myledStatus\", ledstatus);\nvar myValue = global.get(\"myledStatus\");\nif (myValue=='flash'){\n    return [msg,null];\n    }\nelse    \n    return [true,true];","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":280,"wires":[["ce3cb5da10fc8fde"],[]]},{"id":"a100deb0624e7c6b","type":"sqlite","z":"448157ebc5541371","mydb":"04085aab75f5ad41","sqlquery":"msg.topic","sql":"","name":"RFID","x":530,"y":780,"wires":[["1184a6ff8873cab5"]]},{"id":"ccec8a03b3ed4d2e","type":"function","z":"448157ebc5541371","name":"組合刪除一筆 SQL","func":"var id = flow.get('delete_id') || msg.payload;\nif(!id) return null;\nmsg.topic = `DELETE FROM rfid_logs WHERE id= $id`;\nmsg.payload = [id];\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":270,"y":700,"wires":[["640e917b338f3959","062d62b2355724f6"]]},{"id":"717f1b8d1458c3d6","type":"function","z":"448157ebc5541371","name":"組合新增 SQL","func":"var memo, LEDorRFID ;\n\nif (msg.payload === \"ON\") {\n    LEDorRFID = \"ON\";\n    memo = \"LED Status: on\";  // 開啟綠色 LED\n\n} else if (msg.payload === \"OFF\") {\n    LEDorRFID = \"OFF\";\n    memo = \"LED Status: off\"; // 關閉 LED\n} else if (msg.payload === \"FLASH\") {\n    LEDorRFID = \"FLASH\";\n    memo = \"LED Status: flash\"; // LED 交替閃爍\n} else if (msg.payload === \"TIMER\") {\n    LEDorRFID = \"TIMER\";\n    memo = \"LED Status: timer\"; // LED 定時 10 秒後關閉\n}\n\n\n\n// 建立一個 Date 物件,它包含運行 Node-RED 的電腦的當前時間\nvar now = new Date();\n// --- 提取日期時間的不同格式 ---\n// 1. 完整的 ISO 格式 (例如: 2025-10-25T03:28:08.500Z)\nvar isoString = now.toISOString();\n\n// 2. YYYY-MM-DD (日期部分)\n// slice(0, 10) 會擷取 ISO 字串的前 10 個字元\nvar dateOnly = now.toISOString().slice(0, 10); \n\n// 3. HH:MM:SS (時間部分)\n// toTimeString() 輸出類似 \"11:28:08 GMT+0800 (CST)\" 的字串\n// slice(0, 8) 會擷取開頭的 HH:MM:SS\nvar timeOnly = now.toTimeString().slice(0, 8);\n\n\nmsg.topic = \"INSERT INTO rfid_logs (date, time ,LEDorRFID,memo ) VALUES ($dateOnly, $timeOnly ,$LEDorRFID,$memo)\";\nmsg.payload = [dateOnly, timeOnly, LEDorRFID, memo]\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":260,"y":640,"wires":[["c6538f6c363c5177"]]},{"id":"c6538f6c363c5177","type":"sqlite","z":"448157ebc5541371","mydb":"04085aab75f5ad41","sqlquery":"msg.topic","sql":"","name":"RFID","x":430,"y":640,"wires":[["4077e1c59441a301","062d62b2355724f6"]]},{"id":"4077e1c59441a301","type":"debug","z":"448157ebc5541371","name":"debug ","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":590,"y":640,"wires":[]},{"id":"267b3bad3a0b05c1","type":"debug","z":"448157ebc5541371","name":"debug ","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":590,"y":700,"wires":[]},{"id":"640e917b338f3959","type":"sqlite","z":"448157ebc5541371","mydb":"04085aab75f5ad41","sqlquery":"msg.topic","sql":"","name":"RFID","x":450,"y":700,"wires":[["267b3bad3a0b05c1","062d62b2355724f6"]]},{"id":"b8ff3277ea021729","type":"ui_button","z":"448157ebc5541371","name":"刪除一筆","group":"729b8cad8d9aa5ca","order":6,"width":3,"height":1,"passthru":false,"label":"刪除一筆","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"str","topic":"","topicType":"str","x":80,"y":700,"wires":[["ccec8a03b3ed4d2e"]]},{"id":"1223aa8e351649aa","type":"ui_button","z":"448157ebc5541371","name":"顯示最新50筆","group":"729b8cad8d9aa5ca","order":3,"width":3,"height":1,"passthru":false,"label":"顯示全部設定","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"str","topic":"","topicType":"str","x":100,"y":780,"wires":[["062d62b2355724f6"]]},{"id":"062d62b2355724f6","type":"function","z":"448157ebc5541371","name":"組合顯示所有SQL","func":"\nmsg.topic = \"SELECT id, date, time, LEDorRFID, memo FROM rfid_logs ORDER BY id DESC LIMIT 50\";\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":780,"wires":[["a100deb0624e7c6b"]]},{"id":"1184a6ff8873cab5","type":"ui_table","z":"448157ebc5541371","group":"729b8cad8d9aa5ca","name":"設定資料表","order":7,"width":9,"height":9,"columns":[{"field":"id","title":"ID","width":"15%","align":"left","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"date","title":"星期","width":"20%","align":"left","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"time","title":"時間","width":"20%","align":"left","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"LEDorRFID","title":"LED或RFID","width":"30%","align":"left","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"memo","title":"備註","width":"30%","align":"left","formatter":"plaintext","formatterParams":{"target":"_blank"}}],"outputs":0,"cts":false,"x":730,"y":780,"wires":[]},{"id":"9d44b2fe693cc821","type":"ui_dropdown","z":"448157ebc5541371","name":"模式選擇","label":"控制模式","tooltip":"","place":"","group":"5ed27e92f756bcf4","order":4,"width":6,"height":1,"passthru":true,"multiple":false,"options":[{"label":"新增","value":"create","type":"str"},{"label":"比對","value":"verify","type":"str"}],"payload":"","topic":"mode","topicType":"str","className":"","x":80,"y":840,"wires":[["104303cbaa7163f4"]]},{"id":"104303cbaa7163f4","type":"function","z":"448157ebc5541371","name":"暫存模式","func":"flow.set('mode', msg.payload);\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":260,"y":840,"wires":[["6a18874cf8db9321"]]},{"id":"6a18874cf8db9321","type":"debug","z":"448157ebc5541371","name":"debug  ","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":410,"y":840,"wires":[]},{"id":"61290d7a37f46f8a","type":"inject","z":"448157ebc5541371","name":"建立資料庫","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":100,"y":900,"wires":[["db16ac459547969f"]]},{"id":"db16ac459547969f","type":"function","z":"448157ebc5541371","name":"建立資料庫 SQL","func":"msg.topic = `CREATE TABLE IF NOT EXISTS rfid_logs (   id INTEGER PRIMARY KEY AUTOINCREMENT,  \n                date TEXT NOT NULL,\n                time TEXT NOT NULL,\n                LEDorRFID TEXT,\n                memo TEXT )`;\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":900,"wires":[["75209616c5b3e25c"]]},{"id":"75209616c5b3e25c","type":"sqlite","z":"448157ebc5541371","mydb":"04085aab75f5ad41","sqlquery":"msg.topic","sql":"","name":"RFID","x":430,"y":900,"wires":[["996edd23a89ff222"]]},{"id":"996edd23a89ff222","type":"debug","z":"448157ebc5541371","name":"debug  ","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":550,"y":900,"wires":[]},{"id":"a4700ec6830e4740","type":"ui_button","z":"448157ebc5541371","name":"建立資料庫","group":"729b8cad8d9aa5ca","order":1,"width":3,"height":1,"passthru":false,"label":"建立資料庫","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"str","topic":"","topicType":"str","x":90,"y":940,"wires":[["db16ac459547969f"]]},{"id":"0b06ebd430fedead","type":"ui_button","z":"448157ebc5541371","name":"查詢一筆","group":"729b8cad8d9aa5ca","order":5,"width":3,"height":1,"passthru":false,"label":"查詢一筆","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"str","topic":"","topicType":"str","x":80,"y":560,"wires":[["22050570edbba3d8"]]},{"id":"22050570edbba3d8","type":"function","z":"448157ebc5541371","name":"組合查詢一筆 SQL","func":"var id = flow.get('delete_id') || msg.payload;\nif(!id) return null;\nmsg.topic = `SELECT id, date, time, LEDorRFID, memo FROM rfid_logs  WHERE id= $id`;\nmsg.payload = [id];\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":270,"y":560,"wires":[["8f85e8703e7b2348"]]},{"id":"8f85e8703e7b2348","type":"sqlite","z":"448157ebc5541371","mydb":"04085aab75f5ad41","sqlquery":"msg.topic","sql":"","name":"RFID","x":430,"y":560,"wires":[["1184a6ff8873cab5"]]},{"id":"8111ca8ecf61f32d","type":"mqtt in","z":"448157ebc5541371","name":"ledStatus","topic":"alex9ufo/rfid/ledStatus","qos":"1","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":80,"y":640,"wires":[["717f1b8d1458c3d6"]]},{"id":"4035c5171ccf55a1","type":"function","z":"448157ebc5541371","name":"function   flow.set","func":"var idno =msg.payload;\nflow.set('delete_id',idno)\nmsg.payload= idno;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":260,"y":500,"wires":[["5f9fe616a7cef33e"]]},{"id":"da635fbb00a8997c","type":"ui_button","z":"448157ebc5541371","name":"刪除資料庫","group":"729b8cad8d9aa5ca","order":2,"width":3,"height":1,"passthru":false,"label":"刪除資料庫","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"str","topic":"","topicType":"str","x":90,"y":980,"wires":[["ca9c2e1244cf72f3"]]},{"id":"ca9c2e1244cf72f3","type":"function","z":"448157ebc5541371","name":"刪除資料庫 SQL","func":"msg.topic = `DROP TABLE IF EXISTS rfid_logs`;\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":980,"wires":[["75209616c5b3e25c"]]},{"id":"5f9fe616a7cef33e","type":"debug","z":"448157ebc5541371","name":"debug 368","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":450,"y":500,"wires":[]},{"id":"bfca293065dbd0c9","type":"ui_numeric","z":"448157ebc5541371","name":"","label":"ID","tooltip":"","group":"729b8cad8d9aa5ca","order":4,"width":3,"height":1,"wrap":false,"passthru":true,"topic":"topic","topicType":"msg","format":"{{value}}","min":0,"max":"999999","step":1,"className":"","x":70,"y":500,"wires":[["4035c5171ccf55a1"]]},{"id":"85be98c91e459c6f","type":"telegram sender","z":"448157ebc5541371","name":"","bot":"457874f8aa8a857a","haserroroutput":false,"outputs":1,"x":1010,"y":1180,"wires":[[]]},{"id":"9e6f82181faf4bba","type":"inject","z":"448157ebc5541371","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":100,"y":1180,"wires":[["322c7cefeb6da2ef"]]},{"id":"256f3431cc5d9a01","type":"template","z":"448157ebc5541371","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\"chatId\": 7965218469,\n\"type\":\"message\",\n\"Parse Mode\": \"Markdown\",\n\"content\":\"{{payload}}\"}\n","output":"json","x":810,"y":1180,"wires":[["85be98c91e459c6f"]]},{"id":"322c7cefeb6da2ef","type":"function","z":"448157ebc5541371","name":"啟動時發送 Help 訊息給Telegram","func":"msg.payload = `啟動時發送 Help 訊息給 Telegram\n🎉 程式已啟動!\n\n可用命令: (所有命令前方都加上 斜線)\non - 開啟 LED (綠色) \noff - 關閉 LED (紅色)\nflash - LED 閃爍 (紅綠交替)\ntimer - LED 開啟 10 秒後自動關閉\nmode - 切換模式 (新增/比對)\nstatus - 顯示當前模式及最新記錄`;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":1180,"wires":[["256f3431cc5d9a01"]]},{"id":"22dbd6f0dba2137a","type":"mqtt in","z":"448157ebc5541371","name":"","topic":"alex9ufo/rfid/UID","qos":"1","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":100,"y":1100,"wires":[["be52afa039a5e931","f94d2439fd60b7d7"]]},{"id":"be52afa039a5e931","type":"function","z":"448157ebc5541371","name":"[新增]卡號號碼","func":"var mode = flow.get('mode');\nflow.set('uid', msg.payload);\nvar uid=msg.payload;\nif (mode=='create'){\n    msg.payload= '[新增]卡號號碼'+ uid;\n    return msg;\n}\nreturn null;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":1100,"wires":[["256f3431cc5d9a01","5eab10d3417ccc67","51bd7abf40e987f8"]]},{"id":"f1436aa08fb9f73e","type":"sqlite","z":"448157ebc5541371","mydb":"04085aab75f5ad41","sqlquery":"msg.topic","sql":"","name":"RFID","x":690,"y":1100,"wires":[["c6f6859106efe540"]]},{"id":"5eab10d3417ccc67","type":"function","z":"448157ebc5541371","name":"組合新增 SQL","func":"var memo, LEDorRFID ;\nLEDorRFID = flow.get('uid');\nmemo =msg.payload;\n\n// 建立一個 Date 物件,它包含運行 Node-RED 的電腦的當前時間\nvar now = new Date();\n// --- 提取日期時間的不同格式 ---\n// 1. 完整的 ISO 格式 (例如: 2025-10-25T03:28:08.500Z)\nvar isoString = now.toISOString();\n\n// 2. YYYY-MM-DD (日期部分)\n// slice(0, 10) 會擷取 ISO 字串的前 10 個字元\nvar dateOnly = now.toISOString().slice(0, 10); \n\n// 3. HH:MM:SS (時間部分)\n// toTimeString() 輸出類似 \"11:28:08 GMT+0800 (CST)\" 的字串\n// slice(0, 8) 會擷取開頭的 HH:MM:SS\nvar timeOnly = now.toTimeString().slice(0, 8);\n\n\nmsg.topic = \"INSERT INTO rfid_logs (date, time ,LEDorRFID,memo ) VALUES ($dateOnly, $timeOnly ,$LEDorRFID,$memo)\";\nmsg.payload = [dateOnly, timeOnly, LEDorRFID, memo]\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":500,"y":1100,"wires":[["f1436aa08fb9f73e"]]},{"id":"c6f6859106efe540","type":"link out","z":"448157ebc5541371","name":"link out 78","mode":"link","links":["0a89297ad75019f9"],"x":795,"y":1100,"wires":[]},{"id":"0a89297ad75019f9","type":"link in","z":"448157ebc5541371","name":"link in 72","links":["c6f6859106efe540"],"x":235,"y":800,"wires":[["062d62b2355724f6"]]},{"id":"51bd7abf40e987f8","type":"ui_text","z":"448157ebc5541371","group":"5ed27e92f756bcf4","order":5,"width":0,"height":0,"name":"","label":"","format":"{{msg.payload}}","layout":"row-left","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":470,"y":1060,"wires":[]},{"id":"b8a8f3e9e8dc1b42","type":"inject","z":"448157ebc5541371","name":"查詢所有 UID","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":110,"y":1300,"wires":[["f94d2439fd60b7d7"]]},{"id":"f94d2439fd60b7d7","type":"function","z":"448157ebc5541371","name":"建立 SQL 查詢","func":"// Function Name: Get All Stored UIDs Query Generator\nvar mode = flow.get('mode');\nvar uid = msg.payload;\nflow.set('uid', msg.payload);\n\nif (mode == 'verify') {\n    msg.topic = \"SELECT * FROM rfid_logs WHERE LEDorRFID = $uid LIMIT 1\";\n    msg.payload = [uid]\n}\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":300,"y":1300,"wires":[["6e0a917b63964b1e"]]},{"id":"6e0a917b63964b1e","type":"sqlite","z":"448157ebc5541371","mydb":"04085aab75f5ad41","sqlquery":"msg.topic","sql":"","name":"查詢 rfid_logs","x":480,"y":1300,"wires":[["9052d8c7127959db","d245523071a2381a"]]},{"id":"a41300479afbd0a2","type":"debug","z":"448157ebc5541371","name":"debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":790,"y":1380,"wires":[]},{"id":"1b225619fab2f4db","type":"ui_text","z":"448157ebc5541371","group":"5ed27e92f756bcf4","order":5,"width":0,"height":0,"name":"","label":"","format":"{{msg.payload}}","layout":"row-left","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":1050,"y":1240,"wires":[]},{"id":"9052d8c7127959db","type":"function","z":"448157ebc5541371","name":"(Result Processor)","func":"// Function Name: Convert DB Results to UID List\n\nvar dbResults = msg.payload;\nvar uidList = [];\n\nif (Array.isArray(dbResults)) {\n    // 遍歷結果陣列,提取每個物件的 LEDorRFID 值\n    uidList = dbResults.map(function (row) {\n        // 確保 row[0] 或 row.LEDorRFID 存在\n        if (row && row.LEDorRFID !== undefined) {\n            return row.LEDorRFID;\n        }\n        return null;\n    }).filter(function (uid) {\n        // 過濾掉 null 值 (雖然在 SQL 查詢中 LEDorRFID 應該不會是 NULL)\n        return uid !== null;\n    });\n} else {\n    // 處理非預期的錯誤\n    node.error(\"SQLite node did not return an array. Check DB connection.\", msg);\n    // 返回空列表\n    uidList = [];\n}\n\n// 將轉換後的 UID 列表存入 msg.payload\nmsg.payload = uidList;\n\n// 可以在 msg.status 中儲存操作狀態 (模擬 Python 的 True, results)\nmsg.status = (Array.isArray(dbResults));\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":670,"y":1300,"wires":[["a41300479afbd0a2","02d3a50734c8ba53"]]},{"id":"02d3a50734c8ba53","type":"function","z":"448157ebc5541371","name":"Status Checker","func":"// Function Name: Status Checker and Formatter (已修正)\n// 假設要比對的 UID 儲存在 msg.uid 中\n\nvar dbResults = msg.payload;\nvar outputUid = flow.get('uid');\n// 1. 檢查 msg.payload 是否為非空陣列\nif (Array.isArray(dbResults) && dbResults.length > 0) {\n    // 成功:找到至少一筆匹配的記錄\n    msg.payload = '比對正確 卡號:' + outputUid;\n\n    // 輸出到第一個埠 (Port 1: 成功)\n    return [msg, null]; \n    \n} else {\n    // 失敗:未找到匹配的記錄 (msg.payload 為空陣列或其他非陣列/空值)\n    \n    msg.payload = '比對錯誤';\n\n    // 輸出到第二個埠 (Port 2: 錯誤)\n    // 如果 Function 節點只有一個輸出埠,請使用 return [msg];\n    return [null, msg]; \n}\n\n// 注意:請確保此 Function 節點有兩個輸出埠 (Port 1 和 Port 2)","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":860,"y":1280,"wires":[["1b225619fab2f4db","256f3431cc5d9a01"],["1b225619fab2f4db","256f3431cc5d9a01"]]},{"id":"d245523071a2381a","type":"debug","z":"448157ebc5541371","name":"debug  ","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":650,"y":1260,"wires":[]},{"id":"457874f8aa8a857a","type":"telegram bot","botname":"@alextest999_bot","usernames":"","chatids":"7965218469","baseapiurl":"","testenvironment":false,"updatemode":"polling","pollinterval":"300","usesocks":false,"sockshost":"","socksprotocol":"socks5","socksport":"6667","socksusername":"anonymous","sockspassword":"","bothost":"","botpath":"","localbothost":"0.0.0.0","localbotport":"8443","publicbotport":"8443","privatekey":"","certificate":"","useselfsignedcertificate":false,"sslterminated":false,"verboselogging":false},{"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":"5ed27e92f756bcf4","type":"ui_group","name":"控制台","tab":"955b4de83b0e88a3","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"04085aab75f5ad41","type":"sqlitedb","db":"rfid_node-red.db","mode":"RWC"},{"id":"729b8cad8d9aa5ca","type":"ui_group","name":"資料表","tab":"955b4de83b0e88a3","order":2,"disp":true,"width":9,"collapse":false,"className":""},{"id":"955b4de83b0e88a3","type":"ui_tab","name":"RFID 控制","icon":"dashboard","order":1}]

Telegram 送出命令 --> Python +TKinter -->MQTT -->WOKWI ESP32 RFID + LED

 Telegram 送出命令 --> Python +TKinter -->MQTT -->WOKWI ESP32 RFID + LED


// 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/rfid/led";
const char* TOPIC_LED_STATUS = "alex9ufo/rfid/ledStatus";






CREATE TABLE "rfid_logs" (

"id" INTEGER,

"date" TEXT NOT NULL,

"time" TEXT NOT NULL,

"LEDorRFID" TEXT,

"memo" TEXT,

PRIMARY KEY("id" AUTOINCREMENT)

);





wokwi 程式

#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/rfid/led";
const char* TOPIC_LED_STATUS = "alex9ufo/rfid/ledStatus";

// 硬體物件
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);
}

Python 程式

import tkinter as tk

from tkinter import messagebox

import paho.mqtt.client as mqtt

import threading

import time

import asyncio

import sqlite3

import winsound

import sys

from datetime import datetime

from telegram import Update

from telegram.ext import Application, CommandHandler, ContextTypes


# --- 1. 全域設定 ---

DB_NAME = "rfid115.db"

MQTT_BROKER = "broker.mqtt-dashboard.com"

MQTT_PORT = 1883

TOPIC_RFID_UID = "alex9ufo/rfid/UID"

TOPIC_LED_CONTROL = "alex9ufo/rfid/led"

TOPIC_LED_STATUS = "alex9ufo/rfid/ledStatus"


# Telegram 設定

TELEGRAM_BOT_TOKEN = "8022700986:AAGymymK9_d1HcTGJWl3mtqHmilxB64_5Zw"

TARGET_CHAT_ID = 7965218469


MODE_ADD = "新增模式"

MODE_COMPARE = "比對模式"


# --- 2. 資料庫操作 ---

class DatabaseManager:

    def __init__(self, db_name):

        self.db_name = db_name

        self.create_table()

    

    def connect(self):

        return sqlite3.connect(self.db_name)


    def create_table(self):

        conn = self.connect()

        conn.cursor().execute("""

            CREATE TABLE IF NOT EXISTS rfid_logs (

                id INTEGER PRIMARY KEY AUTOINCREMENT,

                date TEXT NOT NULL,

                time TEXT NOT NULL,

                LEDorRFID TEXT, 

                memo TEXT

            )

        """)

        conn.commit()

        conn.close()


    def add_log(self, led_or_rfid, memo):

        conn = self.connect()

        try:

            now = datetime.now()

            conn.cursor().execute(

                "INSERT INTO rfid_logs (date, time, LEDorRFID, memo) VALUES (?, ?, ?, ?)",

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

            )

            conn.commit()

        except Exception as e:

            print(f"資料庫錯誤: {e}")

        finally:

            conn.close()


    def get_latest_logs(self):

        conn = self.connect()

        res = conn.cursor().execute("SELECT * FROM rfid_logs ORDER BY id DESC LIMIT 50").fetchall()

        conn.close()

        return res


    def get_all_rfid_uids(self):

        conn = self.connect()

        res = conn.cursor().execute("SELECT DISTINCT LEDorRFID FROM rfid_logs WHERE memo LIKE '[新增]%'").fetchall()

        conn.close()

        return [row[0] for row in res if row[0]]


# --- 3. Telegram Bot 處理 ---

class TelegramBotHandler:

    def __init__(self, app, token):

        self.app = app

        self.token = token

        self.tg_loop = None 

        self.application = Application.builder().token(token).build()

        

        for cmd in ["on", "off", "flash", "timer"]:

            self.application.add_handler(CommandHandler(cmd, self.handle_commands))

        

        self.bot_thread = threading.Thread(target=self._run_bot, daemon=True)


    def start_bot(self):

        self.bot_thread.start()


    def _run_bot(self):

        loop = asyncio.new_event_loop()

        asyncio.set_event_loop(loop)

        self.tg_loop = loop

        self.application.run_polling(poll_interval=0.5)


    async def handle_commands(self, update: Update, context: ContextTypes.DEFAULT_TYPE):

        if update.message.chat_id != TARGET_CHAT_ID: return

        cmd = update.message.text.replace("/", "")

        self.app.master.after(0, self.app.process_incoming_command, f"Telegram:{cmd}", cmd)


    def send_message(self, text):

        if self.tg_loop:

            asyncio.run_coroutine_threadsafe(

                self.application.bot.send_message(chat_id=TARGET_CHAT_ID, text=text), self.tg_loop

            )


# --- 4. Tkinter 主應用 ---

class RfidControlApp:

    def __init__(self, master):

        self.master = master

        master.title("RFID & MQTT 監控系統")

        self.db = DatabaseManager(DB_NAME)

        

        self.status_var = tk.StringVar(value="系統連線中...")

        self.mode_var = tk.StringVar(value=MODE_ADD)

        

        self._setup_gui()

        

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

        self.client.on_connect = self._on_connect

        self.client.on_message = self._on_message

        

        self.telegram_bot = TelegramBotHandler(self, TELEGRAM_BOT_TOKEN)

        self.telegram_bot.start_bot()


        threading.Thread(target=self._mqtt_thread, daemon=True).start()


        # 啟動後 3 秒送出提示

        self.master.after(3000, self._send_welcome_message)


    def _setup_gui(self):

        f_top = tk.Frame(self.master); f_top.pack(pady=10)

        tk.Label(f_top, text="當前模式:", font=("Arial", 12)).pack(side=tk.LEFT)

        tk.Label(f_top, textvariable=self.mode_var, font=("Arial", 12, "bold"), fg="blue").pack(side=tk.LEFT, padx=10)

        tk.Button(f_top, text="切換模式", command=self._toggle_mode).pack(side=tk.LEFT)

        

        self.info_label = tk.Label(self.master, textvariable=self.status_var, 

                                   font=("Microsoft JhengHei", 14, "bold"), bg="#FFFFE0", relief="sunken", width=45)

        self.info_label.pack(pady=10, padx=20)


        self.log_display = tk.Text(self.master, height=15, width=80, font=("Consolas", 10))

        self.log_display.pack(pady=10, padx=10)

        

        tk.Button(self.master, text="清空資料庫(Reset)", command=self._db_reset, fg="red").pack(pady=5)

        self._handle_db_show()


    def _send_welcome_message(self):

        welcome_text = (

            "🚀 RFID 監控系統已啟動!\n\n"

            "請使用以下指令控制 LED:\n"

            "/on    - 開啟 LED\n"

            "/off   - 關閉 LED\n"

            "/timer - 開啟 5 秒後關閉\n"

            "/flash - 進入閃爍模式"

        )

        self.telegram_bot.send_message(welcome_text)

        self.status_var.set("已向 Telegram 送出指令提示")


    def _play_beep(self, freq, duration, repeat=1):

        def run():

            for _ in range(repeat):

                winsound.Beep(freq, duration)

                if repeat > 1: time.sleep(0.1)

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


    def _toggle_mode(self):

        new_m = MODE_COMPARE if self.mode_var.get() == MODE_ADD else MODE_ADD

        self.mode_var.set(new_m)

        self.status_var.set(f"模式切換: {new_m}")


    def process_incoming_command(self, display_text, mqtt_cmd):

        """核心修正:更正常數名稱為 TOPIC_LED_CONTROL"""

        self.status_var.set(f"收到指令: {display_text}")

        # 修正這裡的 TOP_LED_CONTROL -> TOPIC_LED_CONTROL

        self.client.publish(TOPIC_LED_CONTROL, mqtt_cmd) 

        self.db.add_log("CMD_IN", display_text)

        self._handle_db_show()


    def _mqtt_thread(self):

        try:

            self.client.connect(MQTT_BROKER, MQTT_PORT, 60)

            self.client.loop_forever()

        except Exception as e:

            print(f"MQTT 連線失敗: {e}")


    def _on_connect(self, client, userdata, flags, rc, properties):

        # 修正這裡的 TOP_RFID_UID -> TOPIC_RFID_UID

        client.subscribe(TOPIC_RFID_UID)

        client.subscribe(TOPIC_LED_STATUS)

        self.master.after(0, lambda: self.status_var.set("MQTT 已連線,系統就緒"))


    def _on_message(self, client, userdata, msg):

        payload = msg.payload.decode()

        if msg.topic == TOPIC_RFID_UID:

            self.master.after(0, self._handle_rfid, payload)

        elif msg.topic == TOPIC_LED_STATUS:

            info = f"ESP32回報狀態: {payload.upper()}"

            self.master.after(0, lambda: self.status_var.set(info))

            self.db.add_log("MQTT_STATUS", info)

            self.master.after(0, self._handle_db_show)


    def _handle_rfid(self, uid):

        mode = self.mode_var.get()

        if mode == MODE_ADD:

            self.db.add_log(uid, f"[新增] 卡號: {uid}")

            self._play_beep(880, 1000, 3)

            msg = f"RFID 已註冊: {uid}"

            self.telegram_bot.send_message(f"✅ 已存入卡片: {uid}")

        else:

            uids = self.db.get_all_rfid_uids()

            if uid in uids:

                msg = f"比對正確: {uid}"

                self._play_beep(2000, 200)

                self.telegram_bot.send_message(f"🔓 通過: {uid}")

            else:

                msg = f"非法卡片: {uid}"

                self._play_beep(1200, 5000)

                self.telegram_bot.send_message(f"⚠️ 警報! 未授權卡片: {uid}")

        

        self.status_var.set(msg)

        self._handle_db_show()


    def _handle_db_show(self):

        logs = self.db.get_latest_logs()

        self.log_display.delete(1.0, tk.END)

        header = f"{'ID':<4} | {'日期':<10} | {'時間':<8} | {'LEDorRFID':<15} | 備註\n"

        self.log_display.insert(tk.END, header + "-"*75 + "\n")

        for r in logs:

            self.log_display.insert(tk.END, f"{r[0]:<4} | {r[1]:<10} | {r[2]:<8} | {str(r[3]):<15} | {r[4]}\n")


    def _db_reset(self):

        if messagebox.askyesno("確認", "將刪除所有記錄?"):

            conn = sqlite3.connect(DB_NAME)

            conn.cursor().execute("DROP TABLE IF EXISTS rfid_logs")

            conn.commit()

            conn.close()

            self.db.create_table()

            self._handle_db_show()

            self.status_var.set("資料庫已重置")


if __name__ == '__main__':

    if sys.platform == "win32":

        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

    root = tk.Tk()

    app = RfidControlApp(root)

    root.mainloop()


Telegram +ESP32自動發報機

  Telegram   +ESP32自動發報機 這套系統是一個典型的 IoT(物聯網)架構 ,結合了遠端配置(Python)、通訊中介(MQTT)與硬體執行(ESP32)。 以下我為您拆解這兩支程式的核心運作原理: 一、 系統架構流程 Python 端 (控制台) :使用者輸入...