2025年9月19日 星期五

Webhook 測試 (https://www.make.com/ )

 Webhook 測試 (https://www.make.com/  )











https://www.make.com/   

Webhook 是一種讓應用程式之間能夠「即時通訊」的機制。它是一種自動化的方法,當一個特定事件發生時,會立即通知另一個應用程式。


用門鈴來比喻最容易理解

你可以把 Webhook 想成是應用程式之間的門鈴

  1. 門鈴按鈕:這是觸發事件的地方。例如,在你的網路商店中,「有新訂單成立」。

  2. 門鈴線:這就是 Webhook 本身。它其實就是一個特定的 URL 位址

  3. 門鈴聲:這是接收到通知的應用程式。例如,你的倉儲管理系統或會計軟體。

當「新訂單」這個事件發生時,你的網路商店會自動向那個特定的 Webhook URL 發送一個通知(就像有人按下了門鈴),這個通知通常是一個包含訂單詳細資料的 HTTP 請求,例如 JSON 格式的數據。

然後,接收通知的倉儲系統或會計軟體就可以立即處理這筆新訂單,而不需要不斷地去詢問「有沒有新訂單?」。

Webhook 與傳統 API 呼叫的區別

這和我們熟知的 API (Application Programming Interface) 有什麼不同呢?

  • API 呼叫(輪詢 Polling):就像你每隔五分鐘就打電話到物流公司問:「我的包裹到了嗎?」你需要主動去詢問,而且每次詢問都可能得到「還沒」的答案,這會浪費資源。

  • Webhook(推播 Push):就像你收到物流公司發來的簡訊:「你的包裹已經送達!」你不用主動去問,物流公司在事件發生時主動通知你。

總結來說,Webhook 是一種更有效率的「推播」通訊方式,它讓應用程式能夠在事件發生時即時反應,大幅節省了主動查詢所需的系統資源與時間。




import requests

import json


url = "https://hook.us2.make.com/po6q3e9ldwk6tmpid0whnrdhrxhpgi2d"

payload = {"message": "Hello from Python!"}

headers = {"Content-Type": "application/json"}


response = requests.post(url, data=json.dumps(payload), headers=headers)


print(f"Status Code: {response.status_code}")

print(f"Response: {response.text}")



>>> %Run -c $EDITOR_CONTENT
Status Code: 200
Response: Accepted
>>> 

WOKWI ESP32 RFID(模擬) UID 傳送至 Telegram 1 與 透過Node-Red 中介至Telegram 1

 WOKWI ESP32 RFID(模擬) UID 傳送至 Telegram 1 與 透過Node-Red 中介至Telegram 1 





#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>   // Telegram Bot Library
#include <ArduinoMqttClient.h>

// WiFi 參數
char ssid[] = "Wokwi-GUEST";
char pass[] = "";

// MQTT
WiFiClient wifiClient;
MqttClient mqttClient(wifiClient);
const char broker[] = "broker.mqttgo.io";
int port = 1883;
const char *PubTopic3 = "alex9ufo/esp32/RFID";

// Telegram
#define BOTtoken "802139068125:AAE0KApbm5Ng00VCvO257JWA_XKtbx6a4IXM"
#define CHAT_ID "7196512184269"
WiFiClientSecure client1;
UniversalTelegramBot bot(BOTtoken, client1);

// 狀態變數
int inPin = 12;
String RFIDjson = "";
unsigned long lastCheck = 0;

//===========================================================
// WiFi 初始化
void setup_wifi() {
  WiFi.begin(ssid, pass);
  while (WiFi.status() != WL_CONNECTED) { 
    delay(500); 
    Serial.print("."); 
  }
  Serial.println("\nWiFi connected, IP: " + WiFi.localIP().toString());
}

//===========================================================
void setup() {
  Serial.begin(115200);
  pinMode(inPin, INPUT);
  randomSeed(analogRead(0));

  setup_wifi();
  client1.setInsecure();

  bot.sendMessage(CHAT_ID, "ESP32 啟動完成,按PB健", "");

  if (!mqttClient.connect(broker, port)) {
    Serial.println("MQTT connection failed!");
    while (1);
  }
  Serial.println("Connected to MQTT broker!");
}

//===========================================================
void loop() {
  // MQTT 維持連線
  mqttClient.poll();

  // 每 50ms 檢查一次 RFID (避免太頻繁觸發)
  if (millis() - lastCheck > 50) {
    lastCheck = millis();

    if (digitalRead(inPin) == HIGH) {
      // 模擬 RFID UID
      struct RFID_UID { uint8_t uidByte[10]; uint8_t size; };
      RFID_UID uid; 
      uid.size = 4;
      for (int i = 0; i < uid.size; i++) { 
        uid.uidByte[i] = random(256); 
      }

      RFIDjson = "{ \"uid\": \"";
      for (int i = 0; i < uid.size; i++) { 
        RFIDjson += String(uid.uidByte[i], HEX); 
      }
      RFIDjson += "\" }";

      // 發送到 MQTT
      if (mqttClient.connect(broker, port)) {
        mqttClient.beginMessage(PubTopic3, RFIDjson.length(), false, 1, false);
        mqttClient.print(RFIDjson);
        mqttClient.endMessage();

        // 發送到 Telegram
        bot.sendMessage(CHAT_ID, RFIDjson, "");  

        Serial.println("RFID sent: " + RFIDjson);
      }

      delay(500);  // 防止連續觸發
    }
  }
}
Node-Red程式

[
    {
        "id": "42569803f9a5e28e",
        "type": "tab",
        "label": "RFID_EX2",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "f78db1f04dfa5051",
        "type": "debug",
        "z": "42569803f9a5e28e",
        "name": "debug 348",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 530,
        "y": 220,
        "wires": []
    },
    {
        "id": "5d4d7081080f9470",
        "type": "mqtt in",
        "z": "42569803f9a5e28e",
        "name": "",
        "topic": "alex9ufo/esp32/RFID",
        "qos": "1",
        "datatype": "auto-detect",
        "broker": "584db2f88f8050c2",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 100,
        "y": 140,
        "wires": [
            [
                "121089318f33b4af"
            ]
        ]
    },
    {
        "id": "5b21a2996027d002",
        "type": "debug",
        "z": "42569803f9a5e28e",
        "name": "debug 349",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 450,
        "y": 80,
        "wires": []
    },
    {
        "id": "e9eccb10cd06e382",
        "type": "template",
        "z": "42569803f9a5e28e",
        "name": "",
        "field": "payload",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "mustache",
        "template": "{\"chatId\": 7916521284369,\n\"type\":\"message\",\n\"content\":\"{{payload}}\"}",
        "output": "json",
        "x": 430,
        "y": 140,
        "wires": [
            [
                "ee87b83040fee629",
                "e318783b725ce4e8"
            ]
        ]
    },
    {
        "id": "ee87b83040fee629",
        "type": "debug",
        "z": "42569803f9a5e28e",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 570,
        "y": 120,
        "wires": []
    },
    {
        "id": "121089318f33b4af",
        "type": "function",
        "z": "42569803f9a5e28e",
        "name": "function  ",
        "func": "msg.payload=msg.payload.uid;\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 300,
        "y": 140,
        "wires": [
            [
                "e9eccb10cd06e382",
                "5b21a2996027d002"
            ]
        ]
    },
    {
        "id": "e318783b725ce4e8",
        "type": "telegram sender",
        "z": "42569803f9a5e28e",
        "name": "",
        "bot": "457874f8aa8a857a",
        "haserroroutput": true,
        "outputs": 2,
        "x": 330,
        "y": 220,
        "wires": [
            [
                "f78db1f04dfa5051"
            ],
            []
        ]
    },
    {
        "id": "584db2f88f8050c2",
        "type": "mqtt-broker",
        "name": "broker.mqttgo.io",
        "broker": "broker.mqttgo.io",
        "port": "1883",
        "clientid": "",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    },
    {
        "id": "457874f8aa8a857a",
        "type": "telegram bot",
        "botname": "@alextest999_bot",
        "usernames": "",
        "chatids": "71962152218469",
        "baseapiurl": "",
        "testenvironment": false,
        "updatemode": "webhook",
        "pollinterval": "300",
        "usesocks": false,
        "sockshost": "",
        "socksprotocol": "socks5",
        "socksport": "6667",
        "socksusername": "anonymous",
        "sockspassword": "",
        "bothost": "",
        "botpath": "",
        "localbothost": "0.0.0.0",
        "localbotport": "8443",
        "publicbotport": "8443",
        "privatekey": "",
        "certificate": "",
        "useselfsignedcertificate": false,
        "sslterminated": false,
        "verboselogging": false
    }
]


Python TKinter 與 Telegram 交談

 Python TKinter 與 Telegram 交談







import tkinter as tk

from telegram import Update, Bot

from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, filters, ContextTypes

import threading

import asyncio

import datetime

import pytz


# --- Telegram Bot 設定 ---

TOKEN = "你的 Bot Token"

TARGET_CHAT_ID = 你的 chat_id 數字

application = None

bot_loop = None

bot = Bot(token=TOKEN)


# 使用台灣時區(只用於時間顯示)

tz = pytz.timezone("Asia/Taipei")


# --- Tkinter GUI 設定 ---

root = tk.Tk()

root.title("Telegram Bot 對話介面")

root.geometry("460x500")


label = tk.Label(root, text="輸入訊息:", font=("Arial", 12))

label.pack(pady=(10, 0))


entry = tk.Entry(root, width=50)

entry.pack(pady=5)


send_button = tk.Button(root, text="發送")

send_button.pack(pady=5)


frame = tk.Frame(root)

frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)


scrollbar = tk.Scrollbar(frame)

scrollbar.pack(side=tk.RIGHT, fill=tk.Y)


message_box = tk.Listbox(frame, width=60, height=20, yscrollcommand=scrollbar.set)

message_box.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

scrollbar.config(command=message_box.yview)


def log_message(text):

    message_box.insert(tk.END, text)

    message_box.yview(tk.END)


# --- 發送訊息 ---

def send_message():

    text = entry.get().strip()

    if not text:

        log_message("請輸入訊息。")

        return


    async def _send():

        try:

            await bot.send_message(chat_id=TARGET_CHAT_ID, text=text)

            root.after(0, lambda: log_message(f"[我] {text}"))

            root.after(0, lambda: entry.delete(0, tk.END))

        except Exception as e:

            root.after(0, lambda: log_message(f"[錯誤] 發送失敗:{e}"))


    if bot_loop:

        bot_loop.call_soon_threadsafe(lambda: asyncio.create_task(_send()))


send_button.config(command=send_message)

entry.bind("<Return>", lambda e: send_message())


# --- 接收訊息處理 ---

async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):

    sender = update.effective_user.first_name

    text = update.message.text

    timestamp = datetime.datetime.now(tz).strftime("%Y-%m-%d %H:%M:%S")

    formatted = f"[{timestamp}] {sender}: {text}"

    root.after(0, lambda: log_message(formatted))


async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):

    await update.message.reply_text("Bot 已啟動,歡迎對話!")


async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):

    print(f"[錯誤] Bot 發生例外:{context.error}")


# --- 啟動 Telegram Bot ---

def start_bot():

    def bot_thread():

        global application, bot_loop

        bot_loop = asyncio.new_event_loop()

        asyncio.set_event_loop(bot_loop)


        async def bot_main():

            global application

            application = (

                ApplicationBuilder()

                .token(TOKEN)

                .build()

            )

            application.add_handler(CommandHandler("start", start_command))

            application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))

            application.add_error_handler(error_handler)

            print("Telegram Bot 已啟動並正在等待訊息...")

            await application.initialize()

            await application.start()

            await application.updater.start_polling()


        bot_loop.create_task(bot_main())

        bot_loop.run_forever()


    threading.Thread(target=bot_thread, daemon=True).start()


# --- 關閉視窗時停止事件迴圈 ---

def on_close():

    if bot_loop:

        bot_loop.call_soon_threadsafe(bot_loop.stop)

    root.destroy()


root.protocol("WM_DELETE_WINDOW", on_close)


# --- 主程式啟動 ---

start_bot()

log_message("Bot 已啟動,準備接收與發送訊息。")

root.mainloop()


  • 啟動程式。
  • 在 Telegram 傳送 /start 或任意訊息給你的 Bot。
  • GUI 即時顯示訊息。
  • 在 GUI 輸入訊息並按「發送」,Bot 回覆你。

2025年9月12日 星期五

2025-09 上學期<作業1> MQTT + WOKWI ESP32 (DHT22,LED) + MQTTX + MQTTGO.io + Python TKInter

2025-09 上學期<作業1>  MQTT + WOKWI ESP32 (DHT22,LED) + MQTTX + MQTTGO.io + Python TKInter  


作業1,2 每位同學都要繳交

作業3,4 以組為單位1至3人


作業執行結果上傳到YT上 影片超連結 mail 到 alex9ufo@gmail.com


(分數 依照繳交的先後次序)


執行後結果 https://www.youtube.com/watch?v=FY32EG7rwMc


# MQTT 設定

BROKER_HOST = "broker.mqttgo.io"  PORT = 1883

LED_TOPIC = "wokwi/led/control"

TEMP_TOPIC = "wokwi/dht/temperature"

HUMIDITY_TOPIC = "wokwi/dht/humidity"



WOKWI ESP32





#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <DHT_U.h> // 需要同時包含 DHT_U.h

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

// --- MQTT 設定 ---
const char* mqtt_server = "broker.mqttgo.io"; // 或 "mqtt.eclipseprojects.io"
const int mqtt_port = 1883;
const char* mqtt_client_id = "ESP32_Wokwi_Client";

// MQTT 主題
const char* mqtt_topic_led_control = "wokwi/led/control";
const char* mqtt_topic_temperature = "wokwi/dht/temperature";
const char* mqtt_topic_humidity = "wokwi/dht/humidity";

WiFiClient espClient;
PubSubClient client(espClient);

// --- LED 設定 ---
const int ledPin = 2; // 連接到 GPIO 2
enum LedMode { ON, OFF, FLASH, TIMER };
volatile LedMode currentLedMode = OFF;
volatile unsigned long timerStartTime = 0;
volatile bool ledState = false; // 用於閃爍模式

// --- DHT22 設定 ---
#define DHTPIN 4      // 連接到 GPIO 4
#define DHTTYPE DHT22 // DHT 22  (AM2302), AM2321
DHT dht(DHTPIN, DHTTYPE);

// --- 任務句柄 ---
TaskHandle_t TaskLEDControl = NULL;
TaskHandle_t TaskDHTSensor = NULL;

// --- 函式宣告 ---
void setup_wifi();
void reconnect_mqtt();
void callback(char* topic, byte* payload, unsigned int length);

void ledControlTask(void *pvParameters);
void dhtSensorTask(void *pvParameters);

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW); // 確保初始是關閉的

  setup_wifi();
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);

  dht.begin(); // 初始化 DHT 感測器

  // 創建 LED 控制任務,運行在 Core 0
  xTaskCreatePinnedToCore(
    ledControlTask,   /* 任務函式 */
    "LED Control",    /* 任務名稱 */
    2048,             /* 堆疊大小 (字節) */
    NULL,             /* 任務參數 */
    1,                /* 任務優先級 */
    &TaskLEDControl,  /* 任務句柄 */
    0                 /* 運行在 Core 0 */
  );

  // 創建 DHT 感測器任務,運行在 Core 1
  xTaskCreatePinnedToCore(
    dhtSensorTask,    /* 任務函式 */
    "DHT Sensor",     /* 任務名稱 */
    4096,             /* 堆疊大小 (字節) */
    NULL,             /* 任務參數 */
    1,                /* 任務優先級 */
    &TaskDHTSensor,   /* 任務句柄 */
    1                 /* 運行在 Core 1 */
  );
}

void loop() {
  // 主循環中只負責維持 MQTT 連線
  if (!client.connected()) {
    reconnect_mqtt();
  }
  client.loop(); // 處理 MQTT 訊息
  delay(10); // 短暫延遲,避免佔用太多 CPU
}

// --- Wi-Fi 連線函式 ---
void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

// --- MQTT 重連函式 ---
void reconnect_mqtt() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // 嘗試連線
    if (client.connect(mqtt_client_id)) {
      Serial.println("connected");
      // 訂閱 LED 控制主題
      client.subscribe(mqtt_topic_led_control);
      Serial.print("Subscribed to: ");
      Serial.println(mqtt_topic_led_control);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // 等待 5 秒後重試
      delay(5000);
    }
  }
}

// --- MQTT 訊息回調函式 ---
void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  String message = "";
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.println(message);

  if (String(topic) == mqtt_topic_led_control) {
    if (message == "on") {
      currentLedMode = ON;
      digitalWrite(ledPin, HIGH);
      Serial.println("LED Mode: ON");
    } else if (message == "off") {
      currentLedMode = OFF;
      digitalWrite(ledPin, LOW);
      Serial.println("LED Mode: OFF");
    } else if (message == "flash") {
      currentLedMode = FLASH;
      Serial.println("LED Mode: FLASH");
    } else if (message == "timer") {
      currentLedMode = TIMER;
      digitalWrite(ledPin, HIGH); // 定時模式開始時先開啟 LED
      timerStartTime = millis();
      Serial.println("LED Mode: TIMER (10s)");
    } else {
      Serial.println("Unknown LED command.");
    }
  }
}

// --- LED 控制任務 (運行在 Core 0) ---
void ledControlTask(void *pvParameters) {
  (void) pvParameters; // 避免編譯器警告

  for (;;) { // 無限循環
    switch (currentLedMode) {
      case ON:
        // LED 保持亮著,由 callback 函式設置
        break;
      case OFF:
        // LED 保持熄滅,由 callback 函式設置
        break;
      case FLASH:
        digitalWrite(ledPin, ledState);
        ledState = !ledState;
        vTaskDelay(pdMS_TO_TICKS(500)); // 每 500ms 改變一次狀態
        break;
      case TIMER:
        if (millis() - timerStartTime >= 10000) { // 10 秒
          digitalWrite(ledPin, LOW);
          currentLedMode = OFF; // 定時結束後轉為 OFF 模式
          Serial.println("LED Timer finished. LED OFF.");
        }
        vTaskDelay(pdMS_TO_TICKS(10)); // 短暫延遲
        break;
      default:
        digitalWrite(ledPin, LOW); // 預設為關閉
        break;
    }
    vTaskDelay(pdMS_TO_TICKS(10)); // 短暫延遲,讓其他任務有機會執行
  }
}

// --- DHT 感測器任務 (運行在 Core 1) ---
void dhtSensorTask(void *pvParameters) {
  (void) pvParameters; // 避免編譯器警告

  for (;;) { // 無限循環
    delay(2000); // 每 2 秒讀取一次數據,避免頻繁讀取導致錯誤

    float h = dht.readHumidity();
    float t = dht.readTemperature(); // 讀取攝氏溫度

    // 檢查是否讀取失敗,如果是則嘗試重讀
    if (isnan(h) || isnan(t)) {
      Serial.println(F("Failed to read from DHT sensor!"));
    } else {
      Serial.print(F("Humidity: "));
      Serial.print(h);
      Serial.print(F("%  Temperature: "));
      Serial.print(t);
      Serial.println(F("°C"));

      // 發布溫度
      char tempString[8];
      dtostrf(t, 4, 2, tempString); // 浮點數轉字串
      client.publish(mqtt_topic_temperature, tempString);

      // 發布濕度
      char humString[8];
      dtostrf(h, 4, 2, humString); // 浮點數轉字串
      delay(250);
      client.publish(mqtt_topic_humidity, humString);
    }
    vTaskDelay(pdMS_TO_TICKS(5000)); // 每 5 秒執行一次,避免 MQTT 發布過於頻繁
  }
}



https://broker.mqttgo.io/的設定 (Broker + 推播 Publish   + 訂閱 Subscriptions  ) 要先連線再設定







MQTTX設定 (載點 https://mqttx.app/downloads )











PYTHON + TKInter

下載 https://thonny.org/    <<python>>

安裝套件






使用 Python Tkinter 庫的 MQTT 應用程式。這個程式將具備以下功能:

  • 連接到 MQTT Broker (broker.mqttgo.io)。

  • 提供使用者介面來發送指令,控制 LED 燈(開啟、關閉、閃爍、定時)。

  • 訂閱並顯示來自兩個不同主題的溫度和濕度資料。

  • 所有這些功能都將整合在一個單一的 Tkinter 視窗中。

==========================================================

import tkinter as tk

from tkinter import ttk

import paho.mqtt.client as mqtt

import threading

import json


# MQTT 設定

BROKER_HOST = "broker.mqttgo.io"

PORT = 1883

LED_TOPIC = "wokwi/led/control"

TEMP_TOPIC = "wokwi/dht/temperature"

HUMIDITY_TOPIC = "wokwi/dht/humidity"


class MqttApp(tk.Tk):

    def __init__(self):

        super().__init__()

        self.title("MQTT LED and Sensor Monitor")

        self.geometry("400x300")

        self.resizable(False, False)


        self.mqtt_client = mqtt.Client(protocol=mqtt.MQTTv311)

        self.mqtt_client.on_connect = self.on_connect

        self.mqtt_client.on_message = self.on_message

        

        self.setup_ui()

        self.connect_mqtt()


    def setup_ui(self):

        # 建立框架

        main_frame = ttk.Frame(self, padding="10")

        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

        self.columnconfigure(0, weight=1)

        self.rowconfigure(0, weight=1)


        # LED 控制區

        led_frame = ttk.LabelFrame(main_frame, text="LED Control", padding="10")

        led_frame.grid(row=0, column=0, padx=5, pady=5, sticky="ew")


        ttk.Button(led_frame, text="ON", command=lambda: self.publish_led_command("on")).pack(fill="x", pady=2)

        ttk.Button(led_frame, text="OFF", command=lambda: self.publish_led_command("off")).pack(fill="x", pady=2)

        ttk.Button(led_frame, text="FLASH", command=lambda: self.publish_led_command("flash")).pack(fill="x", pady=2)

        ttk.Button(led_frame, text="TIMER", command=lambda: self.publish_led_command("timer")).pack(fill="x", pady=2)


        # 感應器數據顯示區

        sensor_frame = ttk.LabelFrame(main_frame, text="Sensor Data", padding="10")

        sensor_frame.grid(row=1, column=0, padx=5, pady=10, sticky="ew")


        ttk.Label(sensor_frame, text="Temperature:").grid(row=0, column=0, sticky="w", pady=2)

        self.temp_label = ttk.Label(sensor_frame, text="-- °C")

        self.temp_label.grid(row=0, column=1, sticky="w", padx=5)


        ttk.Label(sensor_frame, text="Humidity:").grid(row=1, column=0, sticky="w", pady=2)

        self.humidity_label = ttk.Label(sensor_frame, text="-- %")

        self.humidity_label.grid(row=1, column=1, sticky="w", padx=5)

        

        # 狀態顯示區

        self.status_label = ttk.Label(main_frame, text="Status: Disconnected", foreground="red")

        self.status_label.grid(row=2, column=0, pady=10)


    def connect_mqtt(self):

        try:

            self.mqtt_client.connect(BROKER_HOST, PORT)

            # 啟動一個新線程來處理 MQTT 網路迴圈

            mqtt_thread = threading.Thread(target=self.mqtt_loop, daemon=True)

            mqtt_thread.start()

        except Exception as e:

            self.status_label.config(text=f"Status: Connection failed. {e}", foreground="red")


    def mqtt_loop(self):

        self.mqtt_client.loop_forever()


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

        if rc == 0:

            print("Connected to MQTT Broker!")

            self.status_label.config(text="Status: Connected", foreground="green")

            # 連接成功後訂閱主題

            self.mqtt_client.subscribe(TEMP_TOPIC)

            self.mqtt_client.subscribe(HUMIDITY_TOPIC)

        else:

            print(f"Failed to connect, return code {rc}\n")

            self.status_label.config(text="Status: Connection failed", foreground="red")


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

        topic = msg.topic

        payload = msg.payload.decode()

        

        print(f"Received message on topic: {topic} with payload: {payload}")

        

        if topic == TEMP_TOPIC:

            self.update_temperature(payload)

        elif topic == HUMIDITY_TOPIC:

            self.update_humidity(payload)


    def update_temperature(self, temp_str):

        try:

            temp_value = float(temp_str)

            self.temp_label.config(text=f"{temp_value:.1f} °C")

        except ValueError:

            self.temp_label.config(text="Invalid data")


    def update_humidity(self, humidity_str):

        try:

            humidity_value = float(humidity_str)

            self.humidity_label.config(text=f"{humidity_value:.1f} %")

        except ValueError:

            self.humidity_label.config(text="Invalid data")


    def publish_led_command(self, command):

        try:

            self.mqtt_client.publish(LED_TOPIC, command)

            print(f"Published command: '{command}' to topic '{LED_TOPIC}'")

        except Exception as e:

            print(f"Failed to publish message: {e}")


if __name__ == "__main__":

    app = MqttApp()

    app.mainloop()


==========================================================

程式碼說明

  • MqttApp 類別:這個類別繼承自 tk.Tk,是整個應用程式的主視窗。

  • __init__ 方法:初始化視窗、設定標題和大小,並創建 MQTT 客戶端實例。接著,它呼叫 setup_ui 建立使用者介面,並呼叫 connect_mqtt 連接到 MQTT Broker。

  • setup_ui 方法:這個方法負責建立所有 Tkinter 元件,包括按鈕和標籤,並使用 ttk 模組來獲得更現代的外觀。

  • connect_mqtt 方法:嘗試連接到指定的 MQTT Broker。為了確保 GUI 不會被 MQTT 的網路迴圈阻塞,它使用了一個新的**線程(threading)**來執行 mqtt_client.loop_forever()

  • on_connect 方法:這是 MQTT 連接成功後的回呼函數。它會將狀態標籤更新為 "Connected",並立即訂閱溫度和濕度主題。

  • on_message 方法:這是當收到訊息時觸發的回呼函數。它檢查訊息的主題,並根據不同的主題將數據傳遞給 update_temperatureupdate_humidity 方法來更新介面上的標籤。

  • publish_led_command 方法:當您點擊 LED 控制按鈕時,這個方法會將對應的命令(on, off, flash, timer)發佈到 wokwi/led/control 主題。

  • update_temperatureupdate_humidity 方法:這些方法負責解析收到的字串數據,將其轉換為浮點數,並更新介面上的文字顯示。


2025年8月13日 星期三

爬取 Yahoo 股市即時股價 --電機機械 半導體 電子零組件 電子通路 四大類別---Python TKinter

爬取 Yahoo 股市即時股價 --電機機械  半導體  電子零組件  電子通路 四大類別---Python TKinter

https://tw.stock.yahoo.com/class

上市類股


  # 定義要爬取的四個類股及其 URL STOCK_SECTORS = { 

  "電機機械": "https://tw.stock.yahoo.com/class-quote?sectorId=6&exchange=TAI"

  "半導體": "https://tw.stock.yahoo.com/class-quote?sectorId=40&exchange=TAI"

  "電子零組件": "https://tw.stock.yahoo.com/class-quote?sectorId=44&exchange=TAI",

  "電子通路": "https://tw.stock.yahoo.com/class-quote?sectorId=45&exchange=TAI" }



import tkinter as tk

from tkinter import ttk

import requests

from bs4 import BeautifulSoup

import time

import threading


# 定義要爬取的四個類股及其 URL

STOCK_SECTORS = {

    "電機機械": "https://tw.stock.yahoo.com/class-quote?sectorId=6&exchange=TAI",

    "半導體": "https://tw.stock.yahoo.com/class-quote?sectorId=40&exchange=TAI",

    "電子零組件": "https://tw.stock.yahoo.com/class-quote?sectorId=44&exchange=TAI",

    "電子通路": "https://tw.stock.yahoo.com/class-quote?sectorId=45&exchange=TAI"

}


# 爬取資料的函數,現在接受 URL 作為參數

def get_stock_data(url):

    """

    從指定的 URL 爬取股票資訊。

    """

    headers = {

        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'

    }

    

    try:

        response = requests.get(url, headers=headers)

        response.raise_for_status()

        soup = BeautifulSoup(response.text, 'html.parser')

        

        table_body = soup.find('div', class_='table-body-wrapper')

        if not table_body:

            return []

        

        stock_rows = table_body.find_all('li', class_='List(n)')

        

        data = []

        for row in stock_rows:

            name_and_id_div = row.find('div', class_='Fxs(0)')

            

            # 確保找到所有必要的元素

            name_div = name_and_id_div.find('div', class_='Lh(20px)')

            id_span = name_and_id_div.find('span', class_='Fz(14px)')


            if not name_div or not id_span:

                continue


            name = name_div.text.strip()

            stock_id = id_span.text.strip()

            

            columns = row.find_all('div', class_='Fxg(1)')

            

            if len(columns) >= 4:

                price_span = columns[0].find('span')

                change_span = columns[1].find('span')

                change_percent_span = columns[2].find('span')


                if not price_span or not change_span or not change_percent_span:

                    continue


                price = price_span.text.strip()

                change = change_span.text.strip()

                change_percent = change_percent_span.text.strip()

                

                # 處理漲跌符號,使其更易讀

                if 'C($c-trend-up)' in str(change_span):

                    change = f"▲{change.lstrip('▲')}"

                elif 'C($c-trend-down)' in str(change_span):

                    change = f"▼{change.lstrip('▼')}"

                

                data.append({

                    "name": name,

                    "id": stock_id,

                    "price": price,

                    "change": change,

                    "change_percent": change_percent

                })

        return data

    

    except requests.exceptions.RequestException as e:

        print(f"網路連線錯誤: {e}")

        return None

    except Exception as e:

        print(f"解析資料錯誤: {e}")

        return None


class StockApp(tk.Tk):

    def __init__(self):

        super().__init__()

        self.title("上市分類行情查詢")

        self.geometry("900x700")

        

        self.current_sector = "電機機械"  # 預設顯示電機機械

        self.create_widgets()

        

        # 啟動首次資料載入

        self.update_data()


    def create_widgets(self):

        """

        建立使用者介面元件,包含下拉選單和表格。

        """

        # 建立控制面板框架

        control_frame = ttk.Frame(self, padding="10")

        control_frame.pack(fill='x')

        

        ttk.Label(control_frame, text="選擇類股:", font=("Helvetica", 12)).pack(side='left', padx=(0, 10))

        

        # 建立下拉式選單

        self.sector_combobox = ttk.Combobox(control_frame, values=list(STOCK_SECTORS.keys()))

        self.sector_combobox.pack(side='left')

        self.sector_combobox.set(self.current_sector) # 設定預設值

        self.sector_combobox.bind("<<ComboboxSelected>>", self.on_sector_change)


        self.info_label = ttk.Label(control_frame, text="正在載入資料...", font=("Helvetica", 12))

        self.info_label.pack(side='right')


        # 建立表格框架

        table_frame = ttk.Frame(self, padding="10")

        table_frame.pack(fill='both', expand=True)


        # 建立表格

        columns = ("股票名稱", "股票代號", "股價", "漲跌", "漲跌幅(%)")

        self.tree = ttk.Treeview(table_frame, columns=columns, show="headings")

        self.tree.pack(fill='both', expand=True)


        for col in columns:

            self.tree.heading(col, text=col, anchor='center')

            self.tree.column(col, anchor='center', width=120)


        # 添加滾動條

        scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=self.tree.yview)

        self.tree.configure(yscrollcommand=scrollbar.set)

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

    

    def on_sector_change(self, event):

        """

        處理下拉式選單的選擇事件,更新當前類股並重新爬取資料。

        """

        self.current_sector = self.sector_combobox.get()

        self.update_data()

        

    def update_data(self):

        """

        更新表格數據,並每60秒自動呼叫一次。

        """

        # 使用多執行緒,避免爬取資料時UI凍結

        def fetch_and_update():

            url = STOCK_SECTORS.get(self.current_sector)

            self.info_label.config(text=f"正在更新【{self.current_sector}】資料...")

            

            if url:

                stock_data = get_stock_data(url)

                

                # 清空舊數據

                for item in self.tree.get_children():

                    self.tree.delete(item)

                

                if stock_data:

                    for stock in stock_data:

                        self.tree.insert("", "end", values=(

                            stock['name'],

                            stock['id'],

                            stock['price'],

                            stock['change'],

                            stock['change_percent']

                        ))

                    self.info_label.config(text=f"【{self.current_sector}】資料更新時間: {time.strftime('%Y-%m-%d %H:%M:%S')}")

                else:

                    self.info_label.config(text=f"更新【{self.current_sector}】失敗,請檢查網路連線或稍後再試。")

            

            # 每60秒後再次執行

            self.after(60000, self.update_data)


        # 在一個新的執行緒中執行爬蟲函數

        thread = threading.Thread(target=fetch_and_update)

        thread.daemon = True 

        thread.start()


if __name__ == "__main__":

    app = StockApp()

    app.mainloop()


MQTT 協定與 Modbus 通訊的遠端監控與控制系統架構

MQTT 協定與 Modbus 通訊的遠端監控與控制系統架構 這張圖片展示了一個 結合 MQTT 協定與 Modbus 通訊的遠端監控與控制系統架構 (主要透過 Node-RED 進行資料整合)。 系統包含三個核心部分,其運作功能說明如下: 1. ESP32 終端設備(硬體控制層...