2026年5月22日 星期五

ESP32 遠端感應控制系統

ESP32 遠端感應控制系統
目前的架構設計(結合了 ESP32、RFID、MQTT、Node-RED 與 Telegram 遠端雙向控制),這個系統的核心價值在於即時感應、雲端中繼、智慧自動化與即時通訊回報



整個架構透過無線網路(Wi-Fi),將現場的硬體感測端、雲端訊息中繼站、後端邏輯處理平台以及前端的使用者行動裝置緊密結合。

我們可以將其分為四大核心區塊來解析:

1. 現場硬體端(Edge Devices)

位於圖片左下方,負責第一線的資料採集與現場狀態顯示:

  • RFID Module(RC522 讀卡模組): 當感應卡片或標籤接近時,讀取其 UID 或內部資料,並將訊號傳送給主控板。

  • ESP32 主控板: 系統的核心微控制器。它接收來自 RFID 模組的資料,進行初步處理後,透過內建的 Wi-Fi 將資料上傳;同時,它也接收來自遠端的控制指令。

  • 16x2 LCD Display(顯示器): 用於現場即時顯示狀態(例如:「請刷卡」、「刷卡成功」或系統訊息)。

  • LED 燈: 作為現場的狀態指示燈(例如:綠燈代表授權通過,紅燈代表拒絕)。

2. 訊息中繼站(Message Broker)

位於圖片左上方:

  • MQTT 雲端服務: 採用輕量級的 MQTT 協定作為通訊橋樑。

    • ESP32 刷卡後,會將資料「發佈(Publish)」到指定的 Topic(主題)。

    • 後端平台則透過「訂閱(Subscribe)」該 Topic 來即時接收刷卡資料。這種架構確保了低延遲與高效率的雙向資料傳輸。

3. 後端邏輯處理中心(Backend Automation)

位於圖片右上方:

  • Node-RED: 這是一個基於流程(Flow-based)的視覺化開發工具,負責處理系統的核心邏輯。

    • 它透過 Wi-Fi 訂閱 MQTT 的訊息,當收到 ESP32 傳來的 RFID 資料時,可以在這裡進行身份比對、紀錄時間,或決定是否核准。

    • 處理完畢後,Node-RED 會將控制指令傳回 MQTT(進而控制現場的 LCD 與 LED),同時負責與通訊軟體串接。

4. 使用者終端(User Interface)

位於圖片右下方:

  • Telegram 應用程式: 作為管理員或使用者的遠端互動介面。

    • 主動通知: 當現場有人刷卡時,Node-RED 可以透過 Telegram Bot 即時發送訊息到使用者的手機(例如:「通知:某某某於 10:00 刷卡進門」)。

    • 遠端控制: 使用者也可以在 Telegram 手機介面上輸入指令(或點選選單),訊息會經由 Node-RED 與 MQTT 傳回 ESP32,實現遠端開門、亮燈或更改 LCD 顯示內容的功能。

💡 總結運作流程

  1. 上行通知: 刷卡 ➔ ESP32 讀取 ➔ 經由 Wi-Fi / MQTTNode-RED 邏輯判斷 ➔ Telegram 手機跳出通知。

  2. 下行控制: Telegram 發送指令 ➔ Node-RED 接收 ➔ 經由 MQTTESP32 執行 ➔ LCD/LED 改變現場狀態。  (需 修改 node-red)


// === MQTT Broker 設定 ===
const char broker[] = "broker.emqx.io";
int    port     = 1883;
// MQTT 主題定義
const char *SubTopic1 = "alex9ufo/2026/RFID/LED";
const char *PubTopic2 = "alex9ufo/2026/RFID/Back_LED";
const char *PubTopic3 = "alex9ufo/2026/RFID/RFID_UID";
const char *PubTopic4 = "alex9ufo/2026/RFID/RFID_PICC";
const char willTopic[] = "alex9ufo/2026/RFID/Starting";





建立 newbot步驟與取得 API Token 與 Chat ID



WOKWI程式

// 定義 MFRC522 RFID read 與 ESP32 介面 接腳連接 Pin assign

/* Wiring RFID RC522 module   

==============================================================

GND     = GND   3.3V    = 3.3V

The following table shows the typical pin layout used:

 * MFRC522      ESP32     

 * Reader/PCD             

 * Signal      Pin          Pin         

 * -----------------------------------

 * RST/Reset   RST          GPIO21   

 * SPI SS      SDA(SS)      GPIO5     

 * SPI MOSI    MOSI         GPIO23    

 * SPI MISO    MISO         GPIO19    

 * SPI SCK     SCK          GPIO18    

=============================================================

* I2C LCD 16x2 接線:

* SDA  --> GPIO 17

* SCL  --> GPIO 16

=============================================================

*/


// Wifi 與 MQttClient 程式庫

#include <WiFi.h>

#include <ArduinoMqttClient.h>


// MFRC522 程式庫

#include <SPI.h>

#include <MFRC522.h>


// I2C 與 LCD 程式庫

#include <Wire.h>

#include <LiquidCrystal_I2C.h>


#define LED 13           // 定義 LED 接腳


// 定義 I2C LCD 腳位與實例 (Wokwi 模擬的 I2C 位址通常為 0x27)

#define I2C_SDA 17

#define I2C_SCL 16

LiquidCrystal_I2C lcd(0x27, 16, 2); 


// === Wokwi 專屬的 Wi-Fi 設定 ===

char ssid[] = "Wokwi-GUEST";    

char pass[] = "";               


WiFiClient wifiClient;

MqttClient mqttClient(wifiClient);


// === MQTT Broker 設定 ===

const char broker[] = "broker.emqx.io"; 

int    port     = 1883;

String json = "";


// MQTT 主題定義

const char *SubTopic1 = "alex9ufo/2026/RFID/LED";

const char *PubTopic2 = "alex9ufo/2026/RFID/Back_LED";

const char *PubTopic3 = "alex9ufo/2026/RFID/RFID_UID";

const char *PubTopic4 = "alex9ufo/2026/RFID/RFID_PICC";

const char willTopic[] = "alex9ufo/2026/RFID/Starting";


//======================================================

#define RST_PIN      21        

#define SS_PIN       5         

MFRC522 rfid(SS_PIN, RST_PIN);

MFRC522::MIFARE_Key key;  

MFRC522::StatusCode status;


//===========================================================

bool Send = false;  

String LEDjson = "OFF"; // 預設狀態為 OFF


// --- 非阻塞計時器變數 ---

unsigned long lcdResetTimer = 0;

bool lcdShowingCard = false;

const unsigned long displayDuration = 2000; // 卡片資訊在 LCD 顯示的時間 (2秒)


//===========================================================

// MQTT 訂閱訊息接收回呼函式 (當遠端發送指令給 ESP32 時觸發)

void onMqttMessage(int messageSize) {

  Serial.print("Received a message with topic '");

  Serial.print(mqttClient.messageTopic());

  String Topic = mqttClient.messageTopic();

  Serial.print("', length ");

  Serial.print(messageSize);

  Serial.println(" bytes:");

  

  String message = "";

  while (mqttClient.available()) {

    message += (char)mqttClient.read();

  }


  Serial.println(message);

  message.trim();

  Topic.trim();


  if (Topic == "alex9ufo/2026/RFID/LED") {

    if (message == "on") {

      digitalWrite(LED, LOW);  // 點亮 LED (低電位觸發)

      LEDjson = "ON";

      Send = true;

    }

    if (message == "off") {

      digitalWrite(LED, HIGH); // 熄滅 LED

      LEDjson = "OFF";

      Send = true;

    }

    

    // 如果目前 LCD 不是在顯示剛刷卡的資訊,就即時更新 LCD 第二行

    if (!lcdShowingCard) {

      lcd.setCursor(0, 1);

      lcd.print("LED: " + LEDjson + "        "); 

    }

    

    Serial.print("LED = ");

    Serial.println(LEDjson);

    Serial.println("\n-----------------------");

  }  

}


//===========================================================

// 16進位 Byte 陣列轉字串工具

String printHex(byte *buffer, byte bufferSize) {

  String id = "";

  for (byte i = 0; i < bufferSize; i++) {

    id += buffer[i] < 0x10 ? "0" : "";

    id += String(buffer[i], HEX);

    id += " ";

  }

  return id;

}


//===========================================================

// Wi-Fi 連線程序

void setup_wifi() {

  delay(10);

  Serial.println();

  Serial.print("Connecting to ");

  Serial.println(ssid);

  WiFi.begin(ssid, pass);

  

  lcd.clear();

  lcd.setCursor(0, 0);

  lcd.print("Connecting WiFi");


  int dotCount = 0;

  while (WiFi.status() != WL_CONNECTED) {

    delay(500);

    Serial.print(".");

    lcd.setCursor(dotCount % 16, 1);

    lcd.print(".");

    dotCount++;

  }


  Serial.println("\nWiFi connected");

  Serial.print("IP address: ");

  Serial.println(WiFi.localIP());


  lcd.clear();

  lcd.setCursor(0, 0);

  lcd.print("WiFi Connected!");

  lcd.setCursor(0, 1);

  lcd.print(WiFi.localIP().toString());

  delay(1500);

}  


//===========================================================

// MQTT 發行:回報當前 LED 狀態

void LED_Message() {

  if (Send) {

    Serial.print("Publish message: ");

    Serial.println(LEDjson);

    LEDjson.trim();


    int qos = 0; // 改為 QoS 0 提高在公開 Broker 上的傳輸穩定度

    bool retained = false;

    bool dup = false;

    

    mqttClient.beginMessage(PubTopic2, LEDjson.length(), retained, qos, dup);

    mqttClient.print(LEDjson);

    mqttClient.endMessage();

    Send = false;

  }


//=========================================================== 

void setup() {

  pinMode(LED, OUTPUT);

  digitalWrite(LED, HIGH); // 初始關閉 LED

  

  Serial.begin(115200);

  while (!Serial);

  

  // 初始化自訂的 I2C 接腳 (SDA=17, SCL=16)

  Wire.begin(I2C_SDA, I2C_SCL);

  

  // 初始化 LCD

  lcd.init();

  lcd.backlight();

  lcd.print("System Init...");


  setup_wifi();

  

  // 設定遺言訊息 (Last Will)

  String willPayload = "ESP32 Disconnected";

  bool willRetain = false;

  int willQos = 0;

  mqttClient.beginWill(willTopic, willPayload.length(), willRetain, willQos);

  mqttClient.print(willPayload);

  mqttClient.endWill();


  Serial.print("Attempting to connect to the MQTT broker: ");

  Serial.println(broker);


  lcd.clear();

  lcd.print("Connect MQTT...");

  if (!mqttClient.connect(broker, port)) {

    Serial.print("MQTT connection failed! Error code = ");

    Serial.println(mqttClient.connectError());

    lcd.clear();

    lcd.print("MQTT Conn Fail!");

    while (1);

  }


  Serial.println("You're connected to the MQTT broker!\n");


  // 註冊訂閱接收市場與訂閱主題

  mqttClient.onMessage(onMqttMessage);

  int subscribeQos = 0; 

  mqttClient.subscribe(SubTopic1, subscribeQos);


  // 初始化 RFID 模組

  SPI.begin();         

  rfid.PCD_Init();     

  delay(4);

  Serial.println("Scan PICC to see UID...");


  // 進入主迴圈前的初始畫面

  lcd.clear();

  lcd.setCursor(0, 0);

  lcd.print("Scan RFID Card..");

  lcd.setCursor(0, 1);

  lcd.print("LED: OFF");

}


//===========================================================

void loop() {

  // 保持 MQTT 心跳與背景訊息接收(非阻塞核心)

  mqttClient.poll();

  

  // 處理 LED 狀態發行回報

  LED_Message();


  // --- LCD 畫面自動復原計時檢查 ---

  if (lcdShowingCard && (millis() - lcdResetTimer >= displayDuration)) {

    lcdShowingCard = false;

    lcd.clear();

    lcd.setCursor(0, 0);

    lcd.print("Scan RFID Card..");

    lcd.setCursor(0, 1);

    lcd.print("LED: " + LEDjson + "        ");

  }


  // 檢查是否有新卡片被擺放,以及是否成功讀取

  if (rfid.PICC_IsNewCardPresent() && rfid.PICC_ReadCardSerial()) {

    

    byte *id = rfid.uid.uidByte;   

    byte idSize = rfid.uid.size;   

    String Type;

    

    MFRC522::PICC_Type piccType = rfid.PICC_GetType(rfid.uid.sak);

    Type = rfid.PICC_GetTypeName(piccType);


    // 格式化 UID 字串

    json = printHex(rfid.uid.uidByte, rfid.uid.size);

    json.toUpperCase();

    json.trim();


    // ====== LCD 顯示邏輯更新 ======

    lcd.clear();

    lcd.setCursor(0, 0);

    lcd.print("ID: " + json);                 // 第一行顯示 UID

    lcd.setCursor(0, 1);

    lcd.print("Type:" + Type.substring(0, 11)); // 第二行顯示卡片類型 (限制長度防止溢出)

    

    // 啟動非阻塞計時器,標記目前正在展示卡片

    lcdResetTimer = millis();

    lcdShowingCard = true;

    // =============================


    // 發送 MQTT:卡片 UID 資訊

    int qos = 0;

    bool retained = false;

    bool dup = false;

    mqttClient.beginMessage(PubTopic3, json.length(), retained, qos, dup);

    mqttClient.print(json);

    mqttClient.endMessage();


    // 發送 MQTT:卡片類型資訊

    String typeJson = "PICC type: " + Type;

    typeJson.trim();

    mqttClient.beginMessage(PubTopic4, typeJson.length(), retained, qos, dup);

    mqttClient.print(typeJson);

    mqttClient.endMessage();

    

    // 本機序列埠除錯輸出

    Serial.println("Published UID: " + json);

    Serial.println("Published Type: " + typeJson);

    Serial.println();


    // 讓卡片進入休眠,避免重複讀取

    rfid.PICC_HaltA();

    rfid.PCD_StopCrypto1(); 

  }

}



這份程式碼是經過優化後的完整版本,主要特點是結合了 MFRC522 RFID 讀卡I2C LCD 16x2 顯示,以及基於 millis() 非阻塞計時器的 MQTT 雙向通訊

以下為您進行這份優化版程式碼的逐行詳細說明:

1. 硬體連接與備忘註解

C++
// 定義 MFRC522 RFID read 與 ESP32 介面 接腳連接 Pin assign
/* Wiring RFID RC522 module   
==============================================================
GND     = GND   3.3V    = 3.3V
The following table shows the typical pin layout used:
 * MFRC522      ESP32     
 * Reader/PCD             
 * Signal      Pin          Pin         
 * -----------------------------------
 * RST/Reset   RST          GPIO21   
 * SPI SS      SDA(SS)      GPIO5     
 * SPI MOSI    MOSI         GPIO23    
 * SPI MISO    MISO         GPIO19    
 * SPI SCK     SCK          GPIO18    
=============================================================
* I2C LCD 16x2 接線:
* SDA  --> GPIO 17
* SCL  --> GPIO 16
=============================================================
*/
  • 說明:純文字註解,紀錄實體硬體或 Wokwi 模擬器上的接線定義。RFID 使用標準 VSPI 腳位(RST 改用 21);I2C LCD 則接在 GPIO 17 與 16。

2. 引入程式庫

C++
// Wifi 與 MQttClient 程式庫
#include <WiFi.h>
#include <ArduinoMqttClient.h>

// MFRC522 程式庫
#include <SPI.h>
#include <MFRC522.h>

// I2C 與 LCD 程式庫
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
  • #include <WiFi.h> / <ArduinoMqttClient.h>:提供 Wi-Fi 連線與官方 MQTT 協定支援。

  • #include <SPI.h> / <MFRC522.h>:驅動以 SPI 介面通訊的 RFID 讀卡機。

  • #include <Wire.h> / <LiquidCrystal_I2C.h>:驅動 I2C 介面的液晶顯示器(LCD)。

3. 引腳、物件與 MQTT 主題定義

C++
#define LED 13           // 定義 LED 接腳

// 定義 I2C LCD 腳位與實例 (Wokwi 模擬的 I2C 位址通常為 0x27)
#define I2C_SDA 17
#define I2C_SCL 16
LiquidCrystal_I2C lcd(0x27, 16, 2); 
  • #define LED 13:LED 連接到 GPIO 13。

  • #define I2C_SDA 17 / #define I2C_SCL 16:自訂 ESP32 的 I2C 腳位。

  • LiquidCrystal_I2C lcd(0x27, 16, 2);:初始化 LCD 物件,設定 I2C 位址為 0x27,規格為 16 欄 2 列。

C++
// === Wokwi 專屬的 Wi-Fi 設定 ===
char ssid[] = "Wokwi-GUEST";    
char pass[] = "";               

WiFiClient wifiClient;
MqttClient mqttClient(wifiClient);
  • 設定 Wokwi 線上模擬專用的模擬基地台 SSID 與無密碼設定。

  • 建立底層 TCP 連線(wifiClient)並將其包裝進 MQTT 客戶端(mqttClient)。

C++
// === MQTT Broker 設定 ===
const char broker[] = "broker.emqx.io"; 
int    port     = 1883;
String json = "";

// MQTT 主題定義
const char *SubTopic1 = "alex9ufo/2026/RFID/LED";
const char *PubTopic2 = "alex9ufo/2026/RFID/Back_LED";
const char *PubTopic3 = "alex9ufo/2026/RFID/RFID_UID";
const char *PubTopic4 = "alex9ufo/2026/RFID/RFID_PICC";
const char willTopic[] = "alex9ufo/2026/RFID/Starting";
  • 定義 EMQX 公開伺服器網址與預設的非加密通訊埠 1883

  • SubTopic1訂閱接收控制 LED 指令的主題。

  • PubTopic2 ~ 4willTopic發行狀態、UID、卡片類型與遺言的主題。

C++
//======================================================
#define RST_PIN      21        
#define SS_PIN       5         
MFRC522 rfid(SS_PIN, RST_PIN);
MFRC522::MIFARE_Key key;  
MFRC522::StatusCode status;
  • 建立 MFRC522 讀卡機實例,傳入晶片選擇引腳(5)與重置引腳(21)。

  • 宣告金鑰(key)與狀態變數(status)供後續呼叫。

4. 全域變數與非阻塞計時器宣告

C++
bool Send = false;  
String LEDjson = "OFF"; // 預設狀態為 OFF

// --- 非阻塞計時器變數 ---
unsigned long lcdResetTimer = 0;
bool lcdShowingCard = false;
const unsigned long displayDuration = 2000; // 卡片資訊在 LCD 顯示的時間 (2秒)
  • Send:觸發發行 LED 狀態的旗標。

  • LEDjson:儲存當前 LED 狀態("ON" 或 "OFF")。

  • lcdResetTimer:紀錄刷卡時的時間戳記(單位:毫秒)。

  • lcdShowingCard:布林旗標,用來標記「目前 LCD 是否正停留在顯示 UID 畫面上」。

  • displayDuration:設定刷卡資訊要在 LCD 上停留多久(2000 毫秒 = 2 秒)。

5. 功能函式與內部邏輯

A. MQTT 訂閱訊息回呼 (Callback) 函式

C++
void onMqttMessage(int messageSize) {
  Serial.print("Received a message with topic '");
  Serial.print(mqttClient.messageTopic());
  String Topic = mqttClient.messageTopic();
  Serial.print("', length ");
  Serial.print(messageSize);
  Serial.println(" bytes:");
  • 當遠端有人發布訊息到 ESP32 訂閱的主題時,此函式會被自動觸發。在序列埠輸出收到的主題與訊息大小。

C++
  String message = "";
  while (mqttClient.available()) {
    message += (char)mqttClient.read();
  }

  Serial.println(message);
  message.trim();
  Topic.trim();
  • 透過迴圈讀取快取中的字元,拼裝成 message 字串,並使用 .trim() 清除字串前後的多餘空白或換行符號。

C++
  if (Topic == "alex9ufo/2026/RFID/LED") {
    if (message == "on") {
      digitalWrite(LED, LOW);  // 點亮 LED (低電位觸發)
      LEDjson = "ON";
      Send = true;
    }
    if (message == "off") {
      digitalWrite(LED, HIGH); // 熄滅 LED
      LEDjson = "OFF";
      Send = true;
    }
  • 比對主題。如果收到小寫的 "on",將 GPIO 13 輸出低電位點亮 LED(許多開發板為低電位觸發),變數改為 "ON",並將 Send 設為 true 準備回報。反之收到 "off" 則關燈。

C++
    // 如果目前 LCD 不是在顯示剛刷卡的資訊,就即時更新 LCD 第二行
    if (!lcdShowingCard) {
      lcd.setCursor(0, 1);
      lcd.print("LED: " + LEDjson + "        "); 
    }
    
    Serial.print("LED = ");
    Serial.println(LEDjson);
    Serial.println("\n-----------------------");
  }  
}
  • 邏輯檢查:如果 LCD 此刻沒有在顯示剛刷完卡的 UID 資訊(避免畫面被蓋掉),就立刻在 LCD 第二行刷新顯示目前的 LED 狀態,後方留空格用以覆蓋舊字。

B. 16 進位字串轉換工具

C++
String printHex(byte *buffer, byte bufferSize) {
  String id = "";
  for (byte i = 0; i < bufferSize; i++) {
    id += buffer[i] < 0x10 ? "0" : "";
    id += String(buffer[i], HEX);
    id += " ";
  }
  return id;
}
  • 將 RFID 晶片讀出的原始二進位 Byte 陣列,逐一轉換成大寫 16 進位英數字串(補零處理,如 0A B3 2C),方便顯示與閱讀。

C. Wi-Fi 連線程序

C++
void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, pass);
  
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Connecting WiFi");

  int dotCount = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    lcd.setCursor(dotCount % 16, 1);
    lcd.print(".");
    dotCount++;
  }
  • 開始連接無線網路,並同時在 LCD 第一行印出 "Connecting WiFi"

  • 運用 while 迴圈每 0.5 秒檢查連線狀態,連線上之前,會在 LCD 第二行像跑馬燈一樣橫向列印點記號 .

C++
  Serial.println("\nWiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("WiFi Connected!");
  lcd.setCursor(0, 1);
  lcd.print(WiFi.localIP().toString());
  delay(1500);
}  
  • 連線成功後,本機序列埠輸出成功訊息,並在 LCD 上短暫顯示獲取的局域網路 IP 位址 1.5 秒。

D. MQTT 狀態發行 (回報 LED 狀態)

C++
void LED_Message() {
  if (Send) {
    Serial.print("Publish message: ");
    Serial.println(LEDjson);
    LEDjson.trim();

    int qos = 0; // 改為 QoS 0 提高在公開 Broker 上的傳輸穩定度
    bool retained = false;
    bool dup = false;
    
    mqttClient.beginMessage(PubTopic2, LEDjson.length(), retained, qos, dup);
    mqttClient.print(LEDjson);
    mqttClient.endMessage();
    Send = false;
  }
} 
  • 檢查 Send 旗標。若成立,則將當前最新的 LED 狀態字串發布至 PubTopic2.../Back_LED)。

  • 關鍵優化:此處將服務品質改為 qos = 0,這對公開且高流量的 broker.emqx.io 伺服器而言,可以大幅提高訊息成功收發的順暢度、降低連線被剔除的機率。

6. 初始化設定 (setup)

C++
void setup() {
  pinMode(LED, OUTPUT);
  digitalWrite(LED, HIGH); // 初始關閉 LED
  
  Serial.begin(115200);
  while (!Serial);
  
  // 初始化自訂的 I2C 接腳 (SDA=17, SCL=16)
  Wire.begin(I2C_SDA, I2C_SCL);
  
  // 初始化 LCD
  lcd.init();
  lcd.backlight();
  lcd.print("System Init...");
  • 配置 LED 為輸出腳位並預設關燈。開啟 115200 鮑率的序列埠。

  • 呼叫 Wire.begin(17, 16) 強制指定 I2C 介面的 SDA 與 SCL 硬體映射引腳。

  • 初始化 LCD 並點亮其背光,顯示 "System Init..."

C++
  setup_wifi();
  
  // 設定遺言訊息 (Last Will)
  String willPayload = "ESP32 Disconnected";
  bool willRetain = false;
  int willQos = 0;
  mqttClient.beginWill(willTopic, willPayload.length(), willRetain, willQos);
  mqttClient.print(willPayload);
  mqttClient.endWill();
  • 呼叫 Wi-Fi 連線。

  • 配置 MQTT 「遺言(Last Will)」機制。若設備未來發生異常斷線,伺服器會替它向 willTopic 發布 "ESP32 Disconnected" 訊息。

C++
  Serial.print("Attempting to connect to the MQTT broker: ");
  Serial.println(broker);

  lcd.clear();
  lcd.print("Connect MQTT...");
  if (!mqttClient.connect(broker, port)) {
    Serial.print("MQTT connection failed! Error code = ");
    Serial.println(mqttClient.connectError());
    lcd.clear();
    lcd.print("MQTT Conn Fail!");
    while (1);
  }

  Serial.println("You're connected to the MQTT broker!\n");
  • 在 LCD 提示下嘗試連線至 MQTT Broker。如果連線失敗,則在 LCD 上顯示錯誤並進入死迴圈停機;成功則繼續。

C++
  // 註冊訂閱接收市場與訂閱主題
  mqttClient.onMessage(onMqttMessage);
  int subscribeQos = 0; 
  mqttClient.subscribe(SubTopic1, subscribeQos);

  // 初始化 RFID 模組
  SPI.begin();         
  rfid.PCD_Init();     
  delay(4);
  Serial.println("Scan PICC to see UID...");

  // 進入主迴圈前的初始畫面
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Scan RFID Card..");
  lcd.setCursor(0, 1);
  lcd.print("LED: OFF");
}
  • 訂閱 LED 控制主題(QoS 改為 0)。

  • 開啟 SPI 匯流排並驅動 MFRC522 晶片初始化。

  • 清除 LCD 畫面,將其設為待命刷卡狀態(第一行 Scan RFID Card..,第二行 LED: OFF)。

7. 主程式工作迴圈 (loop)

C++
void loop() {
  // 保持 MQTT 心跳與背景訊息接收(非阻塞核心)
  mqttClient.poll();
  
  // 處理 LED 狀態發行回報
  LED_Message();
  • mqttClient.poll():每毫秒高頻維持與遠端 Broker 的長連線,並檢查有無最新訂閱訊息。

  • 隨時檢查有無 LED 狀態需要發行回報。

C++
  // --- LCD 畫面自動復原計時檢查 ---
  if (lcdShowingCard && (millis() - lcdResetTimer >= displayDuration)) {
    lcdShowingCard = false;
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Scan RFID Card..");
    lcd.setCursor(0, 1);
    lcd.print("LED: " + LEDjson + "        ");
  }
  • 關鍵計時邏輯(非阻塞式):如果目前 LCD 正在顯示刷卡資訊(lcdShowingCard == true),程式不會用 delay() 卡死,而是用現時時間(millis())減去刷卡當下的時間。

  • 一旦滿 2000 毫秒(2 秒),會自動把標記設回 false,並將 LCD 復原為預設待命畫面,同時顯示當下真實的 LED 狀態。這確保了在等待這 2 秒的期間,MQTT 指令依然進得來、LED 依然能被控制

C++
  // 檢查是否有新卡片被擺放,以及是否成功讀取
  if (rfid.PICC_IsNewCardPresent() && rfid.PICC_ReadCardSerial()) {
    
    byte *id = rfid.uid.uidByte;   
    byte idSize = rfid.uid.size;   
    String Type;
    
    MFRC522::PICC_Type piccType = rfid.PICC_GetType(rfid.uid.sak);
    Type = rfid.PICC_GetTypeName(piccType);

    // 格式化 UID 字串
    json = printHex(rfid.uid.uidByte, rfid.uid.size);
    json.toUpperCase();
    json.trim();
  • 偵測到有新卡片放入且成功讀取序列號。

  • 辨識卡片 SAK 規格取得類型,並呼叫 printHex 轉換出 UID 16 進位字串。

C++
    // ====== LCD 顯示邏輯更新 ======
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("ID: " + json);                 // 第一行顯示 UID
    lcd.setCursor(0, 1);
    lcd.print("Type:" + Type.substring(0, 11)); // 第二行顯示卡片類型 (限制長度防止溢出)
    
    // 啟動非阻塞計時器,標記目前正在展示卡片
    lcdResetTimer = millis();
    lcdShowingCard = true;
    // =============================
  • 清除 LCD 舊畫面。第一行印出 "ID: 8A 2F ..."

  • 第二行印出 "Type:" 加上截取前 11 個字元的類型名稱(防止字串過長溢出螢幕)。

  • 紀錄當前時間戳記到 lcdResetTimer,並將 lcdShowingCard 設為 true 啟動倒數。

C++
    // 發送 MQTT:卡片 UID 資訊
    int qos = 0;
    bool retained = false;
    bool dup = false;
    mqttClient.beginMessage(PubTopic3, json.length(), retained, qos, dup);
    mqttClient.print(json);
    mqttClient.endMessage();

    // 發送 MQTT:卡片類型資訊
    String typeJson = "PICC type: " + Type;
    typeJson.trim();
    mqttClient.beginMessage(PubTopic4, typeJson.length(), retained, qos, dup);
    mqttClient.print(typeJson);
    mqttClient.endMessage();
    
    // 本機序列埠除錯輸出
    Serial.println("Published UID: " + json);
    Serial.println("Published Type: " + typeJson);
    Serial.println();

    // 讓卡片進入休眠,避免重複讀取
    rfid.PICC_HaltA();
    rfid.PCD_StopCrypto1(); 
  }
}

  • 將讀取到的 UID 資訊與 PICC 類型字串分別發行至 PubTopic3PubTopic4

  • 序列埠輸出訊息以供監控除錯。

  • 執行卡片休眠命令(PICC_HaltA()PCD_StopCrypto1()),釋放與該感應卡的加密連線,等待下一次新卡片切入。






上面的圖片需下載


NODE-RED程式

[{"id":"2e49e96118b6754c","type":"mqtt in","z":"68da7d67bf44b8e8","name":"Back LED","topic":"alex9ufo/2026/RFID/Back_LED","qos":"1","datatype":"auto-detect","broker":"emqx_broker","nl":false,"rap":true,"rh":0,"inputs":0,"x":100,"y":500,"wires":[["a114141d2b1c671d","15a64de5208afe18","c2cd10bbb6311396","7e3e72e33e40ec55"]]},{"id":"a114141d2b1c671d","type":"function","z":"68da7d67bf44b8e8","name":"function LED on ,off","func":"var onoff=msg.payload;\n\nif (onoff==\"ON\")\n{\n    msg.payload=true;\n}\nif (onoff==\"OFF\")\n{\n    msg.payload=false;\n}  \nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":580,"wires":[["dc14e92ef1118bf4"]]},{"id":"603c56186d0ba368","type":"inject","z":"68da7d67bf44b8e8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"false","payloadType":"bool","x":310,"y":660,"wires":[["dc14e92ef1118bf4"]]},{"id":"b8999d910b710c44","type":"inject","z":"68da7d67bf44b8e8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":310,"y":620,"wires":[["dc14e92ef1118bf4"]]},{"id":"dc14e92ef1118bf4","type":"ui_led","z":"68da7d67bf44b8e8","order":1,"group":"abdb0fd233aa42ae","width":6,"height":4,"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":510,"y":580,"wires":[]},{"id":"b16c7018f05f92f4","type":"ui_button","z":"68da7d67bf44b8e8","name":"","group":"abdb0fd233aa42ae","order":2,"width":0,"height":0,"passthru":false,"label":"LED ON","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"on","payloadType":"str","topic":"topic","topicType":"msg","x":120,"y":60,"wires":[["d39c27f099e12598","833938b14896242b","1c1382da51906c1b"]]},{"id":"cac10ec6f56e1d4b","type":"ui_button","z":"68da7d67bf44b8e8","name":"","group":"abdb0fd233aa42ae","order":3,"width":0,"height":0,"passthru":false,"label":"LED OFF","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"off","payloadType":"str","topic":"topic","topicType":"msg","x":120,"y":160,"wires":[["d39c27f099e12598","833938b14896242b","1c1382da51906c1b"]]},{"id":"d39c27f099e12598","type":"ui_audio","z":"68da7d67bf44b8e8","name":"","group":"abdb0fd233aa42ae","voice":"Google US English","always":"","x":360,"y":120,"wires":[]},{"id":"833938b14896242b","type":"mqtt out","z":"68da7d67bf44b8e8","name":"LED","topic":"alex9ufo/2026/RFID/LED","qos":"0","retain":"true","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"emqx_broker","x":350,"y":60,"wires":[]},{"id":"15a64de5208afe18","type":"ui_text","z":"68da7d67bf44b8e8","group":"abdb0fd233aa42ae","order":5,"width":0,"height":0,"name":"","label":"MQTT<<訂閱>>的訊息","format":"{{msg.payload}}","layout":"row-left","className":"","style":false,"font":"","fontSize":"","color":"#000000","x":320,"y":540,"wires":[]},{"id":"1c1382da51906c1b","type":"ui_text","z":"68da7d67bf44b8e8","group":"abdb0fd233aa42ae","order":4,"width":0,"height":0,"name":"","label":"MQTT  [ [發行] ] 的訊息","format":"{{msg.payload}}","layout":"row-left","className":"","x":400,"y":160,"wires":[]},{"id":"a8ff543e08df099f","type":"ui_text","z":"68da7d67bf44b8e8","group":"abdb0fd233aa42ae","order":6,"width":0,"height":0,"name":"","label":"MQTT 的Broker :","format":"<font color= {{msg.color}} > {{msg.payload}} </font>","layout":"row-left","className":"","x":510,"y":320,"wires":[]},{"id":"bea7c3443fca3e6d","type":"function","z":"68da7d67bf44b8e8","name":"function on off","func":"var on_off=flow.get(\"onoff\");\n\nif (on_off== true)\n{\n    msg.payload ='emqx.io';\n    flow.set(\"onoff\",false)\n}   \nelse\n{\n    msg.payload='   ';\n    flow.set(\"onoff\",true)\n}\nmsg.color=\"red\";\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":320,"wires":[["a8ff543e08df099f","ccf5ecd004223c65"]]},{"id":"56c4d1820a183fd3","type":"inject","z":"68da7d67bf44b8e8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":110,"y":320,"wires":[["bea7c3443fca3e6d","ddf223246a982fe6"]]},{"id":"ccf5ecd004223c65","type":"delay","z":"68da7d67bf44b8e8","name":"","pauseType":"delay","timeout":"2","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":420,"y":260,"wires":[["bea7c3443fca3e6d"]]},{"id":"ddf223246a982fe6","type":"function","z":"68da7d67bf44b8e8","name":"function  flow set","func":"flow.set(\"onoff\",true);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":300,"y":360,"wires":[[]]},{"id":"ef2e3c6dbc56b108","type":"comment","z":"68da7d67bf44b8e8","name":"發行 alex9ufo/2026/RFID/LED","info":"","x":160,"y":120,"wires":[]},{"id":"1c76ef1d357fef47","type":"comment","z":"68da7d67bf44b8e8","name":"訂閱 alex9ufo/2026/RFID/Back_LED","info":"","x":180,"y":420,"wires":[]},{"id":"e8443a03a94260a3","type":"comment","z":"68da7d67bf44b8e8","name":"定期 閃爍 HiveMQTT","info":"","x":140,"y":260,"wires":[]},{"id":"046d0ba6a012677b","type":"inject","z":"68da7d67bf44b8e8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"10","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":720,"y":400,"wires":[["b240559abb0209a4"]]},{"id":"605ad0d984677b48","type":"template","z":"68da7d67bf44b8e8","name":"Image in","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<img src=\"data:image/png;base64,{{payload}}\"style=\"width=\"400\" height=\"300\"\"/>","output":"str","x":1160,"y":400,"wires":[["0cebf72e4cdc499c"]]},{"id":"0cebf72e4cdc499c","type":"ui_template","z":"68da7d67bf44b8e8","group":"7a9b4b9e6b46c87a","name":"MQTT","order":1,"width":9,"height":6,"format":"<div ng-bind-html=\"msg.payload\"></div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","className":"","x":1310,"y":400,"wires":[[]]},{"id":"b240559abb0209a4","type":"file in","z":"68da7d67bf44b8e8","name":"","filename":"EX.png","filenameType":"str","format":"","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":860,"y":400,"wires":[["417796efed0dcd16"]]},{"id":"417796efed0dcd16","type":"base64","z":"68da7d67bf44b8e8","name":"","action":"","property":"payload","x":1000,"y":400,"wires":[["605ad0d984677b48"]]},{"id":"323196702524a9b9","type":"comment","z":"68da7d67bf44b8e8","name":"On inject add MQTT picture  to the dashboard","info":"","x":1130,"y":360,"wires":[]},{"id":"ec0448a8b357df0c","type":"comment","z":"68da7d67bf44b8e8","name":"存放路徑若是不同需修改","info":"存放路徑\nD:\\2024RFID\\作業1\\EX1.png\n若是不同需修改","x":770,"y":360,"wires":[]},{"id":"c2cd10bbb6311396","type":"function","z":"68da7d67bf44b8e8","name":"function ","func":"msg.payload=\" ---ESP32 回來 LED的狀態---\" +msg.payload;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":500,"wires":[["5aadcb12abb08791","eb856d5ff7f3b1de"]]},{"id":"5aadcb12abb08791","type":"ui_text","z":"68da7d67bf44b8e8","group":"7a9b4b9e6b46c87a","order":3,"width":9,"height":1,"name":"","label":"LED至Telegram訊息","format":"<font color= {{msg.color}} > {{msg.payload}} </font>","layout":"row-left","className":"","style":false,"font":"","fontSize":"","color":"#000000","x":560,"y":460,"wires":[]},{"id":"38a1bf1b20c46a9d","type":"ui_text","z":"68da7d67bf44b8e8","group":"5a5ed4d8bae44949","order":1,"width":6,"height":4,"name":"","label":"RFID UID -->","format":"{{msg.payload}}","layout":"row-left","className":"","style":false,"font":"","fontSize":"","color":"#000000","x":1230,"y":60,"wires":[]},{"id":"64a47d4b25fc2089","type":"mqtt in","z":"68da7d67bf44b8e8","name":"RFID_UID","topic":"alex9ufo/2026/RFID/RFID_UID","qos":"1","datatype":"auto-detect","broker":"emqx_broker","nl":false,"rap":true,"rh":0,"inputs":0,"x":720,"y":60,"wires":[["38a1bf1b20c46a9d","26b27559c57d4621","a13fb7a4be36a41c","99e4fa0c11a4f7e1"]]},{"id":"26b27559c57d4621","type":"delay","z":"68da7d67bf44b8e8","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":860,"y":20,"wires":[["42e4abef9af68b45"]]},{"id":"42e4abef9af68b45","type":"function","z":"68da7d67bf44b8e8","name":"function  覆蓋","func":"msg.payload=\"\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1070,"y":20,"wires":[["38a1bf1b20c46a9d"]]},{"id":"a13fb7a4be36a41c","type":"function","z":"68da7d67bf44b8e8","name":"function  ","func":"msg.payload=\" ---ESP32 回來 UID號碼---\" +msg.payload;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":860,"y":160,"wires":[["d0fcb1a6409d9e37","a216b8c984c30de4"]]},{"id":"607a5e7641e49d3d","type":"debug","z":"68da7d67bf44b8e8","name":"debug ","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1370,"y":140,"wires":[]},{"id":"d0fcb1a6409d9e37","type":"ui_text","z":"68da7d67bf44b8e8","group":"7a9b4b9e6b46c87a","order":4,"width":9,"height":1,"name":"","label":"RFID至Telegram訊息","format":"<font color= {{msg.color}} > {{msg.payload}} </font>","layout":"row-left","className":"","style":false,"font":"","fontSize":"","color":"#000000","x":1020,"y":120,"wires":[]},{"id":"46e24473a70f6a6a","type":"mqtt in","z":"68da7d67bf44b8e8","name":"RFID_PICC","topic":"alex9ufo/2026/RFID/RFID_PICC","qos":"1","datatype":"auto-detect","broker":"emqx_broker","nl":false,"rap":true,"rh":0,"inputs":0,"x":730,"y":260,"wires":[["522c554d5445cf89","bfb037142e02c44c","89e09024c555ede6"]]},{"id":"522c554d5445cf89","type":"delay","z":"68da7d67bf44b8e8","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":920,"y":220,"wires":[["4efad8440f14fa76"]]},{"id":"89e09024c555ede6","type":"ui_text","z":"68da7d67bf44b8e8","group":"5a5ed4d8bae44949","order":2,"width":6,"height":5,"name":"","label":"RFID  -->","format":"{{msg.payload}}","layout":"row-left","className":"","style":false,"font":"","fontSize":"","color":"#000000","x":1260,"y":260,"wires":[]},{"id":"7e3e72e33e40ec55","type":"debug","z":"68da7d67bf44b8e8","name":"debug 270","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":290,"y":460,"wires":[]},{"id":"6f16dd2d1746a92b","type":"comment","z":"68da7d67bf44b8e8","name":"MQTT","info":"const char broker[] = \"broker.emqx.io\"; \nint        port     = 1883;\nconst char *SubTopic1 = \"alex9ufo/2026/RFID/LED\";\nconst char *PubTopic2 = \"alex9ufo/2026/RFID/Back_LED\";\nconst char *PubTopic3 = \"alex9ufo/2026/RFID/RFID_UID\";\nconst char *PubTopic4 = \"alex9ufo/2026/RFID/RFID_PICC\";\nconst char willTopic[] = \"alex9ufo/2026/RFID/Starting\";","x":110,"y":20,"wires":[]},{"id":"a216b8c984c30de4","type":"template","z":"68da7d67bf44b8e8","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\"chatId\": 7965218469,\n\"type\":\"message\",\n\"content\":\"{{payload}}\"}","output":"json","x":1010,"y":160,"wires":[["9080b36523d3c10e"]]},{"id":"9080b36523d3c10e","type":"telegram sender","z":"68da7d67bf44b8e8","name":"","bot":"457874f8aa8a857a","haserroroutput":true,"outputs":2,"x":1190,"y":160,"wires":[["607a5e7641e49d3d"],[]]},{"id":"eb856d5ff7f3b1de","type":"template","z":"68da7d67bf44b8e8","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\"chatId\": 7965218469,\n\"type\":\"message\",\n\"content\":\"{{payload}}\"}","output":"json","x":470,"y":500,"wires":[["f042529b04a87820"]]},{"id":"f042529b04a87820","type":"telegram sender","z":"68da7d67bf44b8e8","name":"","bot":"457874f8aa8a857a","haserroroutput":true,"outputs":2,"x":630,"y":500,"wires":[[],[]]},{"id":"bfb037142e02c44c","type":"debug","z":"68da7d67bf44b8e8","name":"debug 387","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":950,"y":300,"wires":[]},{"id":"4efad8440f14fa76","type":"function","z":"68da7d67bf44b8e8","name":"function  覆蓋","func":"msg.payload=\"\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1090,"y":220,"wires":[["89e09024c555ede6"]]},{"id":"99e4fa0c11a4f7e1","type":"debug","z":"68da7d67bf44b8e8","name":"debug 388","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":910,"y":80,"wires":[]},{"id":"emqx_broker","type":"mqtt-broker","name":"EMQX Public","broker":"broker.emqx.io","port":"1883","clientid":"node-red-alex-gate","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"15","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"abdb0fd233aa42ae","type":"ui_group","name":"LED","tab":"c64388e13e92235d","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"7a9b4b9e6b46c87a","type":"ui_group","name":"Telegram","tab":"c64388e13e92235d","order":2,"disp":true,"width":9,"collapse":false,"className":""},{"id":"5a5ed4d8bae44949","type":"ui_group","name":"RFID","tab":"c64388e13e92235d","order":3,"disp":true,"width":"6","collapse":false,"className":""},{"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":"c64388e13e92235d","type":"ui_tab","name":"2026RFID","icon":"dashboard","order":103,"disabled":false,"hidden":false}]


它透過 MQTT 代理伺服器(使用公用的 broker.emqx.io)與前端硬體(ESP32 / RFID)通訊,並將資料呈現在 Node-RED Dashboard 網頁儀表板上,同時串接 Telegram Bot 實作雙向遠端即時通知與控制。

我們可以將這段程式碼的邏輯流程切分為以下幾個主要核心區塊:

1. 遠端控制現場 LED(下行控制)

這部分負責從儀表板發送指令,去改變現場 ESP32 連接的 LED 狀態:

  • ui_button (LED ON / LED OFF): 在儀表板上提供兩個按鈕,按下時分別送出字串 "on""off"

  • ui_audio: 當按下按鈕時,會觸發 Google US English 的語音提示(增強互動感)。

  • mqtt out (Topic: alex9ufo/2026/RFID/LED): 將控制命令發行(Publish)出去,並設定了 retain: true(保留訊息),讓 ESP32 斷線重連時能立刻取得最後的 LED 狀態。

2. 接收並顯示現場 LED 回傳狀態(上行反饋)

當現場 ESP32 執行完動作並回傳狀態時,此流程負責接收並記錄:

  • mqtt in (Topic: alex9ufo/2026/RFID/Back_LED): 訂閱現場端回傳的狀態訊息(例如收到 "ON""OFF")。

  • function (function LED on ,off): 將字串 "ON" 轉換為布林值 true"OFF" 轉換為 false,用來驅動儀表板上的虛擬燈號(ui_led)。

  • ui_led: 在網頁儀表板上顯示圓形指示燈(true 亮綠燈,false 亮紅燈)。

  • Telegram 串接: 透過 function 節點組裝文字 " ---ESP32 回來 LED的狀態--- " + 狀態值,利用 template 包裝成符合 JSON 格式的 chatId 訊息,最後交由 telegram sender 發送到您的手機。

3. RFID 刷卡資料處理與遠端通知

當現場有人刷卡時,UID 與卡片型態(PICC)會傳送至此:

  • mqtt in (alex9ufo/2026/RFID/RFID_UID): 接收刷卡得到的卡號 UID。

    • 收到後直接顯示在儀表板的 ui_text(欄位:RFID UID -->)。

    • 自動清除機制: 透過 delay 節點延時 10 秒後,經由覆蓋 function 送出空字串 "",讓儀表板上的卡號自動消失,避免畫面一直留著舊資料。

    • Telegram 警報: 同時將卡號包裝,透過 telegram sender 即時傳送 「---ESP32 回來 UID號碼---XXXXXX」到您的手機。

  • mqtt in (alex9ufo/2026/RFID/RFID_PICC): 接收卡片類型(例如 Mifare One S50 等),同樣顯示在儀表板上,並在 10 秒後自動清除。

4. 儀表板維護與圖片顯示

這部分屬於輔助與視覺優化流程:

  • 定時閃爍 Broker 名稱: 使用 inject 啟動後,透過 delay 節點(2秒延遲)與 function on off 形成一個循環迴圈,讓網頁上的文字 emqx.io 定時閃爍(紅字與空白交替),用來識別後端系統是否正常運作。

  • 儀表板背景圖載入: 系統啟動(或每隔10秒)會觸發 file in 讀取本地端路徑下的 EX.png 圖檔,經由 base64 節點編碼,再透過 template 節點轉為 HTML 的 <img src="..."> 標籤,最後在儀表板的 ui_template (MQTT) 區塊顯示出系統架構圖。

    📌 註意事項: 註解節點有特別提醒,如果您的環境更換,必須檢查 file in 節點內的圖片存放路徑(原設定參考路徑為 D:\2024RFID\作業1\EX1.png 相關位置)。

⚙️ 關鍵組態參數摘要

  • MQTT Broker: broker.emqx.io : 1883 (Client ID: node-red-alex-gate)

  • Telegram Bot: 運作於 @alextest999_bot,鎖定發送至特定群組/通訊號碼 (chatId: 7965218469)。

  • 儀表板分頁 (UI Tab): 名稱為 2026RFID,內含 LEDTelegramRFID 三個群組區塊。





ESP32 遠端感應控制系統

ESP32 遠端感應控制系統 目前的架構設計(結合了 ESP32、RFID、MQTT、Node-RED 與 Telegram 遠端雙向控制 ),這個系統的核心價值在於 即時感應、雲端中繼、智慧自動化與即時通訊回報 。 整個架構透過無線網路(Wi-Fi),將現場的硬體感測端、雲端訊...