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程式>>
{
"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"
},


沒有留言:
張貼留言