2025年8月4日 星期一

TKinter MQTT , WOKWI ESP32 Opendata政府開放資放平台

TKinter   MQTT ,  WOKWI ESP32 Opendata政府開放資放平台  

利用TKinter 發布AQI的測站  給MQTT  WOKWI ESP取得測站名稱  向Opendata政府開放資放平台https://data.gov.tw/dataset/40507 取得 "測站名稱" "空氣品質指標" "二氧化硫副指標" "一氧化碳副指標" "臭氧副指標" "懸浮微粒副指標" "二氧化氮副指標"  "臭氧8小時副指標" "細懸浮微粒副指標"  發布到MQTT  再由TKinter顯示





<<Python TKinter 程式>>

import tkinter as tk

from tkinter import ttk

import requests

import paho.mqtt.client as mqtt


# --- 設定 ---

API_URL = "https://data.moenv.gov.tw/api/v2/aqx_p_434?api_key=9e565f9a-84dd-4e79-9097-d403cae1ea75&limit=1000&sort=monitordate%20desc&format=JSON"

MQTT_BROKER = "broker.mqttgo.io"

MQTT_PORT = 1883

MQTT_PUBLISH_TOPIC = "alex9ufo/AQI/sitename"

MQTT_SUBSCRIBE_TOPIC = "alex9ufo/AQX/#"


# 用來儲存所有站名的變數

sitenames = []

message_text = None


# 建立主題名稱與中文說明的對應字典

topic_map = {

    "sitename": "測站名稱",

    "AQI": "空氣品質指標",

    "SO2Subindex": "二氧化硫副指標",

    "COSubindex": "一氧化碳副指標",

    "O3Subindex": "臭氧副指標",

    "PM10Subindex": "懸浮微粒副指標",

    "NO2Subindex": "二氧化氮副指標",

    "O38-hourSubindex": "臭氧8小時副指標",

    "PM2.5Subindex": "細懸浮微粒副指標"

}


# --- MQTT 相關函式 ---

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

    """當客戶端連線到 Broker 時觸發"""

    if rc == 0:

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

        client.subscribe(MQTT_SUBSCRIBE_TOPIC)

        print(f"已訂閱主題: {MQTT_SUBSCRIBE_TOPIC}")

    else:

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


def on_message(client, userdata, msg):

    """當收到訊息時觸發"""

    topic = msg.topic

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

    print(f"收到訊息: Topic='{topic}', Payload='{payload}'")

    

    if message_text:

        # 從主題中取出最後一段,並從字典中尋找對應的中文名稱

        english_topic = topic.split('/')[-1]

        chinese_topic = topic_map.get(english_topic, english_topic) # 如果字典中沒有,就使用原始英文名稱

        message_text.insert(tk.END, f"[{chinese_topic}]: {payload}\n")

        message_text.see(tk.END)


def on_publish(client, userdata, mid, reason_code, properties):

    """當訊息發布時觸發"""

    print(f"訊息已發布,mid: {mid}, 原因碼: {reason_code}")


# --- 擷取資料函式 ---

def fetch_sitenames():

    """從 API 擷取所有站名"""

    try:

        response = requests.get(API_URL)

        response.raise_for_status()

        data = response.json()

        

        sitenames_set = set()

        for record in data['records']:

            sitenames_set.add(record['sitename'])

        

        global sitenames

        sitenames = sorted(list(sitenames_set))

        print(f"資料擷取成功,共 {len(sitenames)} 個測站。")

        return True

    except requests.exceptions.RequestException as e:

        print(f"資料擷取失敗:{e}")

        return False


# --- GUI 相關函式 ---

def publish_sitename(sitename):

    """點選按鈕時發布 MQTT 訊息"""

    topic = MQTT_PUBLISH_TOPIC

    message = sitename

    

    if message_text:

        message_text.delete(1.0, tk.END)

    

    result = mqtt_client.publish(topic, message)

    

    if result[0] == mqtt.MQTT_ERR_SUCCESS:

        print(f"成功發布 '{message}' 到 '{topic}'")

    else:

        print("訊息發布失敗")


def create_gui():

    """建立 Tkinter 視窗並配置按鈕及顯示區域"""

    root = tk.Tk()

    root.title("空氣品質站點 MQTT 發布與接收")

    root.geometry("1200x800")


    main_frame = ttk.Frame(root)

    main_frame.pack(fill="both", expand=True, padx=10, pady=10)

    

    # 左側:按鈕區域

    button_frame = ttk.Frame(main_frame)

    button_frame.pack(side="left", fill="both", expand=True, padx=10, pady=10)


    canvas = tk.Canvas(button_frame)

    scrollbar = ttk.Scrollbar(button_frame, orient="vertical", command=canvas.yview)

    scrollable_frame = ttk.Frame(canvas)


    def on_frame_configure(event):

        canvas.configure(scrollregion=canvas.bbox("all"))


    scrollable_frame.bind("<Configure>", on_frame_configure)


    canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")

    canvas.configure(yscrollcommand=scrollbar.set)


    canvas.pack(side="left", fill="both", expand=True)

    scrollbar.pack(side="right", fill="y")

    

    # 建立按鈕

    print(f"正在建立 {len(sitenames)} 個按鈕...")

    

    # 使用 grid 佈局,並設定每行 4 個按鈕

    row_index = 0

    col_index = 0

    buttons_per_row = 4

    

    for sitename in sitenames:

        button = ttk.Button(scrollable_frame, text=sitename, command=lambda s=sitename: publish_sitename(s))

        button.grid(row=row_index, column=col_index, padx=5, pady=5, sticky="nsew")

        

        col_index += 1

        if col_index >= buttons_per_row:

            col_index = 0

            row_index += 1

            

    for i in range(buttons_per_row):

        scrollable_frame.grid_columnconfigure(i, weight=1)


    # 延遲更新,確保所有按鈕都渲染完成

    def _update_scroll_region():

        root.update_idletasks()

        canvas.configure(scrollregion=canvas.bbox("all"))

    

    root.after(100, _update_scroll_region)


    # 右側:訊息顯示區域

    global message_text

    display_frame = ttk.Frame(main_frame)

    display_frame.pack(side="right", fill="both", expand=True, padx=10, pady=10)

    

    ttk.Label(display_frame, text="接收到的 MQTT 訊息:", font=("Helvetica", 12)).pack(pady=5)

    

    # 修改 Text 控件的字體和大小

    message_text = tk.Text(display_frame, wrap="word", height=20, width=60, font=("黑體", 18))

    message_text.pack(fill="both", expand=True)

    

    root.mainloop()


# --- 主程式 ---

if __name__ == "__main__":

    mqtt_client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2)

    mqtt_client.on_connect = on_connect

    mqtt_client.on_message = on_message

    mqtt_client.on_publish = on_publish

    

    print("嘗試連線到 MQTT Broker...")

    try:

        mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60)

        mqtt_client.loop_start()

    except ConnectionRefusedError:

        print("無法連線到 Broker。請檢查網路或 Broker 狀態。")

        exit()


    if fetch_sitenames():

        create_gui()

    else:

        print("程式終止。")

    

    mqtt_client.loop_stop()

    mqtt_client.disconnect()


<<WOKWI程式>>

#include <WiFi.h>
#include <HTTPClient.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

// Wi-Fi 設定
const char* ssid = "Wokwi-GUEST";
const char* password = "";

// MQTT 設定
const char* mqtt_server = "broker.mqttgo.io";
const int mqtt_port = 1883;
const char* mqtt_user = ""; // 如果需要,請填寫
const char* mqtt_pass = ""; // 如果需要,請填寫
const char* mqtt_subscribe_topic = "alex9ufo/AQI/sitename";
const char* mqtt_publish_base_topic = "alex9ufo/AQX/";

// 環保署空氣品質資料 API
const String api_url = "https://data.moenv.gov.tw/api/v2/aqx_p_434?api_key=9e565f9a-84dd-4e79-9097-d403cae1ea75&limit=100&sort=monitordate%20desc&format=JSON";

// 宣告全域變數
WiFiClient espClient;
PubSubClient client(espClient);
String targetSitename = "";

// 鎖定機制,防止兩個核心同時存取 targetSitename
SemaphoreHandle_t xMutex;

// --- 函式宣告 ---
void connectToWiFi();
void reconnectMQTT();
void callback(char* topic, byte* payload, unsigned int length);
void fetchAndPublishData();
void mqttTask(void * parameter);
void httpTask(void * parameter);

// --- 函式定義 ---
void connectToWiFi() {
  Serial.print("Connecting to WiFi...");
  WiFi.begin(ssid, password);
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 20) {
    delay(500);
    Serial.print(".");
    attempts++;
  }
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\nConnected to WiFi!");
  } else {
    Serial.println("\nFailed to connect to WiFi. Restarting in 5s...");
    delay(5000);
    ESP.restart();
  }
}

void reconnectMQTT() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    String clientId = "ESP32_alex9ufo_" + String(random(0xffff), HEX);
    if (client.connect(clientId.c_str(), mqtt_user, mqtt_pass)) {
      Serial.println("connected!");
      client.subscribe(mqtt_subscribe_topic);
      Serial.print("Subscribed to topic: ");
      Serial.println(mqtt_subscribe_topic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" trying again in 5 seconds");
      vTaskDelay(5000 / portTICK_PERIOD_MS);
    }
  }
}

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message received on topic: ");
  Serial.println(topic);
 
  if (xSemaphoreTake(xMutex, (TickType_t)10) == pdTRUE) {
    if (strcmp(topic, mqtt_subscribe_topic) == 0) {
      targetSitename = "";
      for (int i = 0; i < length; i++) {
        targetSitename += (char)payload[i];
      }
      Serial.print("Target sitename set to: ");
      Serial.println(targetSitename);
    }
    xSemaphoreGive(xMutex);
  }
}

void fetchAndPublishData() {
  HTTPClient http;
  http.begin(api_url.c_str());
  int httpCode = http.GET();
 
  if (httpCode == HTTP_CODE_OK) {
    String payload = http.getString();
    const size_t capacity = 15000;
    DynamicJsonDocument doc(capacity);
    DeserializationError error = deserializeJson(doc, payload);

    if (error) {
      Serial.print(F("deserializeJson() failed: "));
      Serial.println(error.f_str());
    } else {
      JsonArray records = doc["records"];
      for (JsonObject record : records) {
        String sitename_val = record["sitename"].as<String>();
        if (sitename_val == targetSitename) {
          Serial.print("Found data for sitename: ");
          Serial.println(sitename_val);
         
          client.publish((String(mqtt_publish_base_topic) + "sitename").c_str(), sitename_val.c_str());
          client.publish((String(mqtt_publish_base_topic) + "AQI").c_str(), record["aqi"].as<String>().c_str());
          client.publish((String(mqtt_publish_base_topic) + "SO2Subindex").c_str(), record["so2subindex"].as<String>().c_str());
          client.publish((String(mqtt_publish_base_topic) + "COSubindex").c_str(), record["cosubindex"].as<String>().c_str());
         
          String o3subindex_val = record["o3subindex"].as<String>();
          if (o3subindex_val.length() > 0) {
              client.publish((String(mqtt_publish_base_topic) + "O3Subindex").c_str(), o3subindex_val.c_str());
          }
         
          client.publish((String(mqtt_publish_base_topic) + "PM10Subindex").c_str(), record["pm10subindex"].as<String>().c_str());
          client.publish((String(mqtt_publish_base_topic) + "NO2Subindex").c_str(), record["no2subindex"].as<String>().c_str());
          client.publish((String(mqtt_publish_base_topic) + "O38-hourSubindex").c_str(), record["o38subindex"].as<String>().c_str());
          client.publish((String(mqtt_publish_base_topic) + "PM2.5Subindex").c_str(), record["pm25subindex"].as<String>().c_str());
         
          break;
        }
      }
    }
  } else {
    Serial.print("HTTP Request failed with status code: ");
    Serial.println(httpCode);
  }
  http.end();
}

void setup() {
  Serial.begin(115200);

  // 建立互斥鎖
  xMutex = xSemaphoreCreateMutex();

  // 建立 Wi-Fi 連線
  connectToWiFi();
 
  // 設定 MQTT 客戶端
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);

  // 建立 MQTT 任務並在 Core 0 運行
  xTaskCreatePinnedToCore(
    mqttTask,         // 任務函式
    "mqttTask",       // 任務名稱
    10000,            // 堆疊大小 (位元組)
    NULL,             // 任務參數
    1,                // 任務優先級
    NULL,             // 任務句柄
    0);               // Core ID (Core 0)

  // 建立 HTTP 任務並在 Core 1 運行
  xTaskCreatePinnedToCore(
    httpTask,         // 任務函式
    "httpTask",       // 任務名稱
    15000,            // 堆疊大小 (位元組)
    NULL,             // 任務參數
    1,                // 任務優先級
    NULL,             // 任務句柄
    1);               // Core ID (Core 1)
}

void loop() {
  // 保持 loop() 為空,所有工作都由 FreeRTOS 任務處理
}

// 處理 MQTT 的任務,運行在 Core 0
void mqttTask(void * parameter) {
  for (;;) {
    if (WiFi.status() != WL_CONNECTED) {
      connectToWiFi();
    }
    if (!client.connected()) {
      reconnectMQTT();
    }
    client.loop();
    vTaskDelay(10 / portTICK_PERIOD_MS); // 稍微延遲以讓出 CPU
  }
}

// 處理 HTTP 和 JSON 解析的任務,運行在 Core 1
void httpTask(void * parameter) {
  static uint32_t last_restart_time = millis();
  const uint32_t restart_interval = 30000; // 30000 毫秒 = 30 秒
 
  for (;;) {
    if (xSemaphoreTake(xMutex, (TickType_t)10) == pdTRUE) {
      if (targetSitename.length() > 0) {
        fetchAndPublishData();
        targetSitename = ""; // 處理完後清空
      }
      xSemaphoreGive(xMutex); // 釋放互斥鎖
    }
   
    // 檢查是否達到重置時間
    if (millis() - last_restart_time >= restart_interval) {
      Serial.println("30 seconds passed, restarting ESP32...");
      ESP.restart(); // 重新啟動 ESP32
    }
   
    vTaskDelay(1000 / portTICK_PERIOD_MS); // 每秒檢查一次
  }
}


https://data.moenv.gov.tw/api/v2/aqx_p_434?api_key=9e565f9a-84dd-4e79-9097-d403cae1ea75&limit=1000&sort=monitordate%20desc&format=JSON

{
    "fields": [
        {
            "id": "siteid",
            "type": "text",
            "info": {
                "label": "測站編號"
            }
        },
        {
            "id": "sitename",
            "type": "text",
            "info": {
                "label": "測站名稱"
            }
        },
        {
            "id": "monitordate",
            "type": "text",
            "info": {
                "label": "監測日期"
            }
        },
        {
            "id": "aqi",
            "type": "text",
            "info": {
                "label": "空氣品質指標"
            }
        },
        {
            "id": "so2subindex",
            "type": "text",
            "info": {
                "label": "二氧化硫副指標"
            }
        },
        {
            "id": "cosubindex",
            "type": "text",
            "info": {
                "label": "一氧化碳副指標"
            }
        },
        {
            "id": "o3subindex",
            "type": "text",
            "info": {
                "label": "臭氧副指標"
            }
        },
        {
            "id": "pm10subindex",
            "type": "text",
            "info": {
                "label": "懸浮微粒副指標"
            }
        },
        {
            "id": "no2subindex",
            "type": "text",
            "info": {
                "label": "二氧化氮副指標"
            }
        },
        {
            "id": "o38subindex",
            "type": "text",
            "info": {
                "label": "臭氧8小時副指標"
            }
        },
        {
            "id": "pm25subindex",
            "type": "text",
            "info": {
                "label": "細懸浮微粒副指標"
            }
        }
    ],
    "resource_id": "5f5ef051-2dd6-4e4e-b855-f82755d9dfc1",
    "__extras": {
        "api_key": "9e565f9a-84dd-4e79-9097-d403cae1ea75"
    },
    "include_total": true,
    "total": "287181",
    "resource_format": "object",
    "limit": "1000",
    "offset": "0",
    "_links": {
        "start": "\/api\/v2\/aqx_p_434?api_key=9e565f9a-84dd-4e79-9097-d403cae1ea75&limit=1000&format=JSON&sort=monitordate desc",
        "next": "\/api\/v2\/aqx_p_434?api_key=9e565f9a-84dd-4e79-9097-d403cae1ea75&limit=1000&format=JSON&sort=monitordate desc&offset=1000"
    },
    "records": [
        {
            "siteid": "139",
            "sitename": "員林",
            "monitordate": "2025-08-03",
            "aqi": "21",
            "so2subindex": "6",
            "cosubindex": "2",
            "o3subindex": "",
            "pm10subindex": "10",
            "no2subindex": "12",
            "o38subindex": "20",
            "pm25subindex": "21"
        },
        {
            "siteid": "85",
            "sitename": "大城",
            "monitordate": "2025-08-03",
            "aqi": "26",
            "so2subindex": "6",
            "cosubindex": "1",
            "o3subindex": "",
            "pm10subindex": "17",
            "no2subindex": "10",
            "o38subindex": "26",
            "pm25subindex": "23"
        },

沒有留言:

張貼留言

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