2026年5月29日 星期五

IoT 智能控制面板 (Python Tkinter)

 IoT 智能控制面板  (Python Tkinter)

customtkinter

為了在 tkinter 中達到如同網頁般的圓角平滑陰影視覺效果,最頂級且優雅的作法是使用 CustomTkinter 庫。







安裝 Python MQTT 套件

 paho-mqtt   與  customtkinter


Python 與 Wokwi 的主題變數必須一字不差:

  • LED 控制:alex9ufo/home/led

  • 溫度:alex9ufo/home/temperature

  • 濕度:alex9ufo/home/humidity


Python程式

import customtkinter as ctk

import paho.mqtt.client as mqtt

import threading

import json


# --- 基礎視覺風格設定 ---

ctk.set_appearance_mode("Light")

ctk.set_default_color_theme("green")


# --- MQTT 參數設定(必須與 Wokwi 完全一致) ---

MQTT_BROKER = "broker.emqx.io"

MQTT_PORT = 1883

TOPIC_LED = "alex9ufo/home/led"

TOPIC_TEMP = "alex9ufo/home/temperature"

TOPIC_HUM = "alex9ufo/home/humidity"


class EnvironmentDashboard(ctk.CTk):

    def __init__(self):

        super().__init__()


        # 視窗設定

        self.title("IoT 智能控制面板 (Python MQTT 連動版)")

        self.geometry("540x420")

        self.configure(fg_color="#F5F5F7")


        # 建立 UI 介面

        self.setup_ui()


        # 啟動後台 MQTT 連線

        self.start_mqtt()


    def setup_ui(self):

        # 1. 頂部標題欄與狀態 (Header)

        self.header_frame = ctk.CTkFrame(self, fg_color="transparent")

        self.header_frame.pack(fill="x", padx=40, pady=(40, 20))


        self.title_label = ctk.CTkLabel(

            self.header_frame, text="環境主控台", 

            font=ctk.CTkFont(family="Arial", size=26, weight="bold"), text_color="#1D1D1F"

        )

        self.title_label.pack(side="left")


        self.status_frame = ctk.CTkFrame(self.header_frame, fg_color="transparent")

        self.status_frame.pack(side="right")


        self.status_dot = ctk.CTkLabel(self.status_frame, text="●", font=ctk.CTkFont(size=14), text_color="#FF3B30") # 預設紅色斷線

        self.status_dot.pack(side="left", padx=(0, 6))


        self.status_text = ctk.CTkLabel(self.status_frame, text="連線中...", font=ctk.CTkFont(family="Arial", size=14, weight="bold"), text_color="#86868B")

        self.status_text.pack(side="left")


        # 2. 數據卡片區域 (Grid Layout)

        self.grid_frame = ctk.CTkFrame(self, fg_color="transparent")

        self.grid_frame.pack(fill="x", padx=40, pady=10)

        self.grid_frame.grid_columnconfigure(0, weight=1)

        self.grid_frame.grid_columnconfigure(1, weight=1)


        # 🌡️ 溫度卡片

        self.temp_card = ctk.CTkFrame(self.grid_frame, fg_color="#FFFFFF", corner_radius=16)

        self.temp_card.grid(row=0, column=0, padx=(0, 10), sticky="nsew")

        self.temp_title = ctk.CTkLabel(self.temp_card, text="🌡️ 當前溫度", font=ctk.CTkFont(family="Arial", size=14, weight="bold"), text_color="#86868B")

        self.temp_title.pack(anchor="w", padx=20, pady=(20, 5))

        self.temp_value = ctk.CTkLabel(self.temp_card, text="--.- °C", font=ctk.CTkFont(family="Arial", size=28, weight="bold"), text_color="#1D1D1F")

        self.temp_value.pack(anchor="w", padx=20, pady=(0, 20))


        # 💧 濕度卡片

        self.hum_card = ctk.CTkFrame(self.grid_frame, fg_color="#FFFFFF", corner_radius=16)

        self.hum_card.grid(row=0, column=1, padx=(10, 0), sticky="nsew")

        self.hum_title = ctk.CTkLabel(self.hum_card, text="💧 當前濕度", font=ctk.CTkFont(family="Arial", size=14, weight="bold"), text_color="#86868B")

        self.hum_title.pack(anchor="w", padx=20, pady=(20, 5))

        self.hum_value = ctk.CTkLabel(self.hum_card, text="--.- %", font=ctk.CTkFont(family="Arial", size=28, weight="bold"), text_color="#1D1D1F")

        self.hum_value.pack(anchor="w", padx=20, pady=(0, 20))


        # 3. LED 控制卡片區域

        self.control_card = ctk.CTkFrame(self, fg_color="#FFFFFF", corner_radius=16)

        self.control_card.pack(fill="x", padx=40, pady=20)


        self.control_info_frame = ctk.CTkFrame(self.control_card, fg_color="transparent")

        self.control_info_frame.pack(side="left", padx=24, pady=20)


        self.control_title = ctk.CTkLabel(self.control_info_frame, text="遠端 LED 節點", font=ctk.CTkFont(family="Arial", size=18, weight="bold"), text_color="#1D1D1F")

        self.control_title.pack(anchor="w")


        self.control_status = ctk.CTkLabel(self.control_info_frame, text="目前狀態:關閉", font=ctk.CTkFont(family="Arial", size=13), text_color="#86868B")

        self.control_status.pack(anchor="w", pady=(2, 0))


        self.led_switch = ctk.CTkSwitch(

            self.control_card, text="", command=self.on_switch_toggle,

            onvalue="ON", offvalue="OFF", progress_color="#34C759"

        )

        self.led_switch.pack(side="right", padx=24)

        self.led_switch.configure(state="disabled") # 連線前禁用開關


    # --- MQTT 核心邏輯 ---

    def start_mqtt(self):

        # 初始化 paho-mqtt 客戶端

        self.client = mqtt.Client()

        self.client.on_connect = self.on_mqtt_connect

        self.client.on_message = self.on_mqtt_message

        self.client.on_disconnect = self.on_mqtt_disconnect


        # 使用獨立線程執行 MQTT 循環,避免造成 Tkinter 視窗凍結卡死

        mqtt_thread = threading.Thread(target=self.mqtt_loop_runner, daemon=True)

        mqtt_thread.start()


    def mqtt_loop_runner(self):

        try:

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

            self.client.loop_forever() # 持續監聽

        except Exception as e:

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


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

        if rc == 0:

            print("Python 成功連線至 MQTT Broker!")

            # 切換 UI 狀態為已連線 (綠燈)

            self.status_dot.configure(text_color="#34C759")

            self.status_text.configure(text="已連線")

            self.led_switch.configure(state="normal")

            

            # 訂閱 Wokwi 的溫濕度主題

            self.client.subscribe(TOPIC_TEMP)

            self.client.subscribe(TOPIC_HUM)

        else:

            print(f"連線錯誤,錯誤碼: {rc}")


    def on_mqtt_disconnect(self, client, userdata, rc):

        print("與 MQTT Broker 斷開連線")

        self.status_dot.configure(text_color="#FF3B30")

        self.status_text.configure(text="已斷線")

        self.led_switch.configure(state="disabled")


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

        topic = msg.topic

        payload = msg.payload.decode("utf-8")

        print(f"收到 Wokwi 數據 -> 主題: {topic} | 內容: {payload}")


        # 即時更新 Tkinter 介面上的數字

        if topic == TOPIC_TEMP:

            self.temp_value.configure(text=f"{float(payload):.1f} °C")

        elif topic == TOPIC_HUM:

            self.hum_value.configure(text=f"{float(payload):.1f} %")


    def on_switch_toggle(self):

        # 取得目前開關狀態 (ON / OFF)

        command = self.led_switch.get()

        self.control_status.configure(text=f"目前狀態:{'開啟' if command == 'ON' else '關閉'}")

        

        # 發送指令給 Wokwi

        self.client.publish(TOPIC_LED, command)

        print(f"已發送控制指令給 Wokwi -> {command}")


if __name__ == "__main__":

    app = EnvironmentDashboard()

    app.mainloop()




#include <WiFi.h>
#include <PubSubClient.h>
#include <DHTesp.h>  // 使用 DHTesp 函式庫



// --- 網路與 MQTT 伺服器設定 ---
const char* ssid = "Wokwi-GUEST"; // Wokwi 專用虛擬 WiFi
const char* password = "";
const char* mqtt_server = "broker.emqx.io";
const int mqtt_port = 1883;

// 主題 (Topic) 設定 - 建議加上個人化後綴避免與他人衝突
//const char* topic_led = "alex9ufo/esp32/led";
//const char* topic_temp = "alex9ufo/esp32/temp";
//const char* topic_hum = "alex9ufo/esp32/hum";


// 定義 MQTT 主題 (已更新為 alex9ufo 專屬代稱)
const char* topic_led = "alex9ufo/home/led";
const char* topic_temp = "alex9ufo/home/temperature";
const char* topic_hum = "alex9ufo/home/humidity";

#define LED_PIN 2
#define DHTPIN 15

DHTesp dht;          // 宣告 DHTesp 物件
WiFiClient espClient;
PubSubClient client(espClient);

unsigned long lastMsg = 0;

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

// 接收來自 EasyBuilder Pro 的指令
void callback(char* topic, byte* payload, unsigned 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);

  if (String(topic) == topic_led) {
    if (message == "ON") {
      digitalWrite(LED_PIN, HIGH);
    } else if (message == "OFF") {
      digitalWrite(LED_PIN, LOW);
    }
  }
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // 隨機生成 Client ID 避免與他人衝突導致頻繁斷線
    String clientId = "ESP32Client-" + String(random(0, 0xffff), HEX);
    if (client.connect(clientId.c_str())) {
      Serial.println("connected");
      client.subscribe(topic_led); // 訂閱 LED 控制主題
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
 
  // 修正:DHTesp 的初始化寫法
  dht.setup(DHTPIN, DHTesp::DHT22);
}

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

  // 每 2 秒發送一次溫濕度
  unsigned long now = millis();
  if (now - lastMsg > 2000) {
    lastMsg = now;
   
    // 修正:DHTesp 讀取溫濕度的寫法
    float h = dht.getHumidity();
    float t = dht.getTemperature();

    if (!isnan(h) && !isnan(t)) {
      client.publish(topic_temp, String(t, 1).c_str()); // 格式化為小數點後1位
      client.publish(topic_hum, String(h, 1).c_str());
     
      Serial.print("Temp: "); Serial.print(t, 1);
      Serial.print("°C | Hum: "); Serial.print(h, 1); Serial.println("%");
    } else {
      Serial.println("Failed to read from DHT sensor!");
    }
  }
}


沒有留言:

張貼留言

IoT 智能控制面板 (Python Tkinter)

  IoT 智能控制面板  (Python Tkinter) customtkinter 為了在 tkinter 中達到如同網頁般的 圓角 與 平滑陰影 視覺效果,最頂級且優雅的作法是使用 CustomTkinter 庫。 安裝 Python MQTT 套件  paho-mq...