2026-09 mqtt 實驗3
node-red參考網址
這套系統將 ESP32(硬體感知端) 與 Node-RED(軟體控制端) 結合,打造了一個具備即時雙向通訊、閉環確認(ACK)與圖形化遠端監控的完整物聯網 (IoT) 架構。
系統整體運作流程與互動架構
兩者整體功能對比表
| 功能維度 | Node-RED 程式(軟體控制層) | Wokwi ESP32 程式(硬體感知層) |
| 主要角色 | 系統指揮中心、圖形化人機介面 (GUI) | 硬體執行器、數據採集節點 |
| 通訊協定 | MQTT (Pub/Sub) | MQTT (Pub/Sub) |
| 數據處理 | 接收溫濕度並發行 OK ACK;接收 LED 狀態 | 讀取 DHT22 並發行數據;接收 OK ACK 並印出紀錄 |
| 控制邏輯 | 提供按鈕觸發控制指令發行 | 解析控制指令,驅動 GPIO 2(點亮/關閉/閃爍/5秒定時) |
| 使用者互動 | 網頁化 Dashboard (/ui) 提供視覺化監控 | Serial Monitor 輸出除錯日誌與硬體 LED 實體反應 |
Node-RED 程式功能說明
Node-RED 程式取代了原本 Python + Tkinter 的角色,扮演系統圖形化介面 (Dashboard) 與控制邏輯中樞,主要功能包含:
Wokwi ESP32 程式功能說明
ESP32 程式為硬體感知與致動核心,負責實體(或模擬)硬體的控制與數據採集,主要功能包含:
Wi-Fi 與 MQTT 自動連線:開機後連接至 Wokwi-GUEST 熱點,並連接至 broker.hivemq.com。連線成功後訂閱控制主題 alex9ufo/2026/led 以及兩個握手主題 tempOK 與 humiOK。
LED 狀態控制與回報:收到控制命令時切換模式,並向 alex9ufo/2026/ledstatus 回報狀態(on_led、off_led、flash_led、timer(5Sec)_led)。
溫濕度感測與定期發行:在 loop() 中使用 millis() 計時,每 2 秒透過 DHT22 讀取溫濕度,並分別發行至 alex9ufo/2026/temp 與 alex9ufo/2026/humi 主題。
接收確認訊息 (ACK 驗證):當收到來自 Node-RED 的 tempOK 或 humiOK 訊息時,於 Serial Monitor 印出 temp_Rx_OK 與 humi_Rx_OK,用以驗證訊息已成功傳遞至 Node-RED。
非阻塞式模式運作:
ESP32程式
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#define DHTPIN 4
#define DHTTYPE DHT22
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883;
const char* sub_topic = "alex9ufo/2026/led";
const char* pub_topic = "alex9ufo/2026/ledstatus";
const char* temp_topic = "alex9ufo/2026/temp";
const char* humi_topic = "alex9ufo/2026/humi";
const char* temp_ok_topic = "alex9ufo/2026/tempOK";
const char* humi_ok_topic = "alex9ufo/2026/humiOK";
const int ledPin = 2;
WiFiClient espClient;
PubSubClient client(espClient);
DHT dht(DHTPIN, DHTTYPE);
enum Mode { OFF, ON, FLASH, TIMER };
Mode currentMode = OFF;
unsigned long lastFlashTime = 0;
unsigned long timerStartTime = 0;
unsigned long lastDHTTime = 0;
bool ledState = LOW;
void setup_wifi() {
delay(10);
Serial.print("[Wi-Fi] 正在連線至 ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\n[Wi-Fi] 已成功連線!");
}
void publishStatus(const char* payload) {
client.publish(pub_topic, payload);
Serial.print("[MQTT 發行] LED 狀態: ");
Serial.println(payload);
}
void callback(char* topic, byte* payload, unsigned int length) {
String message;
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.print("[MQTT 接收] 主題: ");
Serial.print(topic);
Serial.print(" | Payload: ");
Serial.println(message);
// 接收 tempOK 主題
if (String(topic) == temp_ok_topic) {
Serial.println("temp_Rx_OK");
return;
}
// 接收 humiOK 主題
if (String(topic) == humi_ok_topic) {
Serial.println("humi_Rx_OK");
return;
}
if (message == "on") {
currentMode = ON;
digitalWrite(ledPin, HIGH);
publishStatus("on_led");
} else if (message == "off") {
currentMode = OFF;
digitalWrite(ledPin, LOW);
publishStatus("off_led");
} else if (message == "flash") {
currentMode = FLASH;
publishStatus("flash_led");
} else if (message == "timer(5Sec)") {
currentMode = TIMER;
timerStartTime = millis();
digitalWrite(ledPin, HIGH);
publishStatus("timer(5Sec)_led");
}
}
void reconnect() {
while (!client.connected()) {
Serial.print("[MQTT] 正在嘗試連線至 Broker: ");
Serial.println(mqtt_server);
String clientId = "ESP32Client-" + String(random(0xffff), HEX);
if (client.connect(clientId.c_str())) {
Serial.println("[MQTT] 連線成功!");
client.subscribe(sub_topic);
client.subscribe(temp_ok_topic); // 訂閱 tempOK
client.subscribe(humi_ok_topic); // 訂閱 humiOK
} else {
Serial.print("[MQTT] 連線失敗, rc=");
Serial.print(client.state());
Serial.println(" 5 秒後重試...");
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
dht.begin();
setup_wifi();
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
}
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
unsigned long currentMillis = millis();
// 每 2 秒讀取並發行溫濕度
if (currentMillis - lastDHTTime >= 5000) {
lastDHTTime = currentMillis;
float h = dht.readHumidity();
float t = dht.readTemperature();
if (!isnan(h) && !isnan(t)) {
client.publish(temp_topic, String(t, 1).c_str());
client.publish(humi_topic, String(h, 1).c_str());
Serial.printf("[DHT22] 溫度: %.1f °C | 濕度: %.1f %%\n", t, h);
}
}
if (currentMode == FLASH) {
if (currentMillis - lastFlashTime >= 500) {
lastFlashTime = currentMillis;
ledState = !ledState;
digitalWrite(ledPin, ledState);
}
} else if (currentMode == TIMER) {
if (currentMillis - timerStartTime >= 5000) {
digitalWrite(ledPin, LOW);
currentMode = OFF;
publishStatus("off_led");
}
}
}
Node-Red程式
[{"id":"e98647487e9c1821","type":"mqtt in","z":"191b6456391ed604","name":"Sub LED Status","topic":"alex9ufo/2026/ledstatus","qos":"0","datatype":"auto-detect","broker":"hivemq_broker","nl":false,"rap":true,"rh":0,"inputs":0,"x":100,"y":80,"wires":[["0c7b0f3d1068f7c4"]]},{"id":"0c7b0f3d1068f7c4","type":"function","z":"191b6456391ed604","name":"格式化 LED 狀態","func":"msg.payload = \"LED 狀態: \" + msg.payload;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":80,"wires":[["33cbcbe74dbe1b6b"]]},{"id":"33cbcbe74dbe1b6b","type":"ui_text","z":"191b6456391ed604","group":"esp32_group","order":1,"width":4,"height":1,"name":"LED 狀態顯示","label":"","format":"<div style='color:blue; font-weight:bold; font-size:18px; text-align:center;'>{{msg.payload}}</div>","layout":"col-center","x":540,"y":80,"wires":[]},{"id":"53a8cbf4a4c56341","type":"mqtt in","z":"191b6456391ed604","name":"Sub Temp","topic":"alex9ufo/2026/temp","qos":"0","datatype":"auto-detect","broker":"hivemq_broker","nl":false,"rap":true,"rh":0,"inputs":0,"x":90,"y":160,"wires":[["3fe49a7cc8e45de4","14fd1a1b7f5cbe31","a75de3836fdff505"]]},{"id":"3fe49a7cc8e45de4","type":"function","z":"191b6456391ed604","name":"格式化溫度","func":"msg.payload = \"環境溫度: \" + msg.payload + \" °C\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":140,"wires":[["dcba01ce9b17d90f"]]},{"id":"14fd1a1b7f5cbe31","type":"function","z":"191b6456391ed604","name":"組裝 TempOK","func":"msg.payload = \"OK\";\nmsg.topic = \"alex9ufo/2026/tempOK\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":220,"wires":[["56a3f7a8fd77616c"]]},{"id":"dcba01ce9b17d90f","type":"ui_text","z":"191b6456391ed604","group":"esp32_group","order":3,"width":4,"height":1,"name":"溫度顯示","label":"","format":"<div style='color:darkred; font-weight:bold; font-size:18px; text-align:center;'>{{msg.payload}}</div>","layout":"col-center","x":520,"y":140,"wires":[]},{"id":"ef09c2f981a95de5","type":"mqtt in","z":"191b6456391ed604","name":"Sub Humi","topic":"alex9ufo/2026/humi","qos":"0","datatype":"auto-detect","broker":"hivemq_broker","nl":false,"rap":true,"rh":0,"inputs":0,"x":90,"y":260,"wires":[["4f1df0d2769d467c","4bb11901439c7593","e0fdb83f522c47ce"]]},{"id":"4f1df0d2769d467c","type":"function","z":"191b6456391ed604","name":"格式化濕度","func":"msg.payload = \"環境濕度: \" + msg.payload + \" %\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":260,"wires":[["bca3c946b461e603"]]},{"id":"4bb11901439c7593","type":"function","z":"191b6456391ed604","name":"組裝 HumiOK","func":"msg.payload = \"OK\";\nmsg.topic = \"alex9ufo/2026/humiOK\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":340,"wires":[["56a3f7a8fd77616c"]]},{"id":"bca3c946b461e603","type":"ui_text","z":"191b6456391ed604","group":"esp32_group","order":4,"width":4,"height":1,"name":"濕度顯示","label":"","format":"<div style='color:darkgreen; font-weight:bold; font-size:18px; text-align:center;'>{{msg.payload}}</div>","layout":"col-center","x":520,"y":260,"wires":[]},{"id":"7e56b6a33e7f720c","type":"ui_button","z":"191b6456391ed604","name":"ON 按鈕","group":"esp32_group","order":6,"width":4,"height":1,"passthru":false,"label":"ON (開啟)","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"on","payloadType":"str","topic":"alex9ufo/2026/led","topicType":"str","x":260,"y":420,"wires":[["56a3f7a8fd77616c"]]},{"id":"8975ee23cc5c1257","type":"ui_button","z":"191b6456391ed604","name":"OFF 按鈕","group":"esp32_group","order":8,"width":4,"height":1,"passthru":false,"label":"OFF (關閉)","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"off","payloadType":"str","topic":"alex9ufo/2026/led","topicType":"str","x":260,"y":460,"wires":[["56a3f7a8fd77616c"]]},{"id":"a4d8b92c7d3bdbd9","type":"ui_button","z":"191b6456391ed604","name":"FLASH 按鈕","group":"esp32_group","order":9,"width":4,"height":1,"passthru":false,"label":"FLASH (閃爍)","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"flash","payloadType":"str","topic":"alex9ufo/2026/led","topicType":"str","x":270,"y":500,"wires":[["56a3f7a8fd77616c"]]},{"id":"e59cef064b02ae83","type":"ui_button","z":"191b6456391ed604","name":"Timer 按鈕","group":"esp32_group","order":10,"width":4,"height":1,"passthru":false,"label":"Timer (定時5秒)","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"timer(5Sec)","payloadType":"str","topic":"alex9ufo/2026/led","topicType":"str","x":270,"y":540,"wires":[["56a3f7a8fd77616c"]]},{"id":"56a3f7a8fd77616c","type":"mqtt out","z":"191b6456391ed604","name":"MQTT 發行節點","topic":"","qos":"0","retain":"false","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"hivemq_broker","x":580,"y":480,"wires":[]},{"id":"a75de3836fdff505","type":"ui_gauge","z":"191b6456391ed604","name":"","group":"esp32_group","order":2,"width":6,"height":4,"gtype":"gage","title":"環境溫度","label":"°C","format":"{{value}}","min":"-40","max":"80","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":520,"y":200,"wires":[]},{"id":"e0fdb83f522c47ce","type":"ui_gauge","z":"191b6456391ed604","name":"","group":"esp32_group","order":7,"width":6,"height":4,"gtype":"gage","title":"環境濕度","label":"%","format":"{{value}}","min":"0","max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"50","seg2":"75","diff":false,"className":"","x":520,"y":320,"wires":[]},{"id":"hivemq_broker","type":"mqtt-broker","name":"HiveMQ Broker","broker":"broker.hivemq.com","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true},{"id":"esp32_group","type":"ui_group","name":"ESP32 LED & DHT22 控制器","tab":"esp32_dashboard_tab","order":1,"disp":true,"width":10,"collapse":false},{"id":"esp32_dashboard_tab","type":"ui_tab","name":"ESP32 控制面板","icon":"dashboard","disabled":false,"hidden":false},{"id":"86c9ae5506a16be4","type":"global-config","env":[],"modules":{"node-red-dashboard":"3.6.6"}}]
沒有留言:
張貼留言