2025年12月5日 星期五

WOKWI ESP-IDF MQTT 作業一 (更新)

 WOKWI ESP-IDF MQTT 作業一 (更新) 






主要的修改點包括:

  1. MQTT 設定欄位: 確保 Broker Host, Port, LED Topic 和 LED Status Topic 正確映射到 GUI。

  2. 狀態顯示: 實現 Status: Disconnected 顯示。

  3. LED Control 按鈕: 創建 ONOFFFLASHTIMER 按鈕,並定義它們的 MQTT 發佈(Publish)行為。


WOKWI ESP-32  ESP-IDF程式
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "freertos/timers.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "esp_wifi.h"
#include "lwip/sockets.h"
#include "lwip/pbuf.h"
#include "lwip/netif.h"
#include "driver/gpio.h"
#include "mqtt_client.h"

// **********************************************
// 1. 配置參數
// **********************************************
#define WIFI_SSID           "Wokwi-GUEST"
#define WIFI_PASSWORD       ""
#define MQTT_BROKER_URI     "mqtt://broker.hivemq.com:1883"
#define MQTT_LED_TOPIC      "esp32/wokwi/led/control"

// *** 新增: LED 狀態回傳主題 ***
#define MQTT_STATUS_TOPIC   "esp32/wokwi/led/status"

#define LED_GPIO_PIN        GPIO_NUM_2
#define TIMER_DURATION_SEC  20

static const char *TAG = "MQTT_LED_CTRL";

// **********************************************
// 2. LED 狀態及模式管理
// **********************************************

typedef enum {
    MODE_OFF = 0,
    MODE_ON,
    MODE_TIMER,
    MODE_FLASH
} led_mode_t;

static led_mode_t g_led_mode = MODE_OFF;
static TimerHandle_t s_timer_handle = NULL;
static TaskHandle_t s_flash_task_handle = NULL;
static esp_mqtt_client_handle_t s_mqtt_client = NULL; // *** 新增: MQTT 客戶端句柄 ***

// **********************************************
// 3. 處理函數: MQTT 狀態發布
// **********************************************

/**
 * @brief 根據當前的 g_led_mode 發布狀態到 MQTT
 */
static void publish_led_status(void) {
    if (s_mqtt_client == NULL) {
        ESP_LOGE(TAG, "MQTT 客戶端未初始化或連接。");
        return;
    }

    const char *status_msg = "";
    // 根據 g_led_mode 獲取對應的狀態字串
    switch (g_led_mode) {
        case MODE_OFF:
            status_msg = "OFF";
            break;
        case MODE_ON:
            status_msg = "ON";
            break;
        case MODE_TIMER:
            status_msg = "TIMER";
            break;
        case MODE_FLASH:
            status_msg = "FLASH";
            break;
        default:
            status_msg = "UNKNOWN";
            break;
    }

    int msg_id = esp_mqtt_client_publish(s_mqtt_client, MQTT_STATUS_TOPIC, status_msg, 0, 0, 0);
    if (msg_id > 0) {
        ESP_LOGI(TAG, "狀態回傳成功: topic=%s, data=%s", MQTT_STATUS_TOPIC, status_msg);
    } else {
        ESP_LOGE(TAG, "狀態回傳失敗!");
    }
}

// **********************************************
// 4. 處理函數: LED 操作 (與之前相同)
// **********************************************

static void led_set_level(int level) {
    gpio_set_level(LED_GPIO_PIN, level);
}

static void start_stop_flash_task(bool start_flash);


/**
 * @brief FreeRTOS 軟體定時器的回調函數 (20 秒後執行)
 */
static void timer_callback(TimerHandle_t xTimer) {
    ESP_LOGI(TAG, "定時器 (20 秒) 到期,自動關閉 LED。");
   
    // *** 狀態改變: 從 TIMER -> OFF ***
    g_led_mode = MODE_OFF;
    led_set_level(0); // 關閉 LED

    // *** 新增: 發布新的狀態 ***
    publish_led_status();
}

// **********************************************
// 5. FreeRTOS 任務: 閃爍 (FLASH) (微調邏輯以確保狀態回傳)
// **********************************************

static void flash_task(void *pvParameter) {
    ESP_LOGI(TAG, "FLASH 任務已啟動。");
    // 雖然模式是 FLASH,但閃爍本身不會改變 g_led_mode
    while (g_led_mode == MODE_FLASH) {
        led_set_level(1); // 開啟
        vTaskDelay(pdMS_TO_TICKS(500));
        if (g_led_mode != MODE_FLASH) break; // 閃爍過程中檢查是否被外部命令停止
       
        led_set_level(0); // 關閉
        vTaskDelay(pdMS_TO_TICKS(500));
    }
    ESP_LOGI(TAG, "FLASH 任務已停止。");

    // 任務結束時,如果 LED 剛好是關閉狀態 (level 0),則不需要額外操作
    // 如果外部已經切換到 ON/OFF/TIMER,則 led_control_logic 會處理狀態

    s_flash_task_handle = NULL;
    vTaskDelete(NULL);
}

static void start_stop_flash_task(bool start_flash) {
    if (start_flash) {
        if (s_flash_task_handle == NULL) {
            xTaskCreate(flash_task, "flash_task", 2048, NULL, 5, &s_flash_task_handle);
        }
    } else {
        if (s_flash_task_handle != NULL) {
            // 任務將自行退出,無需立即刪除
        }
    }
}

/**
 * @brief 核心控制邏輯:根據命令字串切換 LED 模式
 */
static void led_control_logic(const char *cmd_str) {
    // 停止所有當前活動 (定時器或閃爍任務)
    xTimerStop(s_timer_handle, 0);
    if (s_flash_task_handle != NULL) {
        // 讓 Flash 任務自行停止
    }

    led_mode_t old_mode = g_led_mode; // 記錄舊模式

    if (strcasecmp(cmd_str, "ON") == 0 || strncmp(cmd_str, "1", 1) == 0) {
        g_led_mode = MODE_ON;
        led_set_level(1);
        ESP_LOGI(TAG, "LED 狀態: ON (模式: ON)");

    } else if (strcasecmp(cmd_str, "OFF") == 0 || strncmp(cmd_str, "0", 1) == 0) {
        g_led_mode = MODE_OFF;
        led_set_level(0);
        ESP_LOGI(TAG, "LED 狀態: OFF (模式: OFF)");
       
    } else if (strcasecmp(cmd_str, "TIMER") == 0) {
        g_led_mode = MODE_TIMER;
        led_set_level(1);
        xTimerChangePeriod(s_timer_handle, pdMS_TO_TICKS(TIMER_DURATION_SEC * 1000), 0);
        xTimerStart(s_timer_handle, 0);
        ESP_LOGI(TAG, "LED 狀態: ON (模式: TIMER,將在 %d 秒後關閉)", TIMER_DURATION_SEC);

    } else if (strcasecmp(cmd_str, "FLASH") == 0) {
        g_led_mode = MODE_FLASH;
        start_stop_flash_task(true);
        ESP_LOGI(TAG, "LED 狀態: 閃爍中 (模式: FLASH)");

    } else {
        ESP_LOGW(TAG, "無法識別的控制命令: %s", cmd_str);
        return;
    }

    // 處理完模式切換後,檢查是否需要停止 FLASH 任務
    if (g_led_mode != MODE_FLASH) {
        start_stop_flash_task(false);
    }

    // *** 新增: 只有模式真正改變時才回傳狀態 ***
    if (old_mode != g_led_mode) {
        publish_led_status();
    }
}


// **********************************************
// 6. MQTT 事件處理 (儲存 client 句柄並在連接成功時回傳初始狀態)
// **********************************************

static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) {
    ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%" PRIi32, base, event_id);
    esp_mqtt_event_handle_t event = event_data;
    esp_mqtt_client_handle_t client = event->client;

    switch ((esp_mqtt_event_id_t)event_id) {
    case MQTT_EVENT_CONNECTED:
        ESP_LOGI(TAG, "MQTT 連線成功!");
        s_mqtt_client = client; // 儲存句柄
        esp_mqtt_client_subscribe(client, MQTT_LED_TOPIC, 0);
       
        // *** 連接成功後,立即發布當前 LED 狀態 ***
        publish_led_status();
        break;

    case MQTT_EVENT_DISCONNECTED:
        ESP_LOGI(TAG, "MQTT 斷線。");
        s_mqtt_client = NULL; // 清除句柄
        break;

    case MQTT_EVENT_SUBSCRIBED:
        ESP_LOGI(TAG, "訂閱主題成功: topic=%s", MQTT_LED_TOPIC);
        break;

    case MQTT_EVENT_DATA:
        // 處理接收到的命令 (控制 LED)
        if (event->topic_len == strlen(MQTT_LED_TOPIC) && strncmp(event->topic, MQTT_LED_TOPIC, event->topic_len) == 0) {
            if (event->data_len > 0) {
                char data_str[event->data_len + 1];
                strncpy(data_str, event->data, event->data_len);
                data_str[event->data_len] = '\0';
                led_control_logic(data_str);
            }
        }
        break;

    case MQTT_EVENT_ERROR:
        ESP_LOGI(TAG, "MQTT 錯誤");
        if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
             int transport_errno = event->error_handle->esp_transport_sock_errno;
             ESP_LOGI(TAG, "Last transport errno: %d (%s)",
                   transport_errno,
                   strerror(transport_errno));
        }
        break;
   
    default:
        break;
    }
}

// **********************************************
// 7. Wi-Fi, LED, MQTT 初始化 (與之前相同)
// **********************************************
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
     if (event_base == WIFI_EVENT) {
        if (event_id == WIFI_EVENT_STA_START) {
            esp_wifi_connect();
        } else if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
            ESP_LOGI(TAG, "Wi-Fi 連接失敗,嘗試重新連線...");
            esp_wifi_connect();
        }
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
        ESP_LOGI(TAG, "已獲得 IP: " IPSTR, IP2STR(&event->ip_info.ip));
    }
}

static void wifi_init_sta(void) {
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, NULL));

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = WIFI_SSID,
            .password = WIFI_PASSWORD,
            .threshold.authmode = WIFI_AUTH_WPA2_PSK,
            .sae_pwe_h2e = WPA3_SAE_PWE_BOTH,
        },
    };
   
    if (strlen(WIFI_PASSWORD) == 0) {
        wifi_config.sta.threshold.authmode = WIFI_AUTH_OPEN;
    }
   
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    ESP_LOGI(TAG, "Wi-Fi 初始化完成,正在等待連接...");
}

static void led_init(void) {
    gpio_reset_pin(LED_GPIO_PIN);
    gpio_set_direction(LED_GPIO_PIN, GPIO_MODE_OUTPUT);
    gpio_set_level(LED_GPIO_PIN, 0);
    ESP_LOGI(TAG, "LED (GPIO %d) 初始化完成。", LED_GPIO_PIN);
}

static void mqtt_app_start(void) {
    esp_mqtt_client_config_t mqtt_cfg = {
        .broker.address.uri = MQTT_BROKER_URI,
    };

    esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);
   
    if (client == NULL) {
        ESP_LOGE(TAG, "無法初始化 MQTT 客戶端!");
        return;
    }
   
    esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
    esp_mqtt_client_start(client);
    ESP_LOGI(TAG, "MQTT 客戶端啟動。");
}


// **********************************************
// 8. 主應用程式入口點
// **********************************************

int __attribute__((weak)) lwip_hook_ip6_input(struct pbuf *p, struct netif *inp) { return 0; }

void _Z5setupv(void) __attribute__((weak, alias("setup")));
void setup(void) {
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);
   
    led_init();

    s_timer_handle = xTimerCreate(
                        "LED_Timer",
                        pdMS_TO_TICKS(1000),
                        pdFALSE,            
                        (void *)0,
                        timer_callback);
    if (s_timer_handle == NULL) {
        ESP_LOGE(TAG, "無法創建 FreeRTOS 定時器!");
    }

    wifi_init_sta();

    vTaskDelay(pdMS_TO_TICKS(5000));
    mqtt_app_start();
}

void _Z4loopv(void) __attribute__((weak, alias("loop")));
void loop(void) {
    vTaskDelay(pdMS_TO_TICKS(10));
}


PYthon + Tkinter  程式
import tkinter as tk
from tkinter import messagebox
import paho.mqtt.client as mqtt

# ******************************************************************************
# 1. 配置參數
# ******************************************************************************
DEFAULT_BROKER_HOST = "broker.hivemq.com"
DEFAULT_BROKER_PORT = 1883
TOPIC_CONTROL = "esp32/wokwi/led/control"
TOPIC_STATUS = "esp32/wokwi/led/status"

# ******************************************************************************
# 2. MQTT 客戶端回調函數
# ******************************************************************************

def on_connect(client, userdata, flags, rc):
    """當客戶端收到來自代理的回應時調用。"""
    if rc == 0:
        print("Connected to MQTT Broker!")
        app.set_status("Connected", "green")
        # 訂閱 LED Status 主題
        client.subscribe(app.topic_status_entry.get())
    else:
        print(f"Failed to connect, return code {rc}")
        app.set_status(f"Connection Failed (RC: {rc})", "red")

def on_disconnect(client, userdata, rc):
    """當客戶端與代理斷開連接時調用。"""
    print("Disconnected from MQTT Broker.")
    app.set_status("Disconnected", "blue")

def on_message(client, userdata, msg):
    """當收到訂閱主題的訊息時調用。"""
    topic = msg.topic
    payload = msg.payload.decode()
    print(f"Received message: Topic='{topic}', Payload='{payload}'")
    
    # 假設這是 LED 狀態更新
    if topic == app.topic_status_entry.get():
        app.update_led_status_label(payload)


# ******************************************************************************
# 3. GUI 應用程式類別
# ******************************************************************************

class MqttLedMonitor(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("MQTT LED and Sensor Monitor")
        self.geometry("450x450") # 調整窗口大小以容納所有元件

        # MQTT 客戶端實例化
        self.client = mqtt.Client()
        self.client.on_connect = on_connect
        self.client.on_disconnect = on_disconnect
        self.client.on_message = on_message
        
        # 設置 GUI 介面
        self._create_widgets()

    def _create_widgets(self):
        # 狀態標籤
        self.status_label = tk.Label(self, text="Status: Disconnected", fg="blue", font=("Arial", 14, "bold"))
        self.status_label.pack(pady=10)

        # --- MQTT Settings 框架 ---
        mqtt_frame = tk.LabelFrame(self, text="⚙️ MQTT Settings", padx=10, pady=10)
        mqtt_frame.pack(padx=20, pady=10, fill="x")

        # 欄位字典: {標籤文字: [預設值, 儲存變數]}
        fields = {
            "Broker Host:": [DEFAULT_BROKER_HOST, None],
            "Broker Port:": [DEFAULT_BROKER_PORT, None],
            "LED Topic:": [TOPIC_CONTROL, None],
            "LED status:": [TOPIC_STATUS, None],
        }

        # 創建輸入欄位
        row_num = 0
        self.topic_control_entry = None # 用於保存 LED Topic 的 Entry 實例
        self.topic_status_entry = None  # 用於保存 LED status 的 Entry 實例

        for label_text, (default_value, var_storage) in fields.items():
            tk.Label(mqtt_frame, text=label_text, anchor="w").grid(row=row_num, column=0, padx=5, pady=5, sticky="w")
            
            entry = tk.Entry(mqtt_frame, width=35)
            entry.insert(0, str(default_value))
            entry.grid(row=row_num, column=1, padx=5, pady=5, sticky="ew")

            if label_text == "Broker Host:":
                self.broker_host_entry = entry
            elif label_text == "Broker Port:":
                self.broker_port_entry = entry
            elif label_text == "LED Topic:":
                self.topic_control_entry = entry
            elif label_text == "LED status:":
                self.topic_status_entry = entry

            row_num += 1
        
        # 連接/斷開按鈕
        tk.Button(mqtt_frame, text="Connect", command=self._connect_mqtt).grid(row=row_num, column=0, padx=5, pady=10, sticky="ew")
        tk.Button(mqtt_frame, text="Disconnect", command=self._disconnect_mqtt).grid(row=row_num, column=1, padx=5, pady=10, sticky="ew")

        # --- LED Control 框架 ---
        control_frame = tk.LabelFrame(self, text="💡 LED Control", padx=10, pady=10)
        control_frame.pack(padx=20, pady=10, fill="x")
        
        # LED 模式控制按鈕
        modes = ["ON", "OFF", "FLASH", "TIMER"]
        for i, mode in enumerate(modes):
            tk.Button(control_frame, text=mode, width=10, command=lambda m=mode: self._publish_control(m)).grid(row=0, column=i, padx=5, pady=5, sticky="ew")
        
        # LED 實際狀態顯示
        self.current_led_status_var = tk.StringVar(value="Waiting for status...")
        tk.Label(control_frame, text="Current Status:", anchor="w").grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="w")
        tk.Label(control_frame, textvariable=self.current_led_status_var, fg="red", font=("Arial", 10, "bold")).grid(row=1, column=2, columnspan=2, padx=5, pady=5, sticky="w")


    # **********************************************
    # 4. MQTT 互動方法
    # **********************************************

    def set_status(self, text, color):
        """更新頂部的狀態標籤"""
        self.status_label.config(text=f"Status: {text}", fg=color)

    def update_led_status_label(self, status):
        """更新 LED 實際狀態標籤"""
        self.current_led_status_var.set(status)

    def _connect_mqtt(self):
        """連接到 MQTT Broker"""
        host = self.broker_host_entry.get()
        port = int(self.broker_port_entry.get())

        try:
            self.client.connect(host, port, 60)
            self.client.loop_start() # 啟動背景線程處理網路流量
            self.set_status("Connecting...", "orange")
        except Exception as e:
            messagebox.showerror("Connection Error", f"Failed to connect to broker: {e}")
            self.set_status("Disconnected", "blue")

    def _disconnect_mqtt(self):
        """斷開與 MQTT Broker 的連接"""
        if self.client.is_connected():
            self.client.disconnect()
            self.client.loop_stop() # 停止網路線程
        self.set_status("Disconnected", "blue")

    def _publish_control(self, command):
        """發佈 LED 控制命令"""
        topic = self.topic_control_entry.get()
        
        if not self.client.is_connected():
            messagebox.showwarning("Warning", "MQTT client is not connected. Please connect first.")
            return

        try:
            # 命令(Payload)與按鈕文字一致 (ON/OFF/FLASH/TIMER)
            result = self.client.publish(topic, command, qos=0) 
            if result.rc == mqtt.MQTT_ERR_SUCCESS:
                print(f"Command '{command}' published successfully to topic '{topic}'.")
            else:
                print(f"Failed to publish command '{command}'.")
        except Exception as e:
            print(f"Publish error: {e}")

# **********************************************
# 5. 應用程式主運行點
# **********************************************

if __name__ == "__main__":
    app = MqttLedMonitor()
    app.mainloop()

WOKWI ESP-IDF MQTT 作業一 (更新)

 WOKWI ESP-IDF MQTT 作業一 (更新)  主要的修改點包括: MQTT 設定欄位: 確保 Broker Host, Port, LED Topic 和 LED Status Topic 正確映射到 GUI。 狀態顯示: 實現 Status: Disconne...