2025年7月8日 星期二

ESP32 雙核心控制 LED 與 DHT22 溫濕度感測器 (Wokwi 模擬) EX2 --Python TKinter

ESP32 雙核心控制 LED 與 DHT22 溫濕度感測器 (Wokwi 模擬) EX2 --Python TKinter 







  Tkinter 程式將作為一個桌面應用,可以讓你:

  1. 直接點擊按鈕控制 ESP32 上的 LED (開/關/閃爍/定時)。

  2. 即時顯示從 ESP32 接收到的 DHT22 溫濕度數據。

  3. 顯示 ESP32 的連線狀態


Tkinter GUI 應用程式架構

這個 Tkinter 應用程式仍然會依賴 MQTT Broker 來與 ESP32 溝通。

  1. Tkinter GUI 介面

    • 提供按鈕來發送 LED 控制指令到 MQTT Broker。

    • 顯示文字標籤來更新溫濕度數據和設備狀態。

  2. MQTT 客戶端 (在 Tkinter 程式內)

    • 連接到相同的 MQTT Broker (broker.hivemq.com)。

    • 發布 LED 控制指令到 telegram_iot/esp32/led_control 主題。

    • 訂閱 telegram_iot/esp32/temperature, telegram_iot/esp32/humidity, telegram_iot/esp32/status 主題,以接收來自 ESP32 的數據和狀態更新。

    • 在背景執行緒中處理 MQTT 訊息,並安全地更新 Tkinter 介面。

Python程式 (在THONNY上執行)


import tkinter as tk

from tkinter import messagebox

import paho.mqtt.client as mqtt

import threading

import time

import sys


# --- MQTT 設定 (必須與 ESP32 程式碼中的主題名稱一致) ---

MQTT_BROKER = "broker.mqttgo.io"

MQTT_PORT = 1883

MQTT_CLIENT_ID = "Tkinter_GUI_Controller_001" # 這個 GUI 程式的唯一 MQTT 客戶端 ID


# MQTT 主題 (與 ESP32 程式碼一致)

TELEGRAM_MQTT_TOPIC_LED_CONTROL = "telegram_iot/esp32/led_control"

TELEGRAM_MQTT_TOPIC_TEMPERATURE = "telegram_iot/esp32/temperature"

TELEGRAM_MQTT_TOPIC_HUMIDITY = "telegram_iot/esp32/humidity"

TELEGRAM_MQTT_TOPIC_STATUS = "telegram_iot/esp32/status"


# --- 全域變數,用於儲存最新的數據和狀態 ---

latest_temperature = "N/A"

latest_humidity = "N/A"

esp32_status = "Offline" # 初始狀態


# --- Tkinter GUI 介面設定 ---

class MqttControllerApp:

    def __init__(self, master):

        self.master = master

        master.title("ESP32 IoT 控制面板")

        master.geometry("400x300") # 設定視窗大小


        # 溫濕度顯示標籤

        self.temp_label = tk.Label(master, text=f"溫度: {latest_temperature}°C", font=("Arial", 14))

        self.temp_label.pack(pady=5)

        self.humidity_label = tk.Label(master, text=f"濕度: {latest_humidity}%", font=("Arial", 14))

        self.humidity_label.pack(pady=5)

        self.status_label = tk.Label(master, text=f"ESP32 狀態: {esp32_status}", font=("Arial", 12), fg="red")

        self.status_label.pack(pady=5)


        # LED 控制按鈕

        self.led_frame = tk.LabelFrame(master, text="LED 控制", padx=10, pady=10)

        self.led_frame.pack(pady=10)


        self.btn_on = tk.Button(self.led_frame, text="開啟 LED", command=lambda: self.publish_led_command("on"), width=15)

        self.btn_on.grid(row=0, column=0, padx=5, pady=5)


        self.btn_off = tk.Button(self.led_frame, text="關閉 LED", command=lambda: self.publish_led_command("off"), width=15)

        self.btn_off.grid(row=0, column=1, padx=5, pady=5)


        self.btn_flash = tk.Button(self.led_frame, text="LED 閃爍", command=lambda: self.publish_led_command("flash"), width=15)

        self.btn_flash.grid(row=1, column=0, padx=5, pady=5)


        self.btn_timer = tk.Button(self.led_frame, text="定時關閉 (10秒)", command=lambda: self.publish_led_command("timer"), width=15)

        self.btn_timer.grid(row=1, column=1, padx=5, pady=5)


        # MQTT 客戶端初始化和連線

        self.mqtt_client = mqtt.Client(client_id=MQTT_CLIENT_ID)

        self.mqtt_client.on_connect = self._on_connect

        self.mqtt_client.on_message = self._on_message


        try:

            self.mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60)

            # 在一個單獨的執行緒中運行 MQTT 網路循環,以免阻塞 GUI

            self.mqtt_thread = threading.Thread(target=self.mqtt_client.loop_forever)

            self.mqtt_thread.daemon = True # 設定為守護執行緒,主程式退出時自動結束

            self.mqtt_thread.start()

            print("MQTT 客戶端啟動並嘗試連接 Broker...")

        except Exception as e:

            print(f"MQTT 連線失敗: {e}")

            messagebox.showerror("MQTT 連線錯誤", f"無法連接到 MQTT Broker: {e}")

            master.destroy() # 如果無法連線,直接關閉應用程式


        # 設置關閉視窗時的處理函數

        master.protocol("WM_DELETE_WINDOW", self.on_closing)

        

        # 啟動定時更新 GUI 的函數

        self.update_gui()


    def _on_connect(self, client, userdata, flags, rc):

        """MQTT 連線成功時的回調函數"""

        if rc == 0:

            print("成功連接到 MQTT Broker!")

            # 連線成功後,訂閱相關主題

            client.subscribe(TELEGRAM_MQTT_TOPIC_TEMPERATURE)

            client.subscribe(TELEGRAM_MQTT_TOPIC_HUMIDITY)

            client.subscribe(TELEGRAM_MQTT_TOPIC_STATUS)

            print(f"已訂閱: {TELEGRAM_MQTT_TOPIC_TEMPERATURE}, {TELEGRAM_MQTT_TOPIC_HUMIDITY}, {TELEGRAM_MQTT_TOPIC_STATUS}")

            self.master.after(0, self.update_status_label, "Online", "green") # 在 GUI 主執行緒中更新狀態

        else:

            print(f"無法連接到 MQTT Broker, 返回碼: {rc}")

            self.master.after(0, self.update_status_label, f"連線錯誤({rc})", "red")


    def _on_message(self, client, userdata, msg):

        """收到 MQTT 訊息時的回調函數"""

        global latest_temperature, latest_humidity, esp32_status

        topic = msg.topic

        payload = msg.payload.decode()

        print(f"收到訊息 - 主題: '{topic}', 內容: '{payload}'")


        if topic == TELEGRAM_MQTT_TOPIC_TEMPERATURE:

            latest_temperature = payload

        elif topic == TELEGRAM_MQTT_TOPIC_HUMIDITY:

            latest_humidity = payload

        elif topic == TELEGRAM_MQTT_TOPIC_STATUS:

            esp32_status = payload

            # 根據 ESP32 傳來的狀態更新狀態標籤顏色

            if esp32_status == "ESP32_online":

                self.master.after(0, self.update_status_label, "Online", "green")

            else:

                self.master.after(0, self.update_status_label, esp32_status, "red") # 假設其他狀態為離線或錯誤


        # 收到任何數據更新時,排程更新 GUI

        self.master.after(0, self.update_gui) # 使用 after() 在 GUI 主執行緒中安全地更新


    def publish_led_command(self, command):

        """發布 LED 控制指令到 MQTT Broker"""

        print(f"發布 LED 指令: {command}")

        self.mqtt_client.publish(TELEGRAM_MQTT_TOPIC_LED_CONTROL, command)


    def update_gui(self):

        """更新 Tkinter 介面上的數據顯示"""

        self.temp_label.config(text=f"溫度: {latest_temperature}°C")

        self.humidity_label.config(text=f"濕度: {latest_humidity}%")

        # 狀態標籤的更新由 _on_message 觸發,這裡只需確保數據是最新的

        

        # 每隔 100 毫秒再次排程更新,保持介面響應

        self.master.after(100, self.update_gui)


    def update_status_label(self, status_text, color):

        """更新狀態標籤的文字和顏色"""

        self.status_label.config(text=f"ESP32 狀態: {status_text}", fg=color)


    def on_closing(self):

        """視窗關閉時的回調函數,用於清理資源"""

        print("關閉應用程式,斷開 MQTT 連線...")

        self.mqtt_client.loop_stop() # 停止 MQTT 網路循環

        self.mqtt_client.disconnect() # 斷開 MQTT 連線

        self.master.destroy() # 銷毀 Tkinter 視窗


# --- 主程式入口 ---

if __name__ == "__main__":

    root = tk.Tk() # 創建 Tkinter 根視窗

    app = MqttControllerApp(root) # 實例化應用程式

    root.mainloop() # 啟動 Tkinter 事件循環




  1. 觀察狀態:剛開始運行時,"ESP32 狀態" 應該是 "Offline"。當你的 ESP32 連接到 MQTT Broker 並發送 "ESP32_online" 狀態後,GUI 上的狀態會更新為 "Online",顏色也會變成綠色。

  2. 控制 LED:點擊 "開啟 LED", "關閉 LED", "LED 閃爍", "定時關閉 (10秒)" 按鈕,觀察 Wokwi 模擬器或你的實際 ESP32 上的 LED 行為。

  3. 查看溫濕度:每當 ESP32 發布新的溫濕度數據時,Tkinter 介面上的「溫度」和「濕度」標籤會自動更新。

注意事項

  • 唯一性 IDMQTT_CLIENT_ID 在 ESP32 程式和 Tkinter 程式中都必須是獨特的。如果你運行多個 MQTT 客戶端,它們應該有不同的 Client ID。

  • 多執行緒:Tkinter GUI 程式使用了一個單獨的執行緒 (threading.Thread) 來運行 mqtt_client.loop_forever()。這是為了確保 MQTT 訊息處理不會阻塞 Tkinter 的主事件循環,使 GUI 保持響應。

  • 安全更新 GUI:來自 MQTT 執行緒的數據更新必須透過 self.master.after(0, ...) 排程到 Tkinter 的主執行緒中執行,這是 Tkinter 的最佳實踐,以避免在非 GUI 執行緒中直接修改 GUI 元件導致的錯誤。

  • 錯誤處理:程式中包含了基本的 MQTT 連線錯誤處理,如果無法連線會彈出警告並關閉程式。

這個 Tkinter 應用程式提供了一個直觀的方式來控制你的 IoT 設備並監控數據,對於桌面控制和展示非常有用。




WOKWI上 Arduino esp32程式

#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <DHT_U.h> // 需要同時包含 DHT_U.h

// --- Wi-Fi 設定 ---
// 如果在 Wokwi 上測試,請使用 "Wokwi-GUEST"
const char* ssid = "Wokwi-GUEST";
const char* password = "";

// --- MQTT 設定 ---
const char* mqtt_server = "broker.mqttgo.io";
const int mqtt_port = 1883;
// 為你的 ESP32 指定一個獨特的 MQTT 客戶端 ID,以避免衝突
const char* mqtt_client_id = "ESP32_Wokwi_Telegram_IoT_001"; // <<< 請更換為你的唯一 ID

// --- MQTT 主題定義 ---
// 這些主題名稱必須與你的 Telegram Bot 後端程式碼完全一致!
const char* TELEGRAM_MQTT_TOPIC_LED_CONTROL = "telegram_iot/esp32/led_control";   // Telegram Bot 發布 LED 指令到這裡
const char* TELEGRAM_MQTT_TOPIC_TEMPERATURE = "telegram_iot/esp32/temperature";   // ESP32 發布溫度到這裡
const char* TELEGRAM_MQTT_TOPIC_HUMIDITY = "telegram_iot/esp32/humidity";     // ESP32 發布濕度到這裡
const char* TELEGRAM_MQTT_TOPIC_STATUS = "telegram_iot/esp32/status";       // ESP32 發布上線狀態到這裡 (可選)

// --- WiFi 和 MQTT 客戶端物件 ---
WiFiClient espClient;
PubSubClient client(espClient);

// --- LED 設定 ---
const int ledPin = 2; // 連接到 ESP32 的 GPIO 2
enum LedMode { OFF, ON, FLASH, TIMER }; // OFF 放在最前面,作為預設安全值
volatile LedMode currentLedMode = OFF;
volatile unsigned long timerStartTime = 0;
volatile bool ledState = false; // 用於閃爍模式

// --- DHT22 感測器設定 ---
#define DHTPIN 4      // 連接到 ESP32 的 GPIO 4
#define DHTTYPE DHT22 // DHT 22 (AM2302)
DHT dht(DHTPIN, DHTTYPE);

// --- FreeRTOS 任務句柄 ---
TaskHandle_t TaskLEDControl = NULL;
TaskHandle_t TaskDHTSensor = NULL;

// --- 函式宣告 ---
void setup_wifi();
void reconnect_mqtt();
void mqtt_callback(char* topic, byte* payload, unsigned int length); // 重命名以提高清晰度
void ledControlTask(void *pvParameters);
void dhtSensorTask(void *pvParameters);

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW); // 確保 LED 初始是關閉的

  setup_wifi(); // 連接 Wi-Fi
  client.setServer(mqtt_server, mqtt_port); // 設定 MQTT Broker
  client.setCallback(mqtt_callback); // 設定 MQTT 訊息回調函式

  dht.begin(); // 初始化 DHT 感測器

  // 創建 LED 控制任務,運行在 Core 0
  xTaskCreatePinnedToCore(
    ledControlTask,    /* 任務函式 */
    "LED Control",     /* 任務名稱 */
    2048,              /* 堆疊大小 (字節) */
    NULL,              /* 任務參數 */
    1,                 /* 任務優先級 */
    &TaskLEDControl,   /* 任務句柄 */
    0                  /* 運行在 Core 0 */
  );

  // 創建 DHT 感測器任務,運行在 Core 1
  xTaskCreatePinnedToCore(
    dhtSensorTask,     /* 任務函式 */
    "DHT Sensor",      /* 任務名稱 */
    4096,              /* 堆疊大小 (字節) */
    NULL,              /* 任務參數 */
    1,                 /* 任務優先級 */
    &TaskDHTSensor,    /* 任務句柄 */
    1                  /* 運行在 Core 1 */
  );
}

void loop() {
  // 主循環中只負責維持 MQTT 連線和處理 MQTT 訊息
  if (!client.connected()) {
    reconnect_mqtt();
  }
  client.loop(); // 處理所有傳入和傳出的 MQTT 訊息
  delay(10); // 短暫延遲,避免佔用過多 CPU 資源
}

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

  WiFi.begin(ssid, password);

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

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

// --- MQTT 重連函式 ---
void reconnect_mqtt() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // 嘗試連線
    if (client.connect(mqtt_client_id)) {
      Serial.println("connected");
      // 連線成功後,訂閱 LED 控制主題,接收來自 Telegram Bot 的指令
      client.subscribe(TELEGRAM_MQTT_TOPIC_LED_CONTROL);
      Serial.print("Subscribed to: ");
      Serial.println(TELEGRAM_MQTT_TOPIC_LED_CONTROL);

      // 可選:發布上線狀態,讓 Telegram Bot 知道 ESP32 已啟動
      client.publish(TELEGRAM_MQTT_TOPIC_STATUS, "ESP32_online");
      Serial.println("Published ESP32 online status.");

    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000); // 等待 5 秒後重試
    }
  }
}

// --- MQTT 訊息回調函式 ---
// 當收到訂閱主題的訊息時,此函式會被調用
void mqtt_callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  String message = "";
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.println(message);

  // 檢查是否為 LED 控制指令
  if (String(topic) == TELEGRAM_MQTT_TOPIC_LED_CONTROL) {
    if (message == "on") {
      currentLedMode = ON;
      digitalWrite(ledPin, HIGH);
      Serial.println("LED Mode: ON (來自 Telegram)");
    } else if (message == "off") {
      currentLedMode = OFF;
      digitalWrite(ledPin, LOW);
      Serial.println("LED Mode: OFF (來自 Telegram)");
    } else if (message == "flash") {
      currentLedMode = FLASH;
      Serial.println("LED Mode: FLASH (來自 Telegram)");
    } else if (message == "timer") {
      currentLedMode = TIMER;
      digitalWrite(ledPin, HIGH); // 定時模式開始時先開啟 LED
      timerStartTime = millis();
      Serial.println("LED Mode: TIMER (10s 來自 Telegram)");
    } else {
      Serial.println("未知 LED 指令。");
    }
  }
}

// --- LED 控制任務 (運行在 Core 0) ---
void ledControlTask(void *pvParameters) {
  (void) pvParameters; // 避免編譯器警告

  for (;;) { // 無限循環
    switch (currentLedMode) {
      case ON:
        // LED 保持亮著,狀態由 mqtt_callback 設置
        break;
      case OFF:
        // LED 保持熄滅,狀態由 mqtt_callback 設置
        break;
      case FLASH:
        digitalWrite(ledPin, ledState);
        ledState = !ledState;
        vTaskDelay(pdMS_TO_TICKS(500)); // 每 500ms 改變一次狀態
        break;
      case TIMER:
        if (millis() - timerStartTime >= 10000) { // 10 秒
          digitalWrite(ledPin, LOW);
          currentLedMode = OFF; // 定時結束後轉為 OFF 模式
          Serial.println("LED 定時結束,LED 關閉。");
        }
        vTaskDelay(pdMS_TO_TICKS(10)); // 短暫延遲
        break;
      default:
        digitalWrite(ledPin, LOW); // 預設為關閉
        break;
    }
    vTaskDelay(pdMS_TO_TICKS(10)); // 短暫延遲,讓其他任務有機會執行
  }
}

// --- DHT 感測器任務 (運行在 Core 1) ---
void dhtSensorTask(void *pvParameters) {
  (void) pvParameters; // 避免編譯器警告

  for (;;) { // 無限循環
    // 每 5 秒讀取一次數據,並發布到 MQTT
    delay(2000); // 延遲,避免頻繁讀取 DHT
    float h = dht.readHumidity();
    float t = dht.readTemperature(); // 讀取攝氏溫度

    // 檢查是否讀取失敗
    if (isnan(h) || isnan(t)) {
      Serial.println(F("從 DHT 感測器讀取失敗!"));
    } else {
      Serial.print(F("濕度: "));
      Serial.print(h);
      Serial.print(F("%  溫度: "));
      Serial.print(t);
      Serial.println(F("°C"));

      // 將浮點數轉換為字串,並發布到 MQTT
      char tempString[8];
      dtostrf(t, 4, 2, tempString);
      client.publish(TELEGRAM_MQTT_TOPIC_TEMPERATURE, tempString);

      char humString[8];
      dtostrf(h, 4, 2, humString);
      client.publish(TELEGRAM_MQTT_TOPIC_HUMIDITY, humString);
    }
    vTaskDelay(pdMS_TO_TICKS(5000)); // 每 5 秒執行一次發布
  }
}


沒有留言:

張貼留言

ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite

 ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite  ESP32 VS Code 程式 ; PlatformIO Project Configuration File ; ;   Build op...