2026年9月11日 星期五

2026-09 MQTT基本觀念(作業1)

 2026-09  MQTT基本觀念(作業1)


將 執行結果 畫面 錄製約 3分鐘 以上
1) MQTT BOX 動作畫面 
2) Python + TKinter 動作畫面 
3) Wokwi ESP32 動作畫面 
並上傳YT (公開) mail alex9ufo@gmail.com






MQTT(Message Queuing Telemetry Transport)是一種輕量級、基於發布/訂閱(Publish/Subscribe)架構的網路傳輸協定,非常適合硬體資源有限、網路頻寬低的物聯網(IoT)應用場景。
以下為您整理 MQTT 的核心觀念,並提供一個可以使用電腦立即測試的快速實驗

核心觀念
MQTT 的核心思維是「解耦」,傳送訊息與接收訊息的裝置不需要知道彼此是誰,完全透過一個中間人來協調。

  • Broker(代理伺服器):MQTT 的核心中樞。負責接收發布者送來的訊息,並將訊息過濾、分發給有訂閱該主題的接收者。
  • Publisher(發布者):發送數據的客戶端(例如:溫度感測器)。它會將訊息貼上特定的「主題」標籤後送往 Broker。
  • Subscriber(訂閱者):接收數據的客戶端(例如:手機 App、伺服器)。它向 Broker 訂閱特定的「主題」,當該主題有新訊息時就會收到通知。
  • Topic(主題):訊息的分類標籤,採用斜線階層設計(例如:home/livingroom/temperature)。發布者與訂閱者全靠主題來對接訊息。
  • 發布/訂閱模型(Publish/Subscribe):發布者與訂閱者彼此不直接相連,雙方透過 Topic(主題) 來分類訊息。訂閱特定主題的人,就會收到該主題的最新資料。 [1]
  • 輕量與省電:標頭檔極小(最小僅 2 位元組),非常適合頻寬低、延遲高或不穩定的網路環境。 [1, 2]
  • QoS(Quality of Service,服務品質):提供三種訊息傳遞等級,確保資料送達:
    • QoS 0:最多送達一次(可能會遺失)。
    • QoS 1:至少送達一次(可能重複)。
    • QoS 2:確保僅送達一次(最安全但耗時)。
  • 參考' https://resource.webduino.io/blog/mqtt-guide 

  • 參考 https://alex9ufoexploer.blogspot.com/2026/03/2026-1-mqtt.html

WOKWI 程式






#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "DHT.h"

// --- 網路與 MQTT 設定 ---
const char* ssid        = "Wokwi-GUEST";
const char* password    = "";
const char* mqtt_server = "mqttgo.io";
const int   mqtt_port   = 1883;

// --- MQTT 主題設定 ---
const char* TOPIC_TEMP   = "alex9ufo/temp";
const char* TOPIC_HUMI   = "alex9ufo/humi";
const char* TOPIC_LED    = "alex9ufo/led";
const char* TOPIC_STATUS = "alex9ufo/status";

// --- 硬體腳位定義 (精確對照圖形) ---
#define DHTPIN 4          // DHT22 接在 GPIO 4
#define DHTTYPE DHT22

#define RELAY1_PIN 27     // Relay 1 接 GPIO 27
#define RELAY2_PIN 14     // Relay 2 接 GPIO 14
#define RELAY3_PIN 12     // Relay 3 接 GPIO 12
#define RELAY4_PIN 13     // Relay 4 接 GPIO 13

// --- 元件初始化 ---
DHT dht(DHTPIN, DHTTYPE);
LiquidCrystal_I2C lcd(0x27, 16, 2); // Wokwi 預設 I2C 位址為 0x27

WiFiClient espClient;
PubSubClient client(espClient);

unsigned long lastMsg = 0;
float currentTemp = 0.0;
float currentHumi = 0.0;

// LCD 更新顯示函式
void updateLCD() {
  lcd.setCursor(0, 0);
  lcd.print("Temp: ");
  lcd.print(currentTemp, 1);
  lcd.print((char)223); // 顯示 ° 符號
  lcd.print("C   ");

  lcd.setCursor(0, 1);
  lcd.print("Humi: ");
  lcd.print(currentHumi, 1);
  lcd.print(" %   ");
}

// Wi-Fi 連線設定
void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

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

// MQTT 接收控制指令處理 (Callback)
void callback(char* topic, byte* payload, unsigned int length) {
  String message = "";
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  message.trim();
  Serial.print("MQTT Command Arrival [");
  Serial.print(topic);
  Serial.print("]: ");
  Serial.println(message);

  // 指令判斷與繼電器控制 (HIGH = 觸發點亮 LED)
  if (message == "1on") {
    digitalWrite(RELAY1_PIN, HIGH);
    client.publish(TOPIC_STATUS, "1on");
  } else if (message == "1off") {
    digitalWrite(RELAY1_PIN, LOW);
    client.publish(TOPIC_STATUS, "1off");
  } else if (message == "2on") {
    digitalWrite(RELAY2_PIN, HIGH);
    client.publish(TOPIC_STATUS, "2on");
  } else if (message == "2off") {
    digitalWrite(RELAY2_PIN, LOW);
    client.publish(TOPIC_STATUS, "2off");
  } else if (message == "3on") {
    digitalWrite(RELAY3_PIN, HIGH);
    client.publish(TOPIC_STATUS, "3on");
  } else if (message == "3off") {
    digitalWrite(RELAY3_PIN, LOW);
    client.publish(TOPIC_STATUS, "3off");
  } else if (message == "4on") {
    digitalWrite(RELAY4_PIN, HIGH);
    client.publish(TOPIC_STATUS, "4on");
  } else if (message == "4off") {
    digitalWrite(RELAY4_PIN, LOW);
    client.publish(TOPIC_STATUS, "4off");
  } else if (message == "allon") {
    digitalWrite(RELAY1_PIN, HIGH);
    digitalWrite(RELAY2_PIN, HIGH);
    digitalWrite(RELAY3_PIN, HIGH);
    digitalWrite(RELAY4_PIN, HIGH);
    client.publish(TOPIC_STATUS, "allon");
  } else if (message == "alloff") {
    digitalWrite(RELAY1_PIN, LOW);
    digitalWrite(RELAY2_PIN, LOW);
    digitalWrite(RELAY3_PIN, LOW);
    digitalWrite(RELAY4_PIN, LOW);
    client.publish(TOPIC_STATUS, "alloff");
  }
}

// MQTT 自動重連機制
void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    String clientId = "ESP32Client-";
    clientId += String(random(0xffff), HEX);

    if (client.connect(clientId.c_str())) {
      Serial.println("connected");
      client.subscribe(TOPIC_LED);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);

  // 初始化 LCD (GPIO 21=SDA, GPIO 22=SCL)
  Wire.begin(21, 22);
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("Connecting WiFi...");

  // 初始化繼電器腳位
  pinMode(RELAY1_PIN, OUTPUT);
  pinMode(RELAY2_PIN, OUTPUT);
  pinMode(RELAY3_PIN, OUTPUT);
  pinMode(RELAY4_PIN, OUTPUT);

  // 預設關閉所有繼電器
  digitalWrite(RELAY1_PIN, LOW);
  digitalWrite(RELAY2_PIN, LOW);
  digitalWrite(RELAY3_PIN, LOW);
  digitalWrite(RELAY4_PIN, LOW);

  // 初始化 DHT 感測器與 Wi-Fi
  dht.begin();
  setup_wifi();

  lcd.clear();
  lcd.print("WiFi Connected!");
  delay(1000);
  lcd.clear();

  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  // 每 2 秒讀取感測器、更新 LCD 與發行 MQTT 數據
  unsigned long now = millis();
  if (now - lastMsg > 2000) {
    lastMsg = now;

    float h = dht.readHumidity();
    float t = dht.readTemperature();

    if (!isnan(h) && !isnan(t)) {
      currentTemp = t;
      currentHumi = h;

      // 發行至 MQTT
      char tempStr[8], humiStr[8];
      dtostrf(t, 1, 2, tempStr);
      dtostrf(h, 1, 2, humiStr);

      client.publish(TOPIC_TEMP, tempStr);
      client.publish(TOPIC_HUMI, humiStr);

      // 更新 16x2 LCD
      updateLCD();

      Serial.print("Temp: "); Serial.print(tempStr);
      Serial.print(" C | Humi: "); Serial.print(humiStr); Serial.println(" %");
    } else {
      Serial.println("Failed to read from DHT sensor!");
    }
  }
}




Python 程式


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

# --- MQTT 設定 ---
MQTT_BROKER = "mqttgo.io"
MQTT_PORT = 1883

TOPIC_TEMP   = "alex9ufo/temp"
TOPIC_HUMI   = "alex9ufo/humi"
TOPIC_LED    = "alex9ufo/led"
TOPIC_STATUS = "alex9ufo/status"

# --- 顏色定義 ---
COLOR_ON  = "#00FF00"  # 亮燈:綠色
COLOR_OFF = "#1E1E1E"  # 滅燈:黑色/深灰

# --- 模擬指示燈狀態控制 ---
def update_led_indicator(led_index, is_on):
    color = COLOR_ON if is_on else COLOR_OFF
    canvas_leds[led_index].itemconfig(led_circles[led_index], fill=color)

def handle_status_payload(payload):
    print(f"[除錯] 解析狀態 Payload: '{payload}'")  # 終端機除錯訊息
    
    # 單顆開關判斷
    if payload in ["1on", "2on", "3on", "4on"]:
        idx = int(payload[0]) - 1
        update_led_indicator(idx, True)
    elif payload in ["1off", "2off", "3off", "4off"]:
        idx = int(payload[0]) - 1
        update_led_indicator(idx, False)
    # 全開 / 全關判斷
    elif payload == "allon":
        for i in range(4):
            update_led_indicator(i, True)
    elif payload == "alloff":
        for i in range(4):
            update_led_indicator(i, False)

# --- MQTT 觀察者事件 ---
def on_connect(client, userdata, flags, rc, properties=None):
    print("已成功連線至 MQTT Broker,回傳碼:", rc)
    client.subscribe([(TOPIC_TEMP, 0), (TOPIC_HUMI, 0), (TOPIC_STATUS, 0)])

def on_message(client, userdata, msg):
    topic = msg.topic
    # 使用 strip() 確保清除 \r \n 或空白字元
    payload = msg.payload.decode('utf-8').strip()
    print(f"[MQTT 收到] 主題: {topic} | 內容: {payload}")
    
    if topic == TOPIC_TEMP:
        root.after(0, lambda: lbl_temp.config(text=f"{payload} °C"))
    elif topic == TOPIC_HUMI:
        root.after(0, lambda: lbl_humi.config(text=f"{payload} %"))
    elif topic == TOPIC_STATUS:
        # 使用 root.after 確保在 Tkinter 主執行緒更新介面
        root.after(0, lambda: lbl_status.config(text=f"最新狀態: {payload}"))
        root.after(0, lambda: handle_status_payload(payload))

def send_cmd(cmd):
    print(f"[MQTT 發送] 指令: {cmd}")
    client.publish(TOPIC_LED, cmd)

# --- GUI 介面建構 ---
root = tk.Tk()
root.title("ESP32 MQTT 控制與燈態中心")
root.geometry("400x520")
root.resizable(False, False)

# 1. 溫濕度顯示區
frame_sensor = ttk.LabelFrame(root, text=" 環境數據 ", padding=10)
frame_sensor.pack(fill="x", padx=15, pady=10)

ttk.Label(frame_sensor, text="溫度:", font=("Arial", 12)).grid(row=0, column=0, sticky="w", pady=5)
lbl_temp = ttk.Label(frame_sensor, text="-- °C", font=("Arial", 14, "bold"), foreground="red")
lbl_temp.grid(row=0, column=1, padx=10)

ttk.Label(frame_sensor, text="濕度:", font=("Arial", 12)).grid(row=1, column=0, sticky="w", pady=5)
lbl_humi = ttk.Label(frame_sensor, text="-- %", font=("Arial", 14, "bold"), foreground="blue")
lbl_humi.grid(row=1, column=1, padx=10)

# 2. LED 指示燈與控制區
frame_ctrl = ttk.LabelFrame(root, text=" LED 控制與實時狀態 ", padding=10)
frame_ctrl.pack(fill="x", padx=15, pady=5)

canvas_leds = []
led_circles = []

for i in range(1, 5):
    ttk.Label(frame_ctrl, text=f"LED {i}:", font=("Arial", 10, "bold")).grid(row=i-1, column=0, padx=5, pady=6)
    
    cv = tk.Canvas(frame_ctrl, width=24, height=24, highlightthickness=0)
    cv.grid(row=i-1, column=1, padx=5, pady=6)
    circle = cv.create_oval(2, 2, 22, 22, fill=COLOR_OFF, outline="#555555", width=1.5)
    
    canvas_leds.append(cv)
    led_circles.append(circle)
    
    btn_on = ttk.Button(frame_ctrl, text="開啟", width=6, command=lambda idx=i: send_cmd(f"{idx}on"))
    btn_on.grid(row=i-1, column=2, padx=5, pady=6)
    
    btn_off = ttk.Button(frame_ctrl, text="關閉", width=6, command=lambda idx=i: send_cmd(f"{idx}off"))
    btn_off.grid(row=i-1, column=3, padx=5, pady=6)

btn_all_on = ttk.Button(frame_ctrl, text="全開 (allon)", command=lambda: send_cmd("allon"))
btn_all_on.grid(row=4, column=0, columnspan=2, padx=5, pady=10, sticky="ew")

btn_all_off = ttk.Button(frame_ctrl, text="全關 (alloff)", command=lambda: send_cmd("alloff"))
btn_all_off.grid(row=4, column=2, columnspan=2, padx=5, pady=10, sticky="ew")

# 3. 狀態回傳顯示
lbl_status = ttk.Label(root, text="最新狀態: 尚未接收", font=("Arial", 10, "italic"))
lbl_status.pack(pady=10)

# --- 啟動 MQTT 背景監聽 ---
# 相容 paho-mqtt v1.x 與 v2.x 寫法
try:
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1)
except AttributeError:
    client = mqtt.Client()

client.on_connect = on_connect
client.on_message = on_message

client.connect(MQTT_BROKER, MQTT_PORT, 60)
client.loop_start()

root.mainloop()




MQTTBO設定







沒有留言:

張貼留言

Easybuilder pro + WOKWI esp32 DHT22 + 4 LED 控制 (MQTT)

Easybuilder pro + WOKWI esp32 DHT22 + 4 LED 控制  (MQTT) 本系統實現了 ESP32 微控制器 (於 Wokwi 模擬環境中執行)與 EasyBuilder Pro 人機介面 (HMI) 透過 MQTT 通訊協定 進行雙向控制與...