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







沒有留言:
張貼留言