2026 作業4 WOKWI ESP32 DHT22+4LED & Node-Red , Telegram , MQTT 練習
Node-Red Telegram 設定需修改 TOKEN API 與 CHATID 才會指向你的Telegram
IoT 遠端控制與監測系統。它是一個典型的「雲端代理(MQTT Broker)」架構,讓原本互相隔離的 Wokwi 模擬器、Node-RED 儀表板與 Telegram 手機端能夠即時溝通。
核心通訊流程:MQTT 作為調度中心
整個系統圍繞著 Mqttgo.io 這個 Broker 運作。所有的指令與數據都是透過「訂閱(Subscribe)」與「發布(Publish)」來傳遞。
1. 指令流 (從手機到硬體)
這是一條「下行控制」路徑,主要負責開關 LED:
來源:您在 Telegram 輸入指令(如 /1on)或在 Node-RED Dashboard 按下開關。
中轉:Node-RED 接收到 Telegram 的訊息後,將其轉換為簡單的字串(如 1on),並發布到 MQTT 主題:alex9ufo/ledcontrol。
執行:Wokwi 中的 ESP32 因為訂閱了這個主題,會立刻收到訊息並透過 core0Task 驅動 GPIO 腳位,點亮對應的 LED。
2. 狀態回報流 (從硬體到手機/網頁)
這是一條「確認機制」路徑,確保指令確實執行:
3. 感測器數據流 (DHT22 監測)
這是一條「數據監測」路徑:
觸發:在 Wokwi 序列監控視窗按 Enter,或從 Telegram 發送 readData。
數據:ESP32 讀取 DHT22 後,將字串(Temperature: 5.00C...)發布到:alex9ufo/temphumi。
應用:Node-RED 接收後,透過您寫的 Function 節點 拆分數據,顯示在儀表板的圓形進度條(Gauge)上。
關鍵技術亮點
| 組件 | 主要角色 | 圖片中呈現的功能 |
| Wokwi (ESP32) | 執行器與感測端 | 運行雙核心程式,負責 WiFi 連線、LED 硬體控制與 DHT22 採集。 |
| Node-RED | 大腦與中間層 | 處理邏輯判斷(如溫度 > 70 警報)、介面顯示與 Telegram 訊息轉譯。 |
| Telegram | 遠端介面 | 提供跨地域的文字指令操作,並接收高溫或狀態通知。 |
| MQTT (Mqttgo) | 訊息郵局 | 確保不同網域下的設備能透過 Topic 機制進行雙向通訊。 |
「手動方式要按 Enter」
這點非常重要。由於 Wokwi 環境的限制,DHT22 的數據並非每秒自動狂噴,而是採用被動式觸發:
本地觸發:在模擬器的 Serial 視窗按 Enter。
遠端觸發:從 Telegram 發送 /readData。
這是一個很好的設計,可以避免 MQTT Broker 在模擬測試期間產生過多不必要的數據流量。
Telegram 控制指令
歡迎 Alex 使用 WOKWI&Node-red+Telegram控制系統
/1on : 開啟 LED1
/1off : 關閉 LED1
/2on : 開啟 LED2
/2off : 關閉 LED2
/3on : 開啟 LED3
/3off : 關閉 LED3
/4on : 開啟 LED4
/4off : 關閉 LED4
/allon : 開啟 LED1-LED4
/alloff : 關閉 LED1-LED4
/WOKWI 的 DHT22 輸出 採用 手動方式 要按enter
MQTT Broker與Topic 設定
const char* mqtt_broker = "mqttgo.io";
const int mqtt_port = 1883;
// MQTT 主題
const char* led_control_topic = "alex9ufo/ledcontrol";
const char* led_status_topic = "alex9ufo/ledstatus";
const char* temp_humi_topic = "alex9ufo/temphumi";
ESP32接腳
#define DHTPIN 4 // DHT22 連接到 ESP32 的 D4 腳位
// LED 腳位定義 (根據 diagram.json 設定)
const int ledPins[] = {13, 12, 14, 27}; // 假設連接到 D13, D12, D14, D27
WOKWIESP32程式
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <DHT_U.h>
#include <freertos/FreeRTOS.h> // 引入 FreeRTOS 庫
#include <freertos/task.h> // 引入 FreeRTOS 任務相關庫
// Wi-Fi 憑證
const char* ssid = "Wokwi-GUEST"; // 替換成您的 Wi-Fi 名稱 (例如 Wokwi 的 "Wokwi-GUEST")
const char* password = ""; // 替換成您的 Wi-Fi 密碼 (例如 Wokwi 的空字串 "")
#define DHTPIN 4 // DHT22 連接到 ESP32 的 D4 腳位
#define DHTTYPE DHT22 // DHT 22 (AM2302), AM2321
DHT_Unified dht(DHTPIN, DHTTYPE);
const char* mqtt_broker = "mqttgo.io";
const int mqtt_port = 1883;
const char* mqtt_client_id = "alex9ufo-wokwi-client-dualcore";
// MQTT 主題
const char* led_control_topic = "alex9ufo/ledcontrol";
const char* led_status_topic = "alex9ufo/ledstatus";
const char* temp_humi_topic = "alex9ufo/temphumi";
// LED 腳位定義 (根據 diagram.json 設定)
const int ledPins[] = {13, 12, 14, 27}; // 假設連接到 D13, D12, D14, D27
const int NUM_LEDS = sizeof(ledPins) / sizeof(ledPins[0]);
WiFiClient espClient;
PubSubClient client(espClient);
// 用於跨任務通訊的佇列 (Queue)
// 我們將使用這個佇列來傳遞要發布的 MQTT 訊息
QueueHandle_t mqttPublishQueue;
// 共享變數,用於通知發布任務 DHT22 數據可用
volatile bool dhtReadTriggered = false;
volatile bool dhtReadData = false;
volatile float lastTemperature = 0.0;
volatile float lastHumidity = 0.0;
void setup_wifi() {
delay(10);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
}
// MQTT 訂閱的回調函數 (在 Core 0 執行)
void mqttSubscribeCallback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
payload[length] = '\0'; // 確保字串結束
String message = String((char*)payload);
Serial.println(message);
if (String(topic) == led_control_topic) {
String publishMsg = ""; // 用於發布的訊息
if (message == "1on") {
digitalWrite(ledPins[0], HIGH);
publishMsg = "LED1 ON";
Serial.println("LED1 ON");
} else if (message == "1off") {
digitalWrite(ledPins[0], LOW);
publishMsg = "LED1 OFF";
Serial.println("LED1 OFF");
} else if (message == "2on") {
digitalWrite(ledPins[1], HIGH);
publishMsg = "LED2 ON";
Serial.println("LED2 ON");
} else if (message == "2off") {
digitalWrite(ledPins[1], LOW);
publishMsg = "LED2 OFF";
Serial.println("LED2 OFF");
} else if (message == "3on") {
digitalWrite(ledPins[2], HIGH);
publishMsg = "LED3 ON";
Serial.println("LED3 ON");
} else if (message == "3off") {
digitalWrite(ledPins[2], LOW);
publishMsg = "LED3 OFF";
Serial.println("LED3 OFF");
} else if (message == "4on") {
digitalWrite(ledPins[3], HIGH);
publishMsg = "LED4 ON";
Serial.println("LED4 ON");
} else if (message == "4off") {
digitalWrite(ledPins[3], LOW);
publishMsg = "LED4 OFF";
Serial.println("LED4 OFF");
} else if (message == "allon") {
for (int i = 0; i < NUM_LEDS; i++) {
digitalWrite(ledPins[i], HIGH);
}
publishMsg = "ALL LEDs ON";
Serial.println("ALL LEDs ON");
} else if (message == "alloff") {
for (int i = 0; i < NUM_LEDS; i++) {
digitalWrite(ledPins[i], LOW);
}
publishMsg = "ALL LEDs OFF";
Serial.println("ALL LEDs OFF");
} else if (message == "readData") {
Serial.println("讀取DHT22 Data 從Telehgram送出命令");
publishMsg = "讀取DHT22 Data";
dhtReadData = true;
}
// 將要發布的訊息放入佇列,由發布任務處理
if (publishMsg.length() > 0) {
char msgBuffer[50];
publishMsg.toCharArray(msgBuffer, sizeof(msgBuffer));
xQueueSend(mqttPublishQueue, &msgBuffer, portMAX_DELAY);
}
}
}
void reconnect() {
// Loop until we're reconnected
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// Attempt to connect
if (client.connect(mqtt_client_id)) {
Serial.println("connected");
// Once connected, publish an announcement...
char initialMsg[] = "Wokwi LED controller connected (Dual Core)";
xQueueSend(mqttPublishQueue, &initialMsg, portMAX_DELAY); // 將初始化訊息放入佇列
// ... and resubscribe
client.subscribe(led_control_topic);
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
// Wait 5 seconds before retrying
delay(5000);
}
}
}
// DHT22 讀取函數 (在 Core 0 觸發,數據由發布任務處理)
void readDHT22() {
sensors_event_t event;
dht.temperature().getEvent(&event);
if (isnan(event.temperature)) {
Serial.println(F("Error reading temperature!"));
} else {
lastTemperature = event.temperature; // 更新共享變數
dht.humidity().getEvent(&event);
if (isnan(event.relative_humidity)) {
Serial.println(F("Error reading humidity!"));
} else {
lastHumidity = event.relative_humidity; // 更新共享變數
dhtReadTriggered = true; // 設定旗標,通知發布任務有新數據
}
}
}
// Core 0 任務:處理 MQTT 訂閱和 Serial Monitor 輸入
void core0Task(void * parameter) {
Serial.println("Core 0 Task running: Handling MQTT Subscriptions and Serial Input.");
client.setCallback(mqttSubscribeCallback); // 設定訂閱回調函數
while (true) {
if (!client.connected()) {
reconnect();
}
client.loop(); // 處理 MQTT 訂閱訊息
// 監聽 Serial Monitor 輸入
if (Serial.available()) {
String command = Serial.readStringUntil('\n');
command.trim(); // 移除前後空白
if (command == "") { // 當使用者按下 Enter (輸入空字串)
readDHT22(); // 讀取 DHT22
}
}
if (dhtReadData) {
dhtReadData=false;
readDHT22(); // 讀取 DHT22
}
vTaskDelay(10 / portTICK_PERIOD_MS); // 短暫延遲,讓其他任務有執行機會
}
}
// Core 1 任務:處理 MQTT 發布
void core1Task(void * parameter) {
Serial.println("Core 1 Task running: Handling MQTT Publications.");
char msgBuffer[50];
while (true) {
// 檢查是否有要從佇列發布的訊息
if (xQueueReceive(mqttPublishQueue, &msgBuffer, 0) == pdTRUE) {
if (client.connected()) {
if (strcmp(msgBuffer, "LED1 ON") == 0 || strcmp(msgBuffer, "LED1 OFF") == 0 ||
strcmp(msgBuffer, "LED2 ON") == 0 || strcmp(msgBuffer, "LED2 OFF") == 0 ||
strcmp(msgBuffer, "LED3 ON") == 0 || strcmp(msgBuffer, "LED3 OFF") == 0 ||
strcmp(msgBuffer, "LED4 ON") == 0 || strcmp(msgBuffer, "LED4 OFF") == 0 ||
strcmp(msgBuffer, "ALL LEDs ON") == 0 || strcmp(msgBuffer, "ALL LEDs OFF") == 0) {
client.publish(led_status_topic, msgBuffer);
Serial.print("Published to led_status_topic: ");
Serial.println(msgBuffer);
}
}
}
// 檢查是否有 DHT22 新數據要發布
if (dhtReadTriggered) {
if (client.connected()) {
String temp_humi_msg = "Temperature: " + String(lastTemperature) + "C, Humidity: " + String(lastHumidity) + "%";
client.publish(temp_humi_topic, temp_humi_msg.c_str());
Serial.print("Published to temp_humi_topic: ");
Serial.println(temp_humi_msg);
}
dhtReadTriggered = false; // 重置旗標
}
vTaskDelay(50 / portTICK_PERIOD_MS); // 適度延遲,避免佔用所有 CPU 資源
}
}
void setup() {
Serial.begin(115200);
for (int i = 0; i < NUM_LEDS; i++) {
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], LOW); // 初始化所有 LED 為關閉
}
dht.begin();
sensor_t sensor;
dht.temperature().getSensor(&sensor);
Serial.println(F("------------------------------------"));
Serial.println(F("Unified Sensor Example"));
Serial.print(F("Sensor: ")); Serial.println(sensor.name);
Serial.print(F("Driver Ver: ")); Serial.println(sensor.version);
Serial.print(F("Unique ID: ")); Serial.println(sensor.sensor_id);
Serial.print(F("Max Value: ")); Serial.print(sensor.max_value); Serial.println(F(" C"));
Serial.print(F("Min Value: ")); Serial.print(sensor.min_value); Serial.println(F(" C"));
Serial.print(F("Resolution: ")); Serial.print(sensor.resolution); Serial.println(F(" C"));
Serial.println(F("------------------------------------"));
setup_wifi();
client.setServer(mqtt_broker, mqtt_port);
// 創建用於跨任務通訊的佇列
mqttPublishQueue = xQueueCreate(10, sizeof(char[50])); // 佇列深度為 10,每個元素為 50 字元的 char 陣列
// 創建 Core 0 任務 (處理訂閱)
xTaskCreatePinnedToCore(
core0Task, // 任務函數
"Core0Task", // 任務名稱
10000, // 任務堆疊大小 (位元組)
NULL, // 傳遞給任務的參數
1, // 任務優先級 (較高優先級)
NULL, // 任務句柄
0 // 運行在 Core 0
);
// 創建 Core 1 任務 (處理發布)
xTaskCreatePinnedToCore(
core1Task, // 任務函數
"Core1Task", // 任務名稱
10000, // 任務堆疊大小 (位元組)
NULL, // 傳遞給任務的參數
1, // 任務優先級
NULL, // 任務句柄
1 // 運行在 Core 1
);
}
void loop() {
// loop() 函數在 FreeRTOS 環境下,如果所有任務都已建立,通常會保持空閒。
// 所有邏輯都將在各自的任務中運行。
vTaskDelete(NULL); // 刪除 loop 任務本身,將控制權完全交給 FreeRTOS
}
這份程式碼是一個非常進階的 ESP32 雙核心 (Dual Core) 應用範例,利用了 ESP32 內部的兩個處理核心(Core 0 與 Core 1)並結合 FreeRTOS 作業系統來分工處理物聯網任務。
其核心邏輯是:Core 0 負責「聽」指令(MQTT 訂閱與 Serial 輸入),Core 1 負責「說」話(MQTT 數據發布)。
程式碼逐行詳解
1. 引用庫與定義 (Includes & Defines)
WiFi.h & PubSubClient.h: 處理 Wi-Fi 連線與 MQTT 協定。
DHT.h & DHT_U.h: 使用 Adafruit 的統一感測器庫來讀取 DHT22 溫濕度。
freertos/FreeRTOS.h & task.h: 這是關鍵,引入 FreeRTOS 核心功能,讓 ESP32 能夠執行多工任務。
DHTPIN 4: 定義 DHT22 接在 GPIO 4。
2. 全域變數與跨核心通訊
3. MQTT 回調函數 (處理接收到的指令)
mqttSubscribeCallback: 當 Node-RED 或 Telegram 傳送指令(如 1on, alloff)到 ledcontrol 主題時,這個函數會被觸發。
指令判斷: 程式會比對 message,執行 digitalWrite 切換 LED 電位。
xQueueSend: 執行完動作後,將「LED1 ON」等回應文字塞進 mqttPublishQueue 佇列,交給 Core 1 去回報狀態。
4. 雙核心任務 (Tasks)
Core 0 任務:core0Task (處理「入」的資料)
client.loop(): 這是 MQTT 的心臟,負責檢查是否有新訊息進來。
Serial 監聽: 檢查電腦序列埠輸入。如果你在 Wokwi 按下 Enter(空字串),它會觸發 readDHT22()。
dhtReadData: 如果從 Telegram 收到 readData 指令,也會觸發讀取。
Core 1 任務:core1Task (處理「出」的資料)
5. 初始化與核心分配 (setup)
6. 主迴圈 (loop)
程式優點總結
非阻塞 (Non-blocking):傳統做法如果 Wi-Fi 斷線,整個程式會卡在 reconnect 裡,LED 就無法控制。但在這個版本中,Core 0 依然可以處理本地指令。
即時性:利用佇列(Queue)緩衝訊息,避免在高頻率傳輸時遺失數據。
靈活性:支援手動(Enter 鍵)與遠端(Telegram 指令)兩種方式觸發 DHT22 讀取。
Node-Red程式

[{"id":"aa38bbd69640e2ae","type":"telegram receiver","z":"8c2053d55f5883f8","name":"","bot":"457874f8aa8a857a","saveDataDir":"","filterCommands":true,"x":130,"y":40,"wires":[["eb8234cf9675d55a"],[]]},{"id":"60bd77976d9b25a8","type":"function","z":"8c2053d55f5883f8","name":"根據指令發佈 MQTT 訊息","func":"if (msg.payload === \"/1on\") {\n msg.topic = \"alex9ufo/ledcontrol\";\n msg.payload = \"1on\"; // 開啟 LED1\n return [msg , null];\n} else if (msg.payload === \"/1off\") {\n msg.topic = \"alex9ufo/ledcontrol\";\n msg.payload = \"1off\"; // 關閉 LED1\n return [msg , null];\n} else if (msg.payload === \"/2on\") {\n msg.topic = \"alex9ufo/ledcontrol\";\n msg.payload = \"2on\"; // 開啟 LED2\n return [msg , null];\n} else if (msg.payload === \"/2off\") {\n msg.topic = \"alex9ufo/ledcontrol\";\n msg.payload = \"2off\"; // 關閉 LED2\n return [msg , null];\n} else if (msg.payload === \"/3on\") {\n msg.topic = \"alex9ufo/ledcontrol\";\n msg.payload = \"3on\"; // 開啟 LED3\n return [msg , null];\n} else if (msg.payload === \"/3off\") {\n msg.topic = \"alex9ufo/ledcontrol\";\n msg.payload = \"3off\"; // 關閉 LED3\n return [msg , null];\n} else if (msg.payload === \"/4on\") {\n msg.topic = \"alex9ufo/ledcontrol\";\n msg.payload = \"4on\"; // 開啟 LED4\n return [msg , null];\n} else if (msg.payload === \"/4off\") {\n msg.topic = \"alex9ufo/ledcontrol\";\n msg.payload = \"4off\"; // 關閉 LED4\n return [msg , null];\n} else if (msg.payload === \"/allon\") {\n msg.topic = \"alex9ufo/ledcontrol\";\n msg.payload = \"allon\"; // 開啟 all LED1-LED4\n return [msg , null];\n} else if (msg.payload === \"/alloff\") {\n msg.topic = \"alex9ufo/ledcontrol\";\n msg.payload = \"alloff\"; // 關閉 all LED1-LED4\n return [msg , null];\n} else if (msg.payload === \"/WOKWI\") {\n msg.topic = \"alex9ufo/ledcontrol\";\n msg.payload = \"readData\"; // 讀取DHT22溫度濕度\n return [msg, null];\n} \n\n//function 根據指令來發佈 MQTT 訊息","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":40,"wires":[["2579e03bbb9008db","3f077821c764e91d"],[]]},{"id":"2579e03bbb9008db","type":"mqtt out","z":"8c2053d55f5883f8","name":"alex9ufo/ledcontrol","topic":"alex9ufo/ledcontrol","qos":"1","retain":"true","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"192c2b20bef1e71a","x":790,"y":60,"wires":[]},{"id":"f04e44b6863fba43","type":"mqtt in","z":"8c2053d55f5883f8","name":"","topic":"alex9ufo/ledstatus","qos":"2","datatype":"auto-detect","broker":"192c2b20bef1e71a","nl":false,"rap":true,"rh":0,"inputs":0,"x":130,"y":260,"wires":[["c4c0948da43caba8","81ada557b64dc604","5df891b22fcf0d78","d20c7a1c503d7b74","9a49877e65ede3d6"]]},{"id":"eb8234cf9675d55a","type":"function","z":"8c2053d55f5883f8","name":"function","func":"var content= msg.payload.content;\nmsg.payload=content;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":40,"wires":[["60bd77976d9b25a8","85fff350d422330f"]]},{"id":"7285f33b22258e53","type":"ui_led","z":"8c2053d55f5883f8","order":1,"group":"9123b13576984e5e","width":3,"height":3,"label":"LED1","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED1","x":570,"y":180,"wires":[]},{"id":"c4c0948da43caba8","type":"function","z":"8c2053d55f5883f8","name":"function","func":"var content = msg.payload;\n\nif (content === 'LED1 ON') {\n msg.payload = true;\n // Route to output 1\n return [msg, null, null, null ];\n}\nif (content === 'LED1 OFF') {\n msg.payload = false;\n // Route to output 1\n return [msg, null, null, null];\n}\nif (content === 'LED2 ON') { \n msg.payload = true;\n // Route to output 2\n return [null, msg, null, null];\n}\nif (content === 'LED2 OFF') {\n msg.payload = false;\n // Route to output 2\n return [null, msg, null, null];\n}\nif (content === 'LED3 ON') { \n msg.payload = true;\n // Route to output 3\n return [null, null, msg, null];\n}\nif (content === 'LED3 OFF') {\n msg.payload = false;\n // Route to output 3\n return [null, null, msg, null];\n}\nif (content === 'LED4 ON') {\n msg.payload = true;\n // Route to output 4\n return [null, null, null, msg];\n}\nif (content === 'LED4 OFF') {\n msg.payload = false;\n // Route to output 4\n return [null, null, null, msg];\n}\n\nif (content === 'ALL LEDs ON') {\n msg.payload = true;\n // Route to output 1-4\n return [msg, msg, msg, msg];\n}\nif (content === 'ALL LEDs OFF') {\n msg.payload = false;\n // Route to output 1-4\n return [msg, msg, msg, msg];\n}\n\n\n\nreturn null;","outputs":4,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":260,"wires":[["7285f33b22258e53"],["38f4e014de0658f6"],["41c06dba6c2f63a0"],["be68d0fe89c4fb10"]]},{"id":"b7fcc81b89152e02","type":"telegram sender","z":"8c2053d55f5883f8","name":"","bot":"457874f8aa8a857a","haserroroutput":false,"outputs":1,"x":510,"y":360,"wires":[[]]},{"id":"ede1cfbc300148a2","type":"debug","z":"8c2053d55f5883f8","name":"debug 371","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":490,"y":400,"wires":[]},{"id":"81ada557b64dc604","type":"template","z":"8c2053d55f5883f8","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\"chatId\": 7965218469,\n\"type\":\"message\",\n\"content\":\"{{payload}}\"}","output":"json","x":290,"y":360,"wires":[["b7fcc81b89152e02","ede1cfbc300148a2"]]},{"id":"e3993085c203bdba","type":"comment","z":"8c2053d55f5883f8","name":"MQTT 設定","info":"**const char* mqtt_broker = \"mqttgo.io\";\nconst int mqtt_port = 1883;\nconst char* mqtt_client_id = \"alex9ufo-wokwi-client-dualcore\";\n\n// MQTT 主題\nconst char* led_control_topic = \"alex9ufo/ledcontrol\";\nconst char* led_status_topic = \"alex9ufo/ledstatus\";\nconst char* temp_humi_topic = \"alex9ufo/temphumi\"; ","x":110,"y":360,"wires":[]},{"id":"38f4e014de0658f6","type":"ui_led","z":"8c2053d55f5883f8","order":2,"group":"9123b13576984e5e","width":3,"height":3,"label":"LED2","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED2","x":570,"y":220,"wires":[]},{"id":"41c06dba6c2f63a0","type":"ui_led","z":"8c2053d55f5883f8","order":3,"group":"9123b13576984e5e","width":3,"height":3,"label":"LED3","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED3","x":570,"y":260,"wires":[]},{"id":"be68d0fe89c4fb10","type":"ui_led","z":"8c2053d55f5883f8","order":4,"group":"9123b13576984e5e","width":3,"height":3,"label":"LED4","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED4","x":570,"y":300,"wires":[]},{"id":"3f077821c764e91d","type":"debug","z":"8c2053d55f5883f8","name":"debug 372","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":770,"y":20,"wires":[]},{"id":"5df891b22fcf0d78","type":"ui_text","z":"8c2053d55f5883f8","group":"9123b13576984e5e","order":5,"width":0,"height":0,"name":"","label":"LED狀態","format":"{{msg.payload}}","layout":"row-left","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":340,"y":320,"wires":[]},{"id":"fa7ececa4e716634","type":"mqtt in","z":"8c2053d55f5883f8","name":"alex9ufo/temphumi","topic":"alex9ufo/temphumi","qos":"1","datatype":"auto-detect","broker":"192c2b20bef1e71a","nl":false,"rap":true,"rh":0,"inputs":0,"x":110,"y":560,"wires":[["3a41c8a502d904a4","bdfcb0188f2a45d1","093cc1f43bb2193f"]]},{"id":"3a41c8a502d904a4","type":"template","z":"8c2053d55f5883f8","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\"chatId\": 7965218469,\n\"type\":\"message\",\n\"content\":\"{{payload}}\"}","output":"json","x":530,"y":560,"wires":[["6aa0d1e45462c3ef"]]},{"id":"6aa0d1e45462c3ef","type":"telegram sender","z":"8c2053d55f5883f8","name":"","bot":"457874f8aa8a857a","haserroroutput":false,"outputs":1,"x":710,"y":560,"wires":[[]]},{"id":"d20c7a1c503d7b74","type":"debug","z":"8c2053d55f5883f8","name":"debug 373","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":330,"y":200,"wires":[]},{"id":"bdfcb0188f2a45d1","type":"function","z":"8c2053d55f5883f8","name":" 分離 溫度 濕度","func":"// 取得原始字串,例如 \"Temperature: 5.00C, Humidity: 88.50%\"\nvar input = msg.payload.toString();\n\n// 使用正規表示法抓取數值\n// ([-+]?\\d*\\.?\\d+) 負責抓取包含正負號、整數與小數點的數字\nvar tempMatch = input.match(/Temperature:\\s*([-+]?\\d*\\.?\\d+)/);\nvar humiMatch = input.match(/Humidity:\\s*(\\d*\\.?\\d+)/);\n\nvar msgTemp = null;\nvar msgHumi = null;\n\n// 處理溫度 (輸出端 1)\nif (tempMatch) {\n var temp = parseFloat(tempMatch[1]);\n // 檢查是否在指定範圍內 -40 到 +80\n if (temp >= -40 && temp <= 80) {\n msgTemp = { \n payload: temp, \n unit: \"C\",\n topic: \"sensor/temperature\" \n };\n } else {\n node.warn(\"溫度數值異常: \" + temp);\n }\n}\n\n// 處理濕度 (輸出端 2)\nif (humiMatch) {\n var humi = parseFloat(humiMatch[1]);\n msgHumi = { \n payload: humi, \n unit: \"%\",\n topic: \"sensor/humidity\" \n };\n}\n\n// 依照 [輸出 1, 輸出 2] 的順序回傳\nreturn [msgTemp, msgHumi];","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":760,"wires":[["253c856683bddece","fef1f76b83d92299"],["f8e06b4a74159f3b","4ec634057e02a3b6"]]},{"id":"253c856683bddece","type":"ui_text","z":"8c2053d55f5883f8","group":"873c37d1e2856b34","order":1,"width":3,"height":1,"name":"","label":"溫度:","format":"{{msg.payload}}℃","layout":"row-left","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":530,"y":720,"wires":[]},{"id":"f8e06b4a74159f3b","type":"ui_text","z":"8c2053d55f5883f8","group":"873c37d1e2856b34","order":2,"width":3,"height":1,"name":"","label":"濕度","format":"{{msg.payload}} %","layout":"row-left","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":530,"y":800,"wires":[]},{"id":"fef1f76b83d92299","type":"ui_gauge","z":"8c2053d55f5883f8","name":"","group":"873c37d1e2856b34","order":3,"width":6,"height":3,"gtype":"compass","title":"溫度","label":"℃","format":"{{value}}","min":"-40","max":"80","colors":["#00b500","#e6e600","#ca3838"],"seg1":"20","seg2":"40","diff":false,"className":"","x":530,"y":680,"wires":[]},{"id":"4ec634057e02a3b6","type":"ui_gauge","z":"8c2053d55f5883f8","name":"","group":"873c37d1e2856b34","order":4,"width":6,"height":3,"gtype":"gage","title":"濕度:","label":"%","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"45","seg2":"60","diff":false,"className":"","x":530,"y":840,"wires":[]},{"id":"b4f570bf787abca2","type":"function","z":"8c2053d55f5883f8","name":"(溫度 > 30°C 與 > 70°C) ","func":"// 1. 取得原始字串:Temperature: 75.00C, Humidity: 88.50%\nvar input = msg.payload;\nvar message = \"\";\n\n// 2. 使用正規表示法抓取溫度數值\nvar tempMatch = input.match(/Temperature: ([\\d.]+)/);\nvar humiMatch = input.match(/Humidity: ([\\d.]+)/);\n\nif (tempMatch) {\n var temp1 = parseFloat(tempMatch[1]); // 將字串轉為數字 75.00\n \n // 3. 判斷溫度門檻 (先判斷最高的,避免重複觸發)\n if (temp1 > 70) {\n message = \"🔥 【緊急火警警報】偵測到極高溫!\\n目前溫度:\" + temp1 + \"°C\\n請立刻採取行動!\";\n }\n else if (temp1 > 30) {\n message = \"⚠️ 【高溫提醒】環境溫度過高。\\n目前溫度:\" + temp1 + \"°C\\n請注意散熱。\";\n }\n}\nif (humiMatch) {\n var temp2 = parseFloat(humiMatch[1]); // 將字串轉為數字 75.00\n\n // 3. 判斷溫度門檻 (先判斷最高的,避免重複觸發)\n if (temp2 > 80) {\n message = \"🌧️ 【緊急警報】偵測到濕度不正常!\\n目前濕度:\" + temp2 + \"%\\n請立刻採取行動!\";\n }\n else if (temp2 <10) {\n message = \"🌧️ 【緊急警報】偵測到濕度不正常。\\n目前濕度:\" + temp2 + \"%\\n請注意散熱。\";\n }\n}\n\nmsg.payload=message;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":500,"y":620,"wires":[["3a41c8a502d904a4","24a5e9920dbfad71"]]},{"id":"093cc1f43bb2193f","type":"delay","z":"8c2053d55f5883f8","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":300,"y":620,"wires":[["b4f570bf787abca2"]]},{"id":"24a5e9920dbfad71","type":"play audio","z":"8c2053d55f5883f8","name":"","voice":"","x":710,"y":640,"wires":[]},{"id":"d5ee749083c5c5a2","type":"telegram sender","z":"8c2053d55f5883f8","name":"","bot":"457874f8aa8a857a","haserroroutput":false,"outputs":1,"x":530,"y":500,"wires":[[]]},{"id":"86f6b5808a905449","type":"template","z":"8c2053d55f5883f8","name":"Telegram 歡迎詞","field":"payload","fieldType":"msg","format":"handlebars","syntax":"plain","template":"{\"chatId\": 7965218469,\n\"type\":\"message\",\n\"content\":\"歡迎 Alex 使用 WOKWI&Node-red+Telegram控制系統\\n/1on : 開啟 LED1\\n/1off : 關閉 LED1\\n/2on : 開啟 LED2\\n/2off : 關閉 LED2\\n/3on : 開啟 LED3\\n/3off : 關閉 LED3\\n/4on : 開啟 LED4\\n/4off : 關閉 LED4\\n/allon : 開啟 LED1-LED4\\n/alloff : 關閉 LED1-LED4\\n/WOKWI 的 DHT22 輸出 採用 手動方式 要按enter\\n\"}","output":"json","x":300,"y":500,"wires":[["d5ee749083c5c5a2"]]},{"id":"47e254e9be34dfbc","type":"inject","z":"8c2053d55f5883f8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":500,"wires":[["86f6b5808a905449"]]},{"id":"85fff350d422330f","type":"debug","z":"8c2053d55f5883f8","name":"debug 375","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":470,"y":80,"wires":[]},{"id":"9a49877e65ede3d6","type":"play audio","z":"8c2053d55f5883f8","name":"","voice":"","x":330,"y":160,"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":"192c2b20bef1e71a","type":"mqtt-broker","name":"mqttgo","broker":"mqttgo.io","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":"9123b13576984e5e","type":"ui_group","name":"LED","tab":"ed8b3742f5900395","order":2,"disp":true,"width":"6","collapse":false,"className":""},{"id":"873c37d1e2856b34","type":"ui_group","name":"DHT22","tab":"ed8b3742f5900395","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"ed8b3742f5900395","type":"ui_tab","name":"2026_09_EX4","icon":"dashboard","disabled":false,"hidden":false}]

它專門負責接收來自 Telegram 的文字指令,並將其轉換成 MQTT 訊息發送給 Wokwi 裡的 ESP32。
以下是該流程(Flow)的詳細解析:
程式節點解析
1. 指令接收端 (Telegram Receiver)
2. 資料預處理 (function 節點 - eb8234cf9675d55a)
if (msg.payload === "/1on") {
msg.topic = "alex9ufo/ledcontrol";
msg.payload = "1on"; // 開啟 LED1
return [msg , null];
} else if (msg.payload === "/1off") {
msg.topic = "alex9ufo/ledcontrol";
msg.payload = "1off"; // 關閉 LED1
return [msg , null];
} else if (msg.payload === "/2on") {
msg.topic = "alex9ufo/ledcontrol";
msg.payload = "2on"; // 開啟 LED2
return [msg , null];
} else if (msg.payload === "/2off") {
msg.topic = "alex9ufo/ledcontrol";
msg.payload = "2off"; // 關閉 LED2
return [msg , null];
} else if (msg.payload === "/3on") {
msg.topic = "alex9ufo/ledcontrol";
msg.payload = "3on"; // 開啟 LED3
return [msg , null];
} else if (msg.payload === "/3off") {
msg.topic = "alex9ufo/ledcontrol";
msg.payload = "3off"; // 關閉 LED3
return [msg , null];
} else if (msg.payload === "/4on") {
msg.topic = "alex9ufo/ledcontrol";
msg.payload = "4on"; // 開啟 LED4
return [msg , null];
} else if (msg.payload === "/4off") {
msg.topic = "alex9ufo/ledcontrol";
msg.payload = "4off"; // 關閉 LED4
return [msg , null];
} else if (msg.payload === "/allon") {
msg.topic = "alex9ufo/ledcontrol";
msg.payload = "allon"; // 開啟 all LED1-LED4
return [msg , null];
} else if (msg.payload === "/alloff") {
msg.topic = "alex9ufo/ledcontrol";
msg.payload = "alloff"; // 關閉 all LED1-LED4
return [msg , null];
} else if (msg.payload === "/WOKWI") {
msg.topic = "alex9ufo/ledcontrol";
msg.payload = "readData"; // 讀取DHT22溫度濕度
return [msg, null];
}
//function 根據指令來發佈 MQTT 訊息
3. 指令判斷中心 (function 節點 - 60bd77976d9b25a8)
這是整個流程的大腦,負責**「翻譯」**工作。它將 Telegram 的斜線指令轉換成 ESP32 能理解的簡單字串:
| Telegram 指令 | 發佈到 MQTT 的內容 (Payload) | 動作說明 |
/1on 到 /4on | 1on ... 4on | 開啟指定的 LED 1~4 |
/1off 到 /4off | 1off ... 4off | 關閉指定的 LED 1~4 |
| /allon | allon | 開啟所有 LED |
| /alloff | alloff | 關閉所有 LED |
| /WOKWI | readData | 觸發 ESP32 讀取 DHT22 數據 |
4. 指令發送端 (MQTT Out)
主題 (Topic):alex9ufo/ledcontrol
功能:將翻譯好的指令(如 readData)送到 mqttgo.io 伺服器。
設定:qos: 1 確保指令至少送達一次,retain: true 則是讓之後連線的 ESP32 也能收到最後一次的狀態指令。
程式碼是系統中的**「狀態反饋與顯示中心」**。它的主要工作是監聽來自 ESP32 的狀態回報訊息(LED 到底是開還是關),然後更新 Dashboard 上的燈號,並同步傳送訊息到您的 Telegram 手機上。
這是一個非常完整的雙向溝通循環。以下是逐行與逐節點的詳細說明:
1. 數據入口:MQTT In 節點
2. 核心邏輯:Function 節點 (c4c0948da43caba8)
這是此流程的大腦,負責將文字訊號轉換為布林值 (True/False) 並分流:
分流設計:它設定了 4 個輸出端,分別對應 4 顆 LED。
邏輯範例:
全開/全關處理:
關鍵代碼:return [msg, null, null, null] 代表只讓第一路有訊號,其餘保持靜默。
var content = msg.payload;
if (content === 'LED1 ON') {
msg.payload = true;
// Route to output 1
return [msg, null, null, null ];
}
if (content === 'LED1 OFF') {
msg.payload = false;
// Route to output 1
return [msg, null, null, null];
}
if (content === 'LED2 ON') {
msg.payload = true;
// Route to output 2
return [null, msg, null, null];
}
if (content === 'LED2 OFF') {
msg.payload = false;
// Route to output 2
return [null, msg, null, null];
}
if (content === 'LED3 ON') {
msg.payload = true;
// Route to output 3
return [null, null, msg, null];
}
if (content === 'LED3 OFF') {
msg.payload = false;
// Route to output 3
return [null, null, msg, null];
}
if (content === 'LED4 ON') {
msg.payload = true;
// Route to output 4
return [null, null, null, msg];
}
if (content === 'LED4 OFF') {
msg.payload = false;
// Route to output 4
return [null, null, null, msg];
}
if (content === 'ALL LEDs ON') {
msg.payload = true;
// Route to output 1-4
return [msg, msg, msg, msg];
}
if (content === 'ALL LEDs OFF') {
msg.payload = false;
// Route to output 1-4
return [msg, msg, msg, msg];
}
return null;
3. 視覺化顯示:UI LED 節點
包含節點:LED1 (7285f33b), LED2, LED3, LED4。
功能:在您的 Node-RED Dashboard 網頁上顯示圓形指示燈。
設定:
群組:全部歸類在名為 LED 的群組,放在 2026_09_EX4 分頁中。
4. Telegram 同步:Template + Telegram Sender
這是讓您的手機同步收到狀態通知的部分:
5. 輔助功能節點
UI Text 節點 (5df891b22fcf0d78):在 Dashboard 上用文字直接顯示「LED1 ON」等字樣,方便閱讀。
Play Audio 節點 (9a49877e65ede3d6):這很有趣!每當狀態改變時,您的瀏覽器會發出聲音提醒,這就是您之前想做的「警報聲」基礎。
Debug 節點:在側邊欄顯示 Log,方便您檢查 MQTT 連線是否正常。
流程總結與圖解
ESP32 發送 "LED1 ON"。
MQTT In 接收訊息。
Function 判斷這是 LED1,將其轉為 true 並從第 1 路輸出。
Dashboard 上的 LED1 燈泡由紅轉綠。
Telegram 同步彈出視窗顯示:「LED1 ON」。
程式碼的功能是系統的**「自動歡迎與說明書發佈器」**。每當 Node-RED 程式啟動或您重新部署(Deploy)時,它會自動發送一則包含完整指令清單的訊息到您的 Telegram 手機上,讓您不需要背誦指令。
以下是針對這四個節點的逐行與運作邏輯說明:
1. 觸發源:Inject 節點 (47e254e9be34dfbc)
2. 內容產生器:Template 節點 (86f6b5808a905449)
3. 發送執行端:Telegram Sender 節點 (d5ee749083c5c5a2)
4. 機器人設定:Telegram Bot 節點 (457874f8aa8a857a)
流程運作圖解
Node-RED 啟動。
Inject 節點 倒數 0.1 秒後發出訊號。
Template 節點 準備好「歡迎 Alex...」這串長文字。
Telegram Sender 透過網路將文字推送到您的手機。
Alex 的手機 響起「叮」一聲,完整的指令選單出現在畫面。

Node-RED 程式碼是您系統中的**「數據監測與警報中心」**。它負責接收溫濕度、進行數據解析(拆分數字)、顯示在 Dashboard 儀表板,並在數值異常(過熱或太濕)時觸發 Telegram 與聲音報警。
以下是針對每個區塊的逐行解析:
1. 數據入口:MQTT In 節點 (fa7ececa4e716634)
2. 視覺化核心:數據分離 Function (bdfcb0188f2a45d1)
這是負責將原始字串拆解為「純數字」的關鍵節點,設定為 2 個輸出端:
正規表示法解析:使用 input.match 抓取 Temperature: 和 Humidity: 後面的數字。
範圍限制:設定溫度必須在 -40°C 到 +80°C 之間才算有效,否則會發出 node.warn 警告。
輸出 1:傳送純溫度數字給 Dashboard 的 溫度計 (Gauge) 與 文字 (Text) 節點。
輸出 2:傳送純濕度數字給 濕度計。
3. 警報決策中心:高溫/濕度判斷 Function (b4f570bf787abca2)
這個節點負責檢查數值是否超標:
4. 警報發佈端
5. Dashboard 介面元件 (UI)
溫度 Gauge (fef1f76b83d92299):採用 Compass (羅盤型) 顯示溫度,範圍 -40 到 80。
濕度 Gauge (4ec634057e02a3b6):採用標準儀表型,範圍 0 到 100,並用顏色區分(綠、黃、紅)。
UI Text 節點:在儀表下方顯示精確的數字與單位。
流程運作圖解
ESP32 發送:Temperature: 75.00C, Humidity: 85.00%。
分離 Function:把數字 75 給溫度計,數字 85 給濕度計。
判斷 Function:
偵測到 75 > 70,生成「火警警報」訊息。
偵測到 85 > 80,生成「濕度異常」訊息。
執行警報:手機 Telegram 響起通知,電腦喇叭發出警報聲。