2025年11月27日 星期四

Python 人機介面 設定Wifi ,MQTT 參數 控制 自訂的GPIO ON,OFF,FLASH,TIMER(20S)

Python 人機介面 設定Wifi ,MQTT 參數 控制 自訂的GPIO ON,OFF,FLASH,TIMER(20S)











  • 當 ESP32 卡住時 (例如當前的 DNS 錯誤,或 Wi-Fi 連線失敗):

  • 使用者操作:

    • 將 ESP32 斷電。

    • 將 GPIO34  = GND  (或您定義的 SAFE_MODE_PIN 按鈕)。

    • 再重新插電啟動 設備會進入 訂定模式 alex9ufo ,alex9981

  • ESP32 動作:

    • check_safe_mode() 偵測到按鈕按下。

    • 執行 clear_config() 清除 NVS 中錯誤的配置。

    • 設備使用硬編碼的預設配置(SSID: alex9ufo, Broker: broker.mqtt-dashboard.com)啟動。

  • 恢復連線:

    • 設備現在會連線到預設的 Wi-Fi 和 MQTT Broker (如果預設的 Wi-Fi 是可用的)。

    • 設備重新訂閱 TOPIC_CONFIG (alex9ufo/rfid/config)。

  • 重新配置:

    • 在 Python GUI 中輸入正確的配置 (例如,將 Broker 從錯誤的 broker.mqttgo.io 改回正確的 broker.mqtt-dashboard.com)。

    • 點擊 "發送配置並儲存" 按鈕。

  • 設備恢復:設備收到正確的配置後,會儲存 NVS 並重新啟動,從而恢復正常操作。



  • #include <Arduino.h>
    #include <WiFi.h>
    #include <PubSubClient.h>
    #include <Preferences.h>
    #include <ArduinoJson.h>

    // 專門用於接收配置的主題,必須硬編碼
    const char* TOPIC_CONFIG = "alex9ufo/rfid/config";
    const int MQTT_PORT = 1883;

    // --- 安全模式設定 ---
    // 使用 GPIO 34 (BOOT 按鈕) 作為安全模式啟動按鈕
    const int SAFE_MODE_PIN = 34;
    const int SAFE_MODE_TRIGGER_STATE = LOW;

    // --- 全域變數 ---
    WiFiClient espClient;
    PubSubClient client(espClient);
    Preferences preferences;

    // 配置結構體
    struct DeviceConfig {
        int LED_PIN;
        char ssid[32];
        char password[64];
        char broker[64];
        char controlTopic[64];
        char statusTopic[64];
    } config;

    // LED 狀態變數
    enum LedMode { OFF, ON, FLASH, TIMER_ACTIVE };
    LedMode currentMode = OFF;
    unsigned long previousMillis = 0;
    const long flashInterval = 500;
    unsigned long timerStartTime = 0;
    const unsigned long timerDuration = 20000; // 20 秒

    // --- 函式宣告 ---
    void setup_default_config();
    void load_config();
    void save_config();
    void clear_config();
    void check_safe_mode();
    void setup_wifi();
    void callback(char* topic, byte* payload, unsigned int length);
    void reconnect();
    void publishLedStatus(const char* status);
    void handleTimer();
    void handleConfig(byte* payload, unsigned int length);

    void setup() {
        Serial.begin(115200);
       
        // !!! 新增:檢查是否進入安全模式 (按住 BOOT 鍵啟動) !!!
        check_safe_mode();
       
        // 1. 載入或設定配置
        setup_default_config();
        load_config();          
       
        // 2. 設置 LED 腳位 (根據配置)
        pinMode(config.LED_PIN, OUTPUT);
        digitalWrite(config.LED_PIN, LOW);
       
        Serial.print("當前配置 LED PIN: "); Serial.println(config.LED_PIN);
        Serial.print("當前配置 SSID: "); Serial.println(config.ssid);
        Serial.print("當前配置 Broker: "); Serial.println(config.broker);

        // 3. 連接 Wi-Fi
        setup_wifi();
       
        // 4. 設置 MQTT
        client.setServer(config.broker, MQTT_PORT);
        client.setCallback(callback);
    }

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

        // 處理閃爍邏輯 (FLASH)
        if (currentMode == FLASH) {
            unsigned long currentMillis = millis();
            if (currentMillis - previousMillis >= flashInterval) {
                previousMillis = currentMillis;
                int ledState = digitalRead(config.LED_PIN);
                digitalWrite(config.LED_PIN, !ledState);
            }
        }
       
        // 處理計時器邏輯 (TIMER)
        handleTimer();
    }

    // --- 配置和安全模式處理函式 ---

    /**
     * @brief 檢查按鈕是否按下,以進入安全模式並清除NVS
     */
    void check_safe_mode() {
        pinMode(SAFE_MODE_PIN, INPUT_PULLUP);
        delay(50);

        if (digitalRead(SAFE_MODE_PIN) == SAFE_MODE_TRIGGER_STATE) {
            Serial.println("\n*** 偵測到 SAFE MODE 按鈕按下 (GPIO 34) ***");
            Serial.println("*** 將清除 NVS 配置並使用預設值啟動 ***");
            delay(1000);
            clear_config();
        }
    }

    /**
     * @brief 清除整個 NVS 命名空間的配置
     */
    void clear_config() {
        preferences.begin("device-cfg", false);
        preferences.clear();
        preferences.end();
        Serial.println("NVS 配置已清除!設備將使用預設 Wi-Fi/MQTT 連線。");
    }

    /**
     * @brief 設定硬編碼預設配置
     */
    void setup_default_config() {
        config.LED_PIN = 2; // ESP32 預設板載 LED
        strcpy(config.ssid, "alex9ufo");
        strcpy(config.password, "alex9981");
        strcpy(config.broker, "broker.mqtt-dashboard.com");
        strcpy(config.controlTopic, "alex9ufo/rfid/led");
        strcpy(config.statusTopic, "alex9ufo/rfid/ledStatus");
    }

    /**
     * @brief 從 NVS 載入配置 (已修正 isKey 錯誤)
     */
    void load_config() {
        preferences.begin("device-cfg", true); // 只讀模式
       
        if (preferences.isKey("ssid")) {
            Serial.println("從 NVS 載入配置...");
            config.LED_PIN = preferences.getInt("led_pin", config.LED_PIN);
            preferences.getString("ssid", config.ssid, sizeof(config.ssid));
            preferences.getString("password", config.password, sizeof(config.password));
            preferences.getString("broker", config.broker, sizeof(config.broker));
            preferences.getString("control", config.controlTopic, sizeof(config.controlTopic));
            preferences.getString("status", config.statusTopic, sizeof(config.statusTopic));
        } else {
            Serial.println("NVS 中未找到配置,將使用預設值。");
        }
        preferences.end();
    }

    /**
     * @brief 儲存當前配置到 NVS
     */
    void save_config() {
        Serial.println("正在儲存配置到 NVS...");
        preferences.begin("device-cfg", false); // 讀寫模式
        preferences.putInt("led_pin", config.LED_PIN);
        preferences.putString("ssid", config.ssid);
        preferences.putString("password", config.password);
        preferences.putString("broker", config.broker);
        preferences.putString("control", config.controlTopic);
        preferences.putString("status", config.statusTopic);
        preferences.end();
        Serial.println("配置儲存完成。");
    }

    // --- 通訊和控制函式 ---

    void setup_wifi() {
        Serial.print("嘗試連接到 SSID: ");
        Serial.println(config.ssid);
        WiFi.begin(config.ssid, config.password);

        int attempts = 0;
        while (WiFi.status() != WL_CONNECTED && attempts < 20) {
            delay(1000);
            Serial.print(".");
            attempts++;
        }

        if (WiFi.status() == WL_CONNECTED) {
            Serial.println("\nWiFi 連線成功");
            Serial.print("IP: "); Serial.println(WiFi.localIP());
        } else {
            Serial.println("\nWiFi 連線失敗 (可能 SSID/Password 錯誤)");
        }
    }

    void reconnect() {
        while (!client.connected()) {
            Serial.print("嘗試 MQTT 連線到 ");
            Serial.print(config.broker);
            Serial.print("...");

            String clientId = "ESP32_";
            clientId += String(random(0xffff), HEX);
           
            if (client.connect(clientId.c_str())) {
                Serial.println("成功!");
                // 訂閱控制主題和配置主題
                client.subscribe(config.controlTopic);
                client.subscribe(TOPIC_CONFIG);

                publishLedStatus("READY");
            } else {
                Serial.print("失敗, rc=");
                Serial.print(client.state());
                Serial.println(" 5 秒後重試 (可能 Broker 地址/DNS 錯誤)");
                delay(5000);
            }
        }
    }

    void callback(char* topic, byte* payload, unsigned int length) {
       
        // 1. 檢查是否為配置主題
        if (strcmp(topic, TOPIC_CONFIG) == 0) {
            handleConfig(payload, length);
            return;
        }

        // 2. LED 控制邏輯
        if (strcmp(topic, config.controlTopic) == 0) {
            String message;
            for (int i = 0; i < length; i++) {
                message += (char)payload[i];
            }

            if (message == "on") {
                currentMode = ON;
                digitalWrite(config.LED_PIN, HIGH);
                publishLedStatus("ON");
            } else if (message == "off") {
                currentMode = OFF;
                digitalWrite(config.LED_PIN, LOW);
                publishLedStatus("OFF");
            } else if (message == "flash") {
                currentMode = FLASH;
                publishLedStatus("FLASH");
            } else if (message == "timer_20") {
                currentMode = TIMER_ACTIVE;
                timerStartTime = millis();
                digitalWrite(config.LED_PIN, HIGH);
                publishLedStatus("TIMER_20S");
            }
        }
    }

    void handleConfig(byte* payload, unsigned int length) {
        StaticJsonDocument<1024> doc;
        DeserializationError error = deserializeJson(doc, payload, length);

        if (error) {
            Serial.print(F("deserializeJson() 失敗: "));
            Serial.println(error.f_str());
            return;
        }

        // 檢查並更新結構體
        if (doc.containsKey("LED_PIN")) {
            config.LED_PIN = doc["LED_PIN"].as<int>();
        }
        if (doc.containsKey("ssid")) {
            strlcpy(config.ssid, doc["ssid"] | "", sizeof(config.ssid));
        }
        if (doc.containsKey("password")) {
            strlcpy(config.password, doc["password"] | "", sizeof(config.password));
        }
        if (doc.containsKey("broker")) {
            strlcpy(config.broker, doc["broker"] | "", sizeof(config.broker));
        }
        if (doc.containsKey("control_topic")) {
            strlcpy(config.controlTopic, doc["control_topic"] | "", sizeof(config.controlTopic));
        }
        if (doc.containsKey("status_topic")) {
            strlcpy(config.statusTopic, doc["status_topic"] | "", sizeof(config.statusTopic));
        }

        // 儲存新配置
        save_config();

        // 提示並重新啟動以應用新配置
        Serial.println("新配置已儲存。ESP32 將在 3 秒後重新啟動...");
        delay(3000);
        ESP.restart();
    }


    void publishLedStatus(const char* status) {
        if (client.connected()) {
            client.publish(config.statusTopic, status);
        }
    }

    void handleTimer() {
        if (currentMode == TIMER_ACTIVE) {
            if (millis() - timerStartTime >= timerDuration) {
                currentMode = OFF;
                digitalWrite(config.LED_PIN, LOW);
                publishLedStatus("OFF_TIMER_DONE");
            }
        }
    }


    Python TKinter程式

    import tkinter as tk

    from tkinter import ttk

    from tkinter import messagebox

    import paho.mqtt.client as mqtt

    import time

    import json 

    import threading


    # --- MQTT 設定 (用於程式自身的連線) ---

    MQTT_BROKER_DEFAULT = "broker.mqtt-dashboard.com"

    MQTT_PORT = 1883

    # 用於傳送配置的主題,程式本身和ESP32都需要硬編碼此主題

    TOPIC_CONFIG = "alex9ufo/rfid/config" 

    TOPIC_LED_CONTROL_DEFAULT = "alex9ufo/rfid/led"

    TOPIC_LED_STATUS_DEFAULT = "alex9ufo/rfid/ledStatus"



    class LedControllerApp:

        def __init__(self, master):

            self.master = master

            master.title("MQTT LED 控制器與設定 (NVS)")


            self.led_status_var = tk.StringVar(value="狀態: 離線")

            self.timer_label_var = tk.StringVar(value="計時器: 閒置")

            self.timer_end_time = 0


            # --- 配置輸入變數 (必須初始化) ---

            self.led_pin_var = tk.StringVar(value="2")

            self.ssid_var = tk.StringVar(value="alex9ufo")

            self.password_var = tk.StringVar(value="alex9981")

            self.broker_var = tk.StringVar(value=MQTT_BROKER_DEFAULT)

            self.control_topic_var = tk.StringVar(value=TOPIC_LED_CONTROL_DEFAULT)

            self.status_topic_var = tk.StringVar(value=TOPIC_LED_STATUS_DEFAULT)


            # --- 設定 MQTT 客戶端 ---

            self.client = mqtt.Client()

            self.client.on_connect = self.on_connect

            self.client.on_message = self.on_message

            self.connect_mqtt()


            # --- 建立 GUI ---

            self.create_widgets()

            

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



        def connect_mqtt(self):

            try:

                print(f"嘗試連線到 MQTT Broker: {MQTT_BROKER_DEFAULT}")

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

                self.client.loop_start() 

            except Exception as e:

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

                self.master.after(0, self.led_status_var.set, "狀態: 連線失敗")



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

            """在 MQTT 客戶端執行緒中執行"""

            if rc == 0:

                print("MQTT 連線成功!")

                # 訂閱狀態主題

                client.subscribe(self.status_topic_var.get())

                client.subscribe(TOPIC_CONFIG) 

                

                # 使用 after 轉移到主執行緒更新 UI

                self.master.after(0, self.update_status_on_connect, rc) 

            else:

                print(f"連線失敗,返回碼 {rc}")

                self.master.after(0, self.update_status_on_connect, rc) 

                

        # 在主執行緒中執行 UI 更新

        def update_status_on_connect(self, rc):

            if rc == 0:

                self.led_status_var.set("狀態: 已連線, LED未知")

            else:

                self.led_status_var.set(f"狀態: 連線失敗 {rc}")



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

            """在 MQTT 客戶端執行緒中執行"""

            topic = msg.topic

            payload = msg.payload.decode()


            if topic == self.status_topic_var.get():

                self.master.after(0, self.update_status_on_message, payload)

            

            if topic == TOPIC_CONFIG:

                 print("收到配置訊息,通常表示設備準備好接收新配置。")



        # 在主執行緒中執行 UI 更新

        def update_status_on_message(self, payload):

            self.led_status_var.set(f"狀態: LED {payload.upper()}")



        def send_command(self, command):

            """發送 LED 控制指令到 MQTT"""

            if self.client.is_connected():

                control_topic = self.control_topic_var.get()

                self.client.publish(control_topic, command, qos=1)

                print(f"發送指令到 {control_topic}: {command}")

                

                if command == "timer_20":

                    self.timer_end_time = time.time() + 20

                    self.timer_label_var.set("計時器: 20秒倒數中...")

                else:

                     self.timer_end_time = 0 

                     self.timer_label_var.set("計時器: 閒置")

            else:

                messagebox.showerror("錯誤", "MQTT 未連線,請檢查網路連線。")



        def send_config(self):

            """將所有配置參數打包成 JSON 並透過 MQTT 發送"""

            try:

                config_data = {

                    "LED_PIN": int(self.led_pin_var.get()),

                    "ssid": self.ssid_var.get(),

                    "password": self.password_var.get(),

                    "broker": self.broker_var.get(),

                    "control_topic": self.control_topic_var.get(),

                    "status_topic": self.status_topic_var.get()

                }

            except ValueError:

                messagebox.showerror("錯誤", "LED PIN 必須是有效的整數。")

                return


            config_json = json.dumps(config_data)

            

            if self.client.is_connected():

                self.client.publish(TOPIC_CONFIG, config_json, qos=1)

                print(f"發送配置數據到 {TOPIC_CONFIG}: {config_json}")

                messagebox.showinfo("配置發送", "配置已發送給 ESP32。設備將儲存並重新啟動以應用新設定。\n\n**提示: 如果配置錯誤,請在啟動時按住 BOOT 按鈕進入安全模式。**")

            else:

                messagebox.showerror("錯誤", "MQTT 未連線,無法發送配置。")


        def update_timer(self):

            """更新計時器顯示 (在主執行緒中執行)"""

            if self.timer_end_time > 0:

                remaining = int(self.timer_end_time - time.time())

                if remaining > 0:

                    self.timer_label_var.set(f"計時器: 剩餘 {remaining} 秒")

                else:

                    self.timer_end_time = 0

                    self.timer_label_var.set("計時器: 結束 (LED OFF)")

            

            self.master.after(1000, self.update_timer)



        def create_widgets(self):

            # --- 狀態顯示區 ---

            status_frame = ttk.LabelFrame(self.master, text="當前連線狀態")

            status_frame.pack(pady=10, padx=10, fill="x")

            ttk.Label(status_frame, textvariable=self.led_status_var, font=('Arial', 12, 'bold')).pack(pady=5)

            ttk.Label(status_frame, textvariable=self.timer_label_var, font=('Arial', 10)).pack(pady=5)


            # --- LED 控制區 ---

            control_frame = ttk.LabelFrame(self.master, text="LED 控制")

            control_frame.pack(pady=10, padx=10, fill="x")

            

            button_frame = ttk.Frame(control_frame)

            button_frame.pack(pady=10, padx=5)


            ttk.Button(button_frame, text="LED ON", command=lambda: self.send_command("on")).pack(side=tk.LEFT, padx=5)

            ttk.Button(button_frame, text="LED OFF", command=lambda: self.send_command("off")).pack(side=tk.LEFT, padx=5)

            ttk.Button(button_frame, text="LED FLASH", command=lambda: self.send_command("flash")).pack(side=tk.LEFT, padx=5)

            ttk.Button(button_frame, text="TIMER (20s)", command=lambda: self.send_command("timer_20")).pack(side=tk.LEFT, padx=5)


            # --- 裝置配置區 ---

            config_frame = ttk.LabelFrame(self.master, text="ESP32 裝置配置 (發送並儲存到 NVS)")

            config_frame.pack(pady=10, padx=10, fill="x")

            

            fields = [

                ("LED PIN (GPIO)", self.led_pin_var),

                ("Wi-Fi SSID", self.ssid_var),

                ("Wi-Fi Password", self.password_var),

                ("MQTT Broker", self.broker_var),

                ("Control Topic", self.control_topic_var),

                ("Status Topic", self.status_topic_var)

            ]


            for i, (label_text, variable) in enumerate(fields):

                row = ttk.Frame(config_frame)

                row.pack(side=tk.TOP, fill=tk.X, padx=5, pady=2)

                ttk.Label(row, text=label_text + ":", width=15, anchor='w').pack(side=tk.LEFT)

                ttk.Entry(row, textvariable=variable).pack(side=tk.LEFT, expand=tk.YES, fill=tk.X) 

                

            ttk.Button(config_frame, text="🚀 發送配置並儲存", command=self.send_config, style='Accent.TButton').pack(pady=10)



        def on_closing(self):

            """關閉應用程式時清理資源"""

            print("關閉應用程式...")

            self.client.loop_stop()

            self.client.disconnect()

            self.master.destroy()


    if __name__ == "__main__":

        if not threading.main_thread().is_alive():

            print("警告: Tkinter 應在主執行緒中運行。")

            

        root = tk.Tk()

        style = ttk.Style()

        style.theme_use('clam')

        style.configure('Accent.TButton', font=('Arial', 10, 'bold'), foreground='black', background='#4CAF50')

        

        app = LedControllerApp(root)

        root.protocol("WM_DELETE_WINDOW", app.on_closing)

        root.mainloop()



    GPIO34 =3.3V 使用新的訂定值



    rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)

    configsip: 0, SPIWP:0xee

    clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00

    mode:DIO, clock div:2

    load:0x3fff0030,len:1184

    load:0x40078000,len:13232

    load:0x40080400,len:3028

    entry 0x400805e4

    從 NVS 載入配置...

    當前配置 LED PIN: 2

    當前配置 SSID: ASUS_30

    當前配置 Broker: broker.mqtt-dashboard.com

    嘗試連接到 SSID: ASUS_30

    .

    WiFi 連線成功

    IP: 192.168.50.24

    嘗試 MQTT 連線到 broker.mqtt-dashboard.com...成功


    GPIO34 =GND  內定值的設定

    Leaving...

    Hard resetting via RTS pin...

    --- Terminal on COM6 | 115200 8-N-1

    --- Available filters and text transformations: colorize, debug, default, direct, esp32_exception_decoder, hexlify, log2file, nocontrol, printable, send_on_enter, time

    --- More details at https://bit.ly/pio-monitor-filters

    --- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H


    *** 偵測到 SAFE MODE 按鈕按下 (GPIO 34) ***

    *** 將清除 NVS 配置並使用預設值啟動 ***

    NVS 配置已清除!設備將使用預設 Wi-Fi/MQTT 連線。

    NVS 中未找到配置,將使用預設值。

    當前配置 LED PIN: 2

    當前配置 SSID: alex9ufo

    當前配置 Broker: broker.mqtt-dashboard.com

    嘗試連接到 SSID: alex9ufo

    ..

    WiFi 連線成功

    IP: 10.143.40.112

    嘗試 MQTT 連線到 broker.mqtt-dashboard.com...成功!






    沒有留言:

    張貼留言

    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...