WOKWI ESP-IDF MQTT 作業一 (更新)
主要的修改點包括:
MQTT 設定欄位: 確保 Broker Host, Port, LED Topic 和 LED Status Topic 正確映射到 GUI。
狀態顯示: 實現
Status: Disconnected顯示。LED Control 按鈕: 創建 ON、OFF、FLASH 和 TIMER 按鈕,並定義它們的 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()




沒有留言:
張貼留言