2026年9月16日 星期三

2026-09 mqtt 實驗2

2026-09 mqtt 實驗2

基於 MQTT 協定發佈/訂閱 (Pub/Sub) 架構 的完整物聯網 (IoT) 監控與遠端控制系統。













本系統為一套基於 MQTT 協定發佈/訂閱 (Pub/Sub) 架構 的完整物聯網 (IoT) 監控與遠端控制系統。系統主要包含硬體感知層、MQTT 通訊中繼層以及使用者控制層三大部分:

1. 硬體感知與執行層 (ESP32 在 Wokwi 模擬環境)

  • 硬體架構

    • GPIO 2:連接 LED 燈,作為控制致動器。

    • GPIO 4:連接 DHT22 溫濕度感測器,作為環境數據採集源。

  • 資料採集與發佈:ESP32 每 2 秒採集一次環境溫濕度,並自動透過 MQTT 發行至 alex9ufo/2026/temp(溫度)與 alex9ufo/2026/humi(濕度)主題。

  • 雙向控制與狀態回報

    • 訂閱 alex9ufo/2026/led 接收控制命令(onoffflashtimer(5Sec)),並即時切換 LED 狀態。

    • 執行命令後,向 alex9ufo/2026/ledstatus 回報當前狀態(如 on_ledoff_led)。

  • 握手確認 (Handshake ACK):訂閱 alex9ufo/2026/tempOKalex9ufo/2026/humiOK 主題。當收到 Python 端發出的確認訊號時,會在 Serial Monitor 印出 temp_Rx_OKhumi_Rx_OK,確保資料傳輸品質。

2. MQTT 通訊中繼層 (Broker)

  • 伺服器:採用 HiveMQ 公開測試伺服器 (broker.hivemq.com:1883)。

  • 主題架構 (Topic Topology)

通訊方向主題名稱功能 / Payload 內容
Python → ESP32alex9ufo/2026/led控制命令 (on, off, flash, timer(5Sec))
ESP32 → Pythonalex9ufo/2026/ledstatusLED 狀態回報 (on_led, off_led, 等)
ESP32 → Pythonalex9ufo/2026/temp實時溫度數值 (如 25.5)
ESP32 → Pythonalex9ufo/2026/humi實時濕度數值 (如 60.0)
Python → ESP32alex9ufo/2026/tempOK溫度接收確認訊號 (OK)
Python → ESP32alex9ufo/2026/humiOK濕度接收確認訊號 (OK)

3. 使用者控制層 (Python + Tkinter GUI)

  • 圖形化監控介面:即時顯示目前 MQTT 連線狀態(綠色已連線/紅色斷線)、LED 實時狀態以及最新的環境溫濕度數值。

  • 遠端按鈕控制:提供四個控制按鈕,使用者點擊即可發送對應命令控制 ESP32。

  • 自動 ACK 回應:當收到 ESP32 發出的溫濕度數據並更新 UI 後,會自動發送 OK 訊息至對應的 tempOKhumiOK 主題,完成閉環通訊驗證。



ESP32 程式碼整合了 Wi-Fi 連線、MQTT 雙向通訊、DHT22 溫濕度感測器讀取 以及 LED 多模式控制

硬體與網路初始化

  • 硬體腳位設定:GPIO 2 連接控制 LED,GPIO 4 連接 DHT22 溫濕度感測器。

  • Wi-Fi 連線:開機後自動連線至 Wokwi-GUEST 虛擬熱點,並在 Serial Monitor 印出連線進度。

  • MQTT Broker 連線:連接至 HiveMQ 公開伺服器(broker.hivemq.com:1883),連線成功後自動訂閱三個主題:

    • alex9ufo/2026/led(接收控制指令)

    • alex9ufo/2026/tempOK(接收溫度確認訊號)

    • alex9ufo/2026/humiOK(接收濕度確認訊號)

訊息接收與處理解析 (Callback) 收到訂閱主題的訊息時會執行對應邏輯:

  • 確認訊號(ACK)處理

    • 收到 tempOK 主題訊息:在 Serial Monitor 輸出 temp_Rx_OK

    • 收到 humiOK 主題訊息:在 Serial Monitor 輸出 humi_Rx_OK

  • LED 控制指令處理(收到後同步向 alex9ufo/2026/ledstatus 回報狀態):

    • "on":點亮 LED,回傳 "on_led"

    • "off":關閉 LED,回傳 "off_led"

    • "flash":開啟閃爍模式,回傳 "flash_led"

    • "timer(5Sec)":點亮 LED 並開始 5 秒倒數,回傳 "timer(5Sec)_led"

非阻塞式背景任務 (Loop) 利用 millis() 代替 delay(),確保 MQTT 能持續保持連線不中斷:

  • 溫濕度採樣發行:每 2 秒自動讀取一次 DHT22 感測器:

    • 將溫度數值發行至 alex9ufo/2026/temp

    • 將濕度數值發行至 alex9ufo/2026/humi

    • 於 Serial Monitor 印出實時數據。

  • FLASH 閃爍邏輯:每 500 毫秒翻轉一次 GPIO 2 電位。

  • TIMER 定時邏輯:滿 5 秒後自動關閉 LED,將模式切回 OFF 並發行 "off_led" 狀態通知控制端。

#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");
    }
  }
}


這份 Python 程式碼使用 Tkinter 打造圖形介面 (GUI),並透過 paho-mqtt (Callback API v2) 實現與 ESP32 的雙向通訊及遠端控制。

MQTT 網路通訊管理

  • Broker 設定:連接至 HiveMQ 公開測試伺服器 (broker.hivemq.com:1883)。

  • 連線與自動訂閱 (on_connect):連線成功後,自動訂閱 ESP32 回傳的 3 個主題:

    • alex9ufo/2026/ledstatus(LED 狀態)

    • alex9ufo/2026/temp(溫度數據)

    • alex9ufo/2026/humi(濕度數據)

  • 連線狀態監控:即時監控連線與斷線事件,並在介面頂端動態更新狀態(藍/橘色「連線中」、綠色「已連線」、紅色「已斷線/連線失敗」)。

  • 背景異步處理 (loop_start):在獨立執行緒運行 MQTT 網路封包接收與發送,確保 Tkinter GUI 視窗畫面運作順暢不卡頓。

訊息接收與握手機制 (on_message) 當收到 ESP32 發佈的訊息時,會進行解析並處理:

  • LED 狀態更新:收到 ledstatus 訊息時,即時更新介面上的 LED 狀態: 文字(如 on_ledoff_led)。

  • 溫濕度更新與握手確認 (ACK)

    • 收到 temp 數據:更新 GUI 的溫度顯示,並自動發行 "OK" 訊息alex9ufo/2026/tempOK 主題通知 ESP32。

    • 收到 humi 數據:更新 GUI 的濕度顯示,並自動發行 "OK" 訊息alex9ufo/2026/humiOK 主題通知 ESP32。

GUI 控制介面與指令發送

  • 按鈕事件觸發 (send_cmd):提供 4 個按鈕控制遠端 ESP32 的 LED:

    • ON (開啟):發送 "on"alex9ufo/2026/led

    • OFF (關閉):發送 "off"alex9ufo/2026/led

    • FLASH (閃爍):發送 "flash"alex9ufo/2026/led

    • Timer (定時5秒):發送 "timer(5Sec)"alex9ufo/2026/led


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

BROKER = "broker.hivemq.com"
PORT = 1883

TOPIC_PUB_LED = "alex9ufo/2026/led"
TOPIC_SUB_LEDSTATUS = "alex9ufo/2026/ledstatus"

TOPIC_SUB_TEMP = "alex9ufo/2026/temp"
TOPIC_SUB_HUMI = "alex9ufo/2026/humi"

# 新增 OK 回應主題
TOPIC_PUB_TEMPOK = "alex9ufo/2026/tempOK"
TOPIC_PUB_HUMIOK = "alex9ufo/2026/humiOK"

def on_connect(client, userdata, flags, reason_code, properties=None):
    if reason_code == 0:
        mqtt_status_label.config(text="MQTT 連線狀態: 已連線", fg="green")
        client.subscribe(TOPIC_SUB_LEDSTATUS)
        client.subscribe(TOPIC_SUB_TEMP)
        client.subscribe(TOPIC_SUB_HUMI)
    else:
        mqtt_status_label.config(text=f"MQTT 連線失敗 ({reason_code})", fg="red")

def on_disconnect(client, userdata, disconnect_flags, reason_code, properties=None):
    mqtt_status_label.config(text="MQTT 連線狀態: 已斷線", fg="red")

def on_message(client, userdata, msg):
    payload = msg.payload.decode("utf-8")
    topic = msg.topic
    
    if topic == TOPIC_SUB_LEDSTATUS:
        led_status_label.config(text=f"LED 狀態: {payload}")
    elif topic == TOPIC_SUB_TEMP:
        temp_label.config(text=f"環境溫度: {payload} °C")
        # 收到溫度資料後發送 tempOK 回應
        client.publish(TOPIC_PUB_TEMPOK, "OK")
    elif topic == TOPIC_SUB_HUMI:
        humi_label.config(text=f"環境濕度: {payload} %")
        # 收到濕度資料後發送 humiOK 回應
        client.publish(TOPIC_PUB_HUMIOK, "OK")

def send_cmd(cmd):
    client.publish(TOPIC_PUB_LED, cmd)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_message = on_message

root = tk.Tk()
root.title("ESP32 LED & DHT22 控制器")
root.geometry("340x380")

mqtt_status_label = tk.Label(root, text="MQTT 連線狀態: 連線中...", font=("Arial", 10), fg="orange")
mqtt_status_label.pack(pady=5)

led_status_label = tk.Label(root, text="LED 狀態: 未知", font=("Arial", 11, "bold"), fg="blue")
led_status_label.pack(pady=5)

temp_label = tk.Label(root, text="環境溫度: -- °C", font=("Arial", 11, "bold"), fg="darkred")
temp_label.pack(pady=3)

humi_label = tk.Label(root, text="環境濕度: -- %", font=("Arial", 11, "bold"), fg="darkgreen")
humi_label.pack(pady=3)

btn_on = tk.Button(root, text="ON (開啟)", width=20, command=lambda: send_cmd("on"))
btn_on.pack(pady=4)

btn_off = tk.Button(root, text="OFF (關閉)", width=20, command=lambda: send_cmd("off"))
btn_off.pack(pady=4)

btn_flash = tk.Button(root, text="FLASH (閃爍)", width=20, command=lambda: send_cmd("flash"))
btn_flash.pack(pady=4)

btn_timer = tk.Button(root, text="Timer (定時5秒)", width=20, command=lambda: send_cmd("timer(5Sec)"))
btn_timer.pack(pady=4)

try:
    client.connect_async(BROKER, PORT, 60)
    client.loop_start()
except Exception as e:
    mqtt_status_label.config(text=f"連線異常: {e}", fg="red")

root.mainloop()





沒有留言:

張貼留言

2026-09 mqtt 實驗2

2026-09 mqtt 實驗2 基於 MQTT 協定 與 發佈/訂閱 (Pub/Sub) 架構 的完整物聯網 (IoT) 監控與遠端控制系統。 本系統為一套基於 MQTT 協定 與 發佈/訂閱 (Pub/Sub) 架構 的完整物聯網 (IoT) 監控與遠端控制系統。系統...