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. 狀態回報流 (從硬體到手機/網頁)
這是一條「確認機制」路徑,確保指令確實執行:
來源:ESP32 成功切換 LED 電位後,由
core1Task將結果(如LED1 ON)發布到:alex9ufo/ledstatus。顯示:Node-RED 訂閱此主題後,更新儀表板上的指示燈顏色,並將成功訊息回傳給 Telegram。
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;
WOKWIESP32程式
#include <WiFi.h>
這份程式碼是一個非常進階的 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. 全域變數與跨核心通訊
QueueHandle_t mqttPublishQueue: 這是 「佇列」。因為兩個核心不能直接隨意存取對方的區域變數,所以建立一個「傳聲筒」,Core 0 把要發出的訊息丟進去,Core 1 再從裡面拿出來發送。volatile bool dhtReadTriggered: 加上volatile關鍵字,告訴編譯器這個變數會在不同的核心(任務)之間變動,確保讀取到的是最新值。
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 (處理「出」的資料)
xQueueReceive: 持續檢查「傳聲筒(佇列)」裡有沒有訊息。如果有(如:初始化訊息或 LED 狀態),就執行client.publish發送出去。dhtReadTriggered檢查: 當 Core 0 完成 DHT22 讀取後會舉起這個旗標,Core 1 看到後就會將溫度與濕度組合成字串,發布到temphumi主題。
5. 初始化與核心分配 (setup)
xQueueCreate: 初始化佇列空間。xTaskCreatePinnedToCore: 這是最重要的一步:建立 Core0Task 並指派給 Core 0。
建立 Core1Task 並指派給 Core 1。
這確保了當感測器讀取或 Wi-Fi 重連時,不會卡住 LED 的控制反應。
6. 主迴圈 (loop)
vTaskDelete(NULL): 在典型的 FreeRTOS 程式中,我們不在loop()裡寫程式。這行代碼會刪除原本 Arduino 預設的 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)
節點名稱:
telegram receiver功能:監聽您的 Telegram 機器人(@alextest999_bot)。當您在手機輸入任何文字時,這個節點會抓取訊息物件。
關鍵屬性:
filterCommands: true。這代表它會特別留意以/開頭的指令。
2. 資料預處理 (function 節點 - eb8234cf9675d55a)
功能:提取純文字內容。
邏輯:Telegram 傳來的原始資料是一個複雜的物件(包含使用者名稱、時間、ID等),這段代碼
var content = msg.payload.content;只取出您輸入的指令文字(例如/1on),並將其重新存入msg.payload中,方便後續判斷。
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 數據 |
輸出設計:這個節點設有 2 個輸出端,但目前程式碼主要回傳
[msg, null],代表訊息會從第一個輸出端流向 MQTT。
4. 指令發送端 (MQTT Out)
主題 (Topic):
alex9ufo/ledcontrol功能:將翻譯好的指令(如
readData)送到 mqttgo.io 伺服器。設定:
qos: 1確保指令至少送達一次,retain: true則是讓之後連線的 ESP32 也能收到最後一次的狀態指令。
程式碼是系統中的**「狀態反饋與顯示中心」**。它的主要工作是監聽來自 ESP32 的狀態回報訊息(LED 到底是開還是關),然後更新 Dashboard 上的燈號,並同步傳送訊息到您的 Telegram 手機上。
這是一個非常完整的雙向溝通循環。以下是逐行與逐節點的詳細說明:
1. 數據入口:MQTT In 節點
節點名稱:
alex9ufo/ledstatus功能:這是系統的耳朵。它訂閱了 MQTT Broker (
mqttgo.io) 上的主題。運作:當 Wokwi 的 ESP32 成功切換 LED 後,會發送如
"LED1 ON"或"ALL LEDs OFF"的字串,這個節點會第一時間接收到這些文字。
2. 核心邏輯:Function 節點 (c4c0948da43caba8)
這是此流程的大腦,負責將文字訊號轉換為布林值 (True/False) 並分流:
分流設計:它設定了 4 個輸出端,分別對應 4 顆 LED。
邏輯範例:
收到
"LED1 ON":將msg.payload設為true,並從 第 1 個 輸出端送出。收到
"LED1 OFF":將msg.payload設為false,從 第 1 個 輸出端送出。
全開/全關處理:
收到
"ALL LEDs ON":會將true同時發送到 所有 4 個輸出端,讓介面上的燈一次全亮。
關鍵代碼:
return [msg, null, null, null]代表只讓第一路有訊號,其餘保持靜默。
3. 視覺化顯示:UI LED 節點
包含節點:
LED1(7285f33b),LED2,LED3,LED4。功能:在您的 Node-RED Dashboard 網頁上顯示圓形指示燈。
設定:
當收到
true:顯示為綠色 (#008000)。當收到
false:顯示為紅色 (#ff0000)。
群組:全部歸類在名為
LED的群組,放在2026_09_EX4分頁中。
4. Telegram 同步:Template + Telegram Sender
這是讓您的手機同步收到狀態通知的部分:
Template 節點 (
81ada557b64dc604):將 MQTT 傳來的純文字(如 "LED1 ON")包裝成 Telegram 專用的 JSON 格式。
設定
chatId: 7965218469,確保訊息傳給您正確的帳號。
Telegram Sender 節點 (
b7fcc81b89152e02):負責將包裝好的 JSON 訊息正式發送到您的手機。
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)
關鍵設定:
once: true與onceDelay: 0.1。功能說明:這是流程的開關。
正常情況下,Inject 節點需要手動點擊才會執行。
但因為您勾選了「啟動後立即執行(Inject once after 0.1 seconds)」,所以每當 Node-RED 伺服器啟動,它就會自動發出一個訊號給下一個節點。
2. 內容產生器:Template 節點 (86f6b5808a905449)
名稱:
Telegram 歡迎詞。格式:
JSON輸出。功能說明:這是訊息的「樣板」。它將您想看到的文字包裝成 Telegram 節點能讀懂的格式。
內容解析:
chatId:7965218469(確保訊息精確傳送到 Alex 的手機)。type:"message"(告知這是一則純文字訊息)。content: 這是最核心的部分,包含了:歡迎詞:
歡迎 Alex 使用 WOKWI&Node-red+Telegram控制系統。指令表:列出從
/1on到/alloff的所有可用指令。提示語:提醒您 Wokwi 端的 DHT22 讀取需要按 Enter。
換行符號:代碼中的
\n代表在手機顯示時會自動換行,讓排版整齊、易於閱讀。
3. 發送執行端:Telegram Sender 節點 (d5ee749083c5c5a2)
功能說明:這是最後的發射台。
運作邏輯:它接收 Template 節點產生的 JSON 物件,並透過您設定好的機器人帳號(@alextest999_bot),經由網際網路將這份「操作手冊」發送到您的 Telegram App 中。
4. 機器人設定:Telegram Bot 節點 (457874f8aa8a857a)
功能說明:這不是畫布上的實體流程節點,而是背景的配置檔(Configuration Node)。
設定細節:
儲存了您的 Bot Token(金鑰)。
設定了 Polling Interval(300ms),代表機器人每 0.3 秒會去 Telegram 伺服器檢查有沒有人傳新訊息。
流程運作圖解
Node-RED 啟動。
Inject 節點 倒數 0.1 秒後發出訊號。
Template 節點 準備好「歡迎 Alex...」這串長文字。
Telegram Sender 透過網路將文字推送到您的手機。
Alex 的手機 響起「叮」一聲,完整的指令選單出現在畫面。
Node-RED 程式碼是您系統中的**「數據監測與警報中心」**。它負責接收溫濕度、進行數據解析(拆分數字)、顯示在 Dashboard 儀表板,並在數值異常(過熱或太濕)時觸發 Telegram 與聲音報警。
以下是針對每個區塊的逐行解析:
1. 數據入口:MQTT In 節點 (fa7ececa4e716634)
主題 (Topic):
alex9ufo/temphumi功能:監聽 ESP32 發送的溫濕度字串,格式通常為:
Temperature: 25.00C, Humidity: 60.00%。
2. 視覺化核心:數據分離 Function (bdfcb0188f2a45d1)
這是負責將原始字串拆解為「純數字」的關鍵節點,設定為 2 個輸出端:
正規表示法解析:使用
input.match抓取Temperature:和Humidity:後面的數字。範圍限制:設定溫度必須在 -40°C 到 +80°C 之間才算有效,否則會發出
node.warn警告。輸出 1:傳送純溫度數字給 Dashboard 的 溫度計 (Gauge) 與 文字 (Text) 節點。
輸出 2:傳送純濕度數字給 濕度計。
3. 警報決策中心:高溫/濕度判斷 Function (b4f570bf787abca2)
這個節點負責檢查數值是否超標:
溫度邏輯:
> 70°C:產生「🔥 緊急火警警報」文字。
> 30°C:產生「⚠️ 高溫提醒」文字。
濕度邏輯:
> 80% 或 < 10%:產生「🌧️ 濕度不正常」警報文字。
延遲處理 (
093cc1f43bb2193f):在判斷前加了 2 秒延遲,避免數據剛進來時太過頻繁觸發警報。
4. 警報發佈端
Telegram 同步 (
3a41c8a502d904a4):將產生的警告文字封裝成 JSON 格式,並傳送給 Telegram Sender。聲音報警 (
24a5e9920dbfad71):當判斷 Function 有訊息輸出時,同步觸發Play Audio,讓您的電腦喇叭響起(這就是您想要的救護車/警報音效觸發點)。
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 響起通知,電腦喇叭發出警報聲。










沒有留言:
張貼留言