2026年3月6日 星期五

2026 作業1 MQTT基本觀念

 2026 作業1MQTT基本觀念

作業 參考下面網址

https://alex9ufoexploer.blogspot.com/2025/02/1-mqtt-relay-dht22-mqtt-box-pc-mymqtt.html

https://alex9ufoexploer.blogspot.com/2026/03/mqtt.html

https://sites.google.com/ism.edu.mo/f4dat/topic1-arduino%E5%9F%BA%E6%9C%AC%E6%87%89%E7%94%A8/1-0-%E7%B7%9A%E4%B8%8A%E7%B7%A8%E7%A8%8B-wokwi?authuser=0



將 執行結果 畫面 錄製約 3分鐘 並上傳YT (公開) mail alex9ufo@gmail.com

// --- MQTT 設定 ---
const char* mqtt_broker = "mqttgo.io";
const int mqtt_port = 1883;

const char* led_control_topic = "alex9ufo/ledcontrol";
const char* led_status_topic = "alex9ufo/ledstatus";
const char* temp_humi_topic = "alex9ufo/temphumi";



要有1602 I2C LCD 電路








WOKWI Arduino程式

#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <DHT_U.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h> // 引入 LCD I2C 庫
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>    

// --- Wi-Fi 憑證 ---
const char* ssid = "Wokwi-GUEST";    
const char* password = "";            

// --- 硬體定義 ---
#define DHTPIN 4      
#define DHTTYPE DHT22
#define SDA_PIN 21
#define SCL_PIN 22

DHT_Unified dht(DHTPIN, DHTTYPE);
// 設定 LCD 地址 (通常為 0x27) 與大小 (16x2)
LiquidCrystal_I2C lcd(0x27, 16, 2);

const int ledPins[] = {13, 12, 14, 27};
const int NUM_LEDS = sizeof(ledPins) / sizeof(ledPins[0]);

// --- MQTT 設定 ---
const char* mqtt_broker = "mqttgo.io";
const int mqtt_port = 1883;
const char* mqtt_client_id = "alex9ufo-wokwi-client-dualcore";

const char* led_control_topic = "alex9ufo/ledcontrol";
const char* led_status_topic = "alex9ufo/ledstatus";
const char* temp_humi_topic = "alex9ufo/temphumi";

// --- 全域變數 ---
WiFiClient espClient;
PubSubClient client(espClient);
QueueHandle_t mqttPublishQueue;

volatile bool dhtReadTriggered = false;
volatile float lastTemperature = 0.0;
volatile float lastHumidity = 0.0;

// --- 函式實作 ---

void setup_wifi() {
  delay(10);
  Serial.print("\nConnecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected.");
}

void mqttSubscribeCallback(char* topic, byte* payload, unsigned int length) {
  payload[length] = '\0';
  String message = String((char*)payload);

  if (String(topic) == led_control_topic) {
    String publishMsg = "";
    if (message == "1on") { digitalWrite(ledPins[0], HIGH); publishMsg = "LED1 ON"; }
    else if (message == "1off") { digitalWrite(ledPins[0], LOW); publishMsg = "LED1 OFF"; }
    else if (message == "2on") { digitalWrite(ledPins[1], HIGH); publishMsg = "LED2 ON"; }
    else if (message == "2off") { digitalWrite(ledPins[1], LOW); publishMsg = "LED2 OFF"; }
    else if (message == "3on") { digitalWrite(ledPins[2], HIGH); publishMsg = "LED3 ON"; }
    else if (message == "3off") { digitalWrite(ledPins[2], LOW); publishMsg = "LED3 OFF"; }
    else if (message == "4on") { digitalWrite(ledPins[3], HIGH); publishMsg = "LED4 ON"; }
    else if (message == "4off") { digitalWrite(ledPins[3], LOW); publishMsg = "LED4 OFF"; }
    else if (message == "allon") {
      for (int i = 0; i < NUM_LEDS; i++) digitalWrite(ledPins[i], HIGH);
      publishMsg = "ALL ON";
    } else if (message == "alloff") {
      for (int i = 0; i < NUM_LEDS; i++) digitalWrite(ledPins[i], LOW);
      publishMsg = "ALL OFF";
    }

    if (publishMsg.length() > 0) {
      char msgBuffer[50];
      publishMsg.toCharArray(msgBuffer, sizeof(msgBuffer));
      xQueueSend(mqttPublishQueue, &msgBuffer, portMAX_DELAY);
    }
  }
}

void reconnect() {
  while (!client.connected()) {
    if (client.connect(mqtt_client_id)) {
      client.subscribe(led_control_topic);
    } else {
      vTaskDelay(5000 / portTICK_PERIOD_MS);
    }
  }
}

void readDHT22() {
  sensors_event_t event;
  dht.temperature().getEvent(&event);
  if (!isnan(event.temperature)) {
    lastTemperature = event.temperature;
    dht.humidity().getEvent(&event);
    if (!isnan(event.relative_humidity)) {
      lastHumidity = event.relative_humidity;
      dhtReadTriggered = true;
     
      // 更新 LCD 顯示
      lcd.setCursor(0, 0);
      lcd.print("Temp: "); lcd.print(lastTemperature, 1); lcd.print(" C   ");
      lcd.setCursor(0, 1);
      lcd.print("Humi: "); lcd.print(lastHumidity, 1); lcd.print(" %   ");
    }
  }
}

// --- Core 0: 處理訂閱、定時器與 LCD ---
void core0Task(void * parameter) {
  client.setCallback(mqttSubscribeCallback);
  unsigned long lastDHTReadTime = 0;
  const unsigned long dhtInterval = 10000;

  while (true) {
    if (!client.connected()) reconnect();
    client.loop();

    unsigned long currentMillis = millis();
    if (currentMillis - lastDHTReadTime >= dhtInterval) {
      lastDHTReadTime = currentMillis;
      readDHT22();
    }

    if (Serial.available()) {
      if (Serial.readStringUntil('\n') == "") readDHT22();
    }
    vTaskDelay(10 / portTICK_PERIOD_MS);
  }
}

// --- Core 1: 處理 MQTT 發布 ---
void core1Task(void * parameter) {
  char msgBuffer[50];
  while (true) {
    if (xQueueReceive(mqttPublishQueue, &msgBuffer, 0) == pdTRUE) {
      if (client.connected()) client.publish(led_status_topic, msgBuffer);
    }

    if (dhtReadTriggered) {
      if (client.connected()) {
        String msg = "Temp: " + String(lastTemperature) + "C, Humi: " + String(lastHumidity) + "%";
        client.publish(temp_humi_topic, msg.c_str());
      }
      dhtReadTriggered = false;
    }
    vTaskDelay(50 / portTICK_PERIOD_MS);
  }
}

void setup() {
  Serial.begin(115200);
 
  // 初始化 LCD (指定 SDA, SCL)
  Wire.begin(SDA_PIN, SCL_PIN);
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("System Ready");
  lcd.setCursor(0, 1);
  lcd.print("Connecting...");

  for (int i = 0; i < NUM_LEDS; i++) {
    pinMode(ledPins[i], OUTPUT);
    digitalWrite(ledPins[i], LOW);
  }

  dht.begin();
  setup_wifi();
  client.setServer(mqtt_broker, mqtt_port);
 
  mqttPublishQueue = xQueueCreate(10, sizeof(char[50]));

  xTaskCreatePinnedToCore(core0Task, "Core0Task", 10000, NULL, 1, NULL, 0);
  xTaskCreatePinnedToCore(core1Task, "Core1Task", 10000, NULL, 1, NULL, 1);
}

void loop() {
  vTaskDelete(NULL);
}

程式碼利用了 ESP32 的雙核心 (Dual-core) 特性與 FreeRTOS 作業系統,將「網路指令接收與顯示」與「數據發送」拆分處理,確保系統運行流暢。

以下是程式碼的逐行詳細說明:


1. 引入庫與硬體定義

C++
#include <WiFi.h>              // 用於 ESP32 的 Wi-Fi 連線
#include <PubSubClient.h>      // MQTT 通訊協定庫
#include <Adafruit_Sensor.h>   // 感測器統一驅動框架
#include <DHT.h>               // DHT 感測器庫
#include <DHT_U.h>             // DHT 統一介面庫
#include <Wire.h>              // I2C 通訊庫
#include <LiquidCrystal_I2C.h> // 1602 LCD I2C 驅動庫
#include <freertos/FreeRTOS.h> // FreeRTOS 核心
#include <freertos/task.h>     // FreeRTOS 多工任務管理

這部分載入所有必要的工具套件,包含網路、MQTT、感測器以及多工處理所需的庫。

C++
#define DHTPIN 4      // DHT22 訊號線接到 GPIO 4
#define DHTTYPE DHT22 // 定義感測器型號為 DHT22
#define SDA_PIN 21    // I2C 數據線
#define SCL_PIN 22    // I2C 時脈線

DHT_Unified dht(DHTPIN, DHTTYPE); // 初始化 DHT 物件
LiquidCrystal_I2C lcd(0x27, 16, 2); // 初始化 LCD (位址 0x27, 16欄2列)
const int ledPins[] = {13, 12, 14, 27}; // 定義 4 顆 LED 的腳位

2. MQTT 與全域變數

C++
const char* led_control_topic = "alex9ufo/ledcontrol"; // 訂閱指令的主題
const char* led_status_topic = "alex9ufo/ledstatus";   // 回報 LED 狀態的主題
const char* temp_humi_topic = "alex9ufo/temphumi";     // 發布溫濕度數據的主題

QueueHandle_t mqttPublishQueue; // 定義一個「佇列」,用於在兩個核心之間傳遞要發送的消息
volatile bool dhtReadTriggered = false; // 旗標:通知 Core 1 該發送感測數據了
volatile float lastTemperature = 0.0;   // 儲存最新的溫度值
volatile float lastHumidity = 0.0;      // 儲存最新的濕度值
  • Queue (佇列):這是跨核心通訊的橋樑,Core 0 把訊息「丟進去」,Core 1 從「另一頭拿出來」發送。


3. 功能函式 (Helpers)

mqttSubscribeCallback (處理收到的指令)

當 MQTT 經紀人收到發往 ledcontrol 的訊息時會觸發此函式:

  • 解析指令:判斷是 1on, 1off ... 到 alloff

  • 執行硬體動作:使用 digitalWrite 切換 LED。

  • 準備回饋訊息:將執行結果字串放入 mqttPublishQueue 佇列,交由另一個核心發送回雲端。

readDHT22 (讀取感測器與更新 LCD)

  • 讀取數值:獲取溫度與濕度。

  • 更新旗標:設定 dhtReadTriggered = true

  • LCD 刷新:將數據即時顯示在 1602 螢幕上。


4. FreeRTOS 多工任務 (核心邏輯)

core0Task:運行在 Core 0 (管理員任務)

C++
void core0Task(void * parameter) {
  // 1. 設定 MQTT 回調
  // 2. 無限迴圈處理:
  //    - 保持 MQTT 連線 (reconnect)
  //    - 處理訂閱消息 (client.loop)
  //    - 每 10 秒自動呼叫 readDHT22()
  //    - 監聽序列埠,按下 Enter 手動觸發讀取
}
  • 這個任務專門負責「輸入」與「本地顯示」,確保網路反應快速。

core1Task:運行在 Core 1 (通訊員任務)

C++
void core1Task(void * parameter) {
  // 無限迴圈處理:
  // 1. 檢查佇列 (Queue):若有 LED 狀態訊息,就 publish 出去。
  // 2. 檢查旗標 (dhtReadTriggered):若為 true,就把溫濕度發布出去。
}
  • 這個任務專門負責「輸出」,避免頻繁的網路發送動作卡住主程式的硬體反應。


5. 初始化與主循環

C++
void setup() {
  Wire.begin(SDA_PIN, SCL_PIN); // 啟動 I2C
  lcd.init();                   // 初始化 LCD
  lcd.backlight();              // 開啟背光
  
  dht.begin();                  // 啟動 DHT
  setup_wifi();                 // 連線 Wi-Fi
  
  // 創建佇列:長度 10,每個元素 50 字節
  mqttPublishQueue = xQueueCreate(10, sizeof(char[50]));

  // 任務指派:指定 Core 0 執行訂閱與顯示,Core 1 執行數據發送
  xTaskCreatePinnedToCore(core0Task, "Core0Task", 10000, NULL, 1, NULL, 0);
  xTaskCreatePinnedToCore(core1Task, "Core1Task", 10000, NULL, 1, NULL, 1);
}

void loop() {
  vTaskDelete(NULL); // 刪除 loop 任務,因為所有工作都交給了上述兩個 Task
}

總結運作流程:

  1. 計時器觸發:Core 0 發現 10 秒到了,讀取 DHT22 並更新 LCD

  2. 跨核通知:Core 0 更新全域變數並舉起「旗標」。

  3. 雲端發送:Core 1 偵測到旗標,把溫濕度字串透過 MQTT 發送到 temphumi 主題。

  4. 指令控制:若你從手機發送 1on,Core 0 接收後點亮 LED,並把訊息塞進「佇列」,Core 1 隨後將狀態報回雲端。


Python+TKinter程式

import tkinter as tk from tkinter import ttk, messagebox import paho.mqtt.client as mqtt import threading # --- MQTT 設定 --- MQTT_BROKER = "mqttgo.io" MQTT_PORT = 1883 TOPIC_CONTROL = "alex9ufo/ledcontrol" TOPIC_STATUS = "alex9ufo/ledstatus" TOPIC_SENSOR = "alex9ufo/temphumi" class MQTTApp: def __init__(self, root): self.root = root self.root.title("ESP32 雙核監控中心") self.root.geometry("400x500") self.root.configure(bg="#f0f0f0") # UI 標題 title_label = tk.Label(root, text="智慧家居控制系統", font=("微軟正黑體", 16, "bold"), bg="#f0f0f0") title_label.pack(pady=10) # --- 溫濕度顯示區 --- sensor_frame = tk.LabelFrame(root, text="即時感測數據", padx=10, pady=10, bg="#ffffff") sensor_frame.pack(padx=20, pady=10, fill="x") self.temp_var = tk.StringVar(value="-- °C") self.humi_var = tk.StringVar(value="-- %") tk.Label(sensor_frame, text="溫度:", bg="#ffffff").grid(row=0, column=0, sticky="w") tk.Label(sensor_frame, textvariable=self.temp_var, font=("Arial", 14, "bold"), fg="#d32f2f", bg="#ffffff").grid(row=0, column=1, padx=20) tk.Label(sensor_frame, text="濕度:", bg="#ffffff").grid(row=1, column=0, sticky="w") tk.Label(sensor_frame, textvariable=self.humi_var, font=("Arial", 14, "bold"), fg="#1976d2", bg="#ffffff").grid(row=1, column=1, padx=20) # --- LED 控制區 --- control_frame = tk.LabelFrame(root, text="LED 遠端控制", padx=10, pady=10, bg="#ffffff") control_frame.pack(padx=20, pady=10, fill="x") # 建立 4 個 LED 的控制按鈕 for i in range(1, 5): btn_on = ttk.Button(control_frame, text=f"開啟 LED {i}", command=lambda idx=i: self.send_command(f"{idx}on")) btn_on.grid(row=i-1, column=0, pady=5, padx=5) btn_off = ttk.Button(control_frame, text=f"關閉 LED {i}", command=lambda idx=i: self.send_command(f"{idx}off")) btn_off.grid(row=i-1, column=1, pady=5, padx=5) # 全開/全關 ttk.Button(root, text="全部開啟", command=lambda: self.send_command("allon")).pack(pady=5) ttk.Button(root, text="全部關閉", command=lambda: self.send_command("alloff")).pack(pady=2) # 狀態列 self.status_bar = tk.Label(root, text="正在連線至 MQTT...", bd=1, relief=tk.SUNKEN, anchor=tk.W) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) # --- 初始化 MQTT --- self.client = mqtt.Client() self.client.on_connect = self.on_connect self.client.on_message = self.on_message # 啟動 MQTT 執行緒,避免 GUI 卡死 mqtt_thread = threading.Thread(target=self.start_mqtt, daemon=True) mqtt_thread.start() def start_mqtt(self): try: self.client.connect(MQTT_BROKER, MQTT_PORT, 60) self.client.loop_forever() except Exception as e: self.status_bar.config(text=f"連線錯誤: {e}") def on_connect(self, client, userdata, flags, rc): if rc == 0: self.status_bar.config(text="MQTT 已連線") client.subscribe([(TOPIC_STATUS, 0), (TOPIC_SENSOR, 0)]) else: self.status_bar.config(text=f"連線失敗 代碼: {rc}") def on_message(self, client, userdata, msg): payload = msg.payload.decode() # 處理溫濕度數據 (格式: Temperature: 25.0C, Humidity: 60.0%) if msg.topic == TOPIC_SENSOR: try: # 簡單解析字串 parts = payload.split(", ") temp = parts[0].split(": ")[1] humi = parts[1].split(": ")[1] self.temp_var.set(temp) self.humi_var.set(humi) except: pass # 處理 LED 狀態回饋 elif msg.topic == TOPIC_STATUS: self.status_bar.config(text=f"設備狀態: {payload}") def send_command(self, cmd): self.client.publish(TOPIC_CONTROL, cmd) if __name__ == "__main__": root = tk.Tk() app = MQTTApp(root) root.mainloop()


Python 程式結合了 Tkinter (圖形化介面)Paho-MQTT (通訊協定),並使用 Threading (多執行緒) 確保介面流暢。以下是逐行詳細說明:


1. 匯入模組

Python
import tkinter as tk                 # 建立 GUI 視窗的核心庫
from tkinter import ttk, messagebox  # 使用更美觀的按鈕元件與對話框
import paho.mqtt.client as mqtt      # MQTT 通訊協定庫
import threading                     # 用於同時執行背景連線與前端介面

2. MQTT 與主題設定

Python
MQTT_BROKER = "mqttgo.io"            # MQTT 伺服器網址
MQTT_PORT = 1883                     # MQTT 通訊埠 (標準為 1883)
TOPIC_CONTROL = "alex9ufo/ledcontrol" # 發送指令的主題 (Python -> ESP32)
TOPIC_STATUS = "alex9ufo/ledstatus"   # 訂閱 LED 狀態的主題 (ESP32 -> Python)
TOPIC_SENSOR = "alex9ufo/temphumi"    # 訂閱感測器數據的主題 (ESP32 -> Python)

3. 初始化介面 (__init__)

Python
class MQTTApp:
    def __init__(self, root):
        self.root = root
        self.root.title("ESP32 雙核監控中心")
        self.root.geometry("400x500")      # 設定視窗大小
        self.root.configure(bg="#f0f0f0") # 設定背景顏色

        # --- 溫濕度顯示區 ---
        # 使用 StringVar 綁定變數,當變數改變時,介面上的文字會自動更新
        self.temp_var = tk.StringVar(value="-- °C")
        self.humi_var = tk.StringVar(value="-- %")
        # (中間省略標籤佈局:使用 grid 將標籤排列在 sensor_frame 中)

        # --- LED 控制按鈕 (動態產生) ---
        for i in range(1, 5):
            # 建立開啟按鈕,使用 lambda 傳入特定參數 (如 1on, 2on)
            btn_on = ttk.Button(control_frame, text=f"開啟 LED {i}", 
                                command=lambda idx=i: self.send_command(f"{idx}on"))
            # 建立關閉按鈕 (如 1off, 2off)
            btn_off = ttk.Button(control_frame, text=f"關閉 LED {i}", 
                                 command=lambda idx=i: self.send_command(f"{idx}off"))

4. 多執行緒與連線 (start_mqtt)

Python
        # 啟動 MQTT 執行緒
        # 因為 loop_forever() 會卡死程式,必須放在 daemon 執行緒中背景執行
        mqtt_thread = threading.Thread(target=self.start_mqtt, daemon=True)
        mqtt_thread.start()

    def start_mqtt(self):
        self.client.connect(MQTT_BROKER, MQTT_PORT, 60)
        self.client.loop_forever() # 持續監聽訊息

5. MQTT 事件回調

  • on_connect:連線成功後觸發。此時向伺服器訂閱(Subscribe)TOPIC_STATUSTOPIC_SENSOR,這樣才能接收到來自 ESP32 的訊息。

  • on_message (核心解析邏輯)

    Python
    def on_message(self, client, userdata, msg):
        payload = msg.payload.decode() # 將接收到的二進位數據轉為字串
    
        if msg.topic == TOPIC_SENSOR:
            # 解析格式 "Temperature: 25.0C, Humidity: 60.0%"
            parts = payload.split(", ")
            temp = parts[0].split(": ")[1] # 取得 25.0C
            humi = parts[1].split(": ")[1] # 取得 60.0%
            self.temp_var.set(temp)        # 更新介面溫度文字
            self.humi_var.set(humi)        # 更新介面濕度文字
    

6. 指令發送 (send_command)

Python
    def send_command(self, cmd):
        # 當按鈕被按下時,將指令 (如 "allon") 發送到 alex9ufo/ledcontrol
        self.client.publish(TOPIC_CONTROL, cmd)

程式運作邏輯總結:

  1. 背景任務start_mqtt 在背景不斷檢查是否有新訊息。

  2. 接收訊息:當 ESP32 每 10 秒發出溫濕度時,on_message 會被觸發,拆解字串後,透過 StringVar 自動刷新 螢幕上的數據。

  3. 發送指令:當你點擊介面按鈕,Python 會立刻 Publish 訊息到雲端,ESP32 收到後會切換繼電器並控制 LED。

沒有留言:

張貼留言

2026 作業1 MQTT基本觀念

 2026 作業1MQTT基本觀念 作業  參考下面網址 https://alex9ufoexploer.blogspot.com/2025/02/1-mqtt-relay-dht22-mqtt-box-pc-mymqtt.html https://alex9ufoexploer...