2026年1月1日 星期四

Telegram +ESP32自動發報機

 

Telegram +ESP32自動發報機











這套系統是一個典型的 IoT(物聯網)架構,結合了遠端配置(Python)、通訊中介(MQTT)與硬體執行(ESP32)。

以下我為您拆解這兩支程式的核心運作原理:


一、 系統架構流程

  1. Python 端 (控制台):使用者輸入 Telegram 的金鑰 (Token) 與警報文字,透過 MQTT 發送到雲端。

  2. MQTT Broker (中轉站):負責在 Python 與 ESP32 之間傳遞訊息,兩者不需要在同一個區域網路。

  3. ESP32 端 (執行器)

    • 接收 MQTT 傳來的設定值。

    • 每 5 秒掃描一次硬體輸入(光耦)。

    • 若有觸發,執行輸出(Relay)並透過 Telegram API 發送通知。


二、 Python Tkinter 程式詳細解釋

這支程式的角色是 「遠端遙控器」

  • MQTT 連線與 retain=True: 程式使用 paho-mqtt 函式庫。在發送訊息時設置了 retain=True(保留訊息)。這非常重要,因為即使 ESP32 暫時斷電,重新連線後 MQTT Broker 會立刻把最後一次設定好的 Token 發送給 ESP32,不需要人手動重新設定。

  • 介面佈局 (GUI): 使用 tk.Entry 建立輸入框。程式將 6 個 Relay 的訊息儲存在一個列表(List)中,並透過迴圈產生介面,方便擴展與維護。

  • 非同步循環 loop_start(): 這讓 Python 介面在等待 MQTT 連線時不會「凍結」,使用者可以一邊修改文字,程式一邊在背景維持與 Server 的連線。


三、 ESP32 Wokwi 程式詳細解釋

這支程式的角色是 「感測與通訊終端」

  • 定時器邏輯 (millis): 程式沒有使用 delay(5000),而是使用 millis() 檢查時間差。

    • 為什麼? 如果用 delay,ESP32 在這 5 秒內會完全斷訊,無法接收 MQTT 的新訊息。使用 millis() 可以讓 MQTT 接收(mqttClient.loop())與硬體掃描並行。

  • 光耦輸入與 Relay 輸出

    • 光耦 (Photo Coupler):通常用於隔離高電壓雜訊,程式設定為 INPUT_PULLDOWN,代表預設是低電位,接收到信號時變為高電位。

    • Relay (繼電器):對應 6 個輸出,當輸入為 HIGH 時,digitalWrite(relayPins[i], HIGH) 點亮 LED 或驅動繼電器。

  • Telegram 通訊

    • 使用 WiFiClientSecure:因為 Telegram 的伺服器強制要求 HTTPS 加密連線。

    • setInsecure():在 Wokwi 模擬環境中,我們跳過複雜的憑證驗證,直接建立安全通道。

  • 動態初始化 bot 物件: 這是本程式最精巧的地方。ESP32 啟動時並沒有 Token,它會一直等到 MQTT 接收到 alex9ufo/Token 後,才在 callback 函式裡執行 new UniversalTelegramBot。這就是為什麼介面上設定完後,ESP32 的 Pin 2 LED 會亮起。

python tkinter 程式
1) 畫面顯示 連接到 broker.mqtt-dashboard.com (連上綠色)
2) 畫面顯示 主題: alex9ufo/Token 內定 payload:8022701986:AAGymymK9_d12HcTGJWle3mtqHmilxB64_5Zw  可以變更
3) 畫面顯示 主題: alex9ufo/chatID  內定 payload:749652818469   可以變更
4) 畫面顯示 主題: alex9ufo/relay1 內定 payload: 輸入<1> 被觸動   可以變更
5) 畫面顯示 主題: alex9ufo/relay2 內定 payload: 輸入<2> 被觸動   可以變更
6) 畫面顯示 主題: alex9ufo/relay3 內定 payload: 輸入<3> 被觸動   可以變更
7) 畫面顯示 主題: alex9ufo/relay4 內定 payload: 輸入<4> 被觸動   可以變更
8) 畫面顯示 主題: alex9ufo/relay5 內定 payload: 輸入<5> 被觸動   可以變更
9) 畫面顯示 主題: alex9ufo/relay6 內定 payload: 輸入<6> 被觸動   可以變更



input  phtot coupler pin 34,35,32,33,25,26
output relay pin    23,22,21,19,18,5


1) input  phtot coupler pin 34,35,32,33,25,26 , output relay pin 23,22,21,19,18,5  LED pin 2
2) 透過 mqtt  broker.mqtt-dashboard.com 接收主題 : alex9ufo/Token的內容 為telegram bot token 
   接收主題 : alex9ufo/chatID的內容 為telegram chat id 若成功 則 pin 2 輸出 hi LED亮
3) 檢查 input  phtot coupler pin 34,35,32,33,25,26  若是輸入為 hi 則對應 output relay pin    23,22,21,19,18,5 輸出Hi  並 透過 telegram  Token chatID , 輸出對應的內容 輸入<1> .....<6> 被觸動 到 telegram 上通知使用者 
 

Wokwi程式
#include <WiFi.h>
#include <PubSubClient.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>

// WiFi & MQTT 設定
const char* ssid = "Wokwi-GUEST";
const char* password = "";
const char* mqtt_server = "broker.mqtt-dashboard.com";

// 引腳定義
const int inputPins[] = {34, 35, 32, 33, 25, 26};
const int relayPins[] = {23, 22, 21, 19, 18, 5};
const int ledPin = 2; // 系統狀態燈 (MQTT/Telegram 配置成功)

// 設定變數
String telegramToken = "";
String telegramChatID = "";
String relayMessages[6] = {"輸入<1> 被觸動", "輸入<2> 被觸動", "輸入<3> 被觸動",
                           "輸入<4> 被觸動", "輸入<5> 被觸動", "輸入<6> 被觸動"};
bool botInitialized = false;

WiFiClient espClient;
PubSubClient mqttClient(espClient);
WiFiClientSecure secured_client;
UniversalTelegramBot *bot = nullptr;

unsigned long lastCheckTime = 0;
const unsigned long checkInterval = 5000; // 5秒檢查一次

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  for (int i = 0; i < 6; i++) {
    pinMode(inputPins[i], INPUT_PULLDOWN);
    pinMode(relayPins[i], OUTPUT);
    digitalWrite(relayPins[i], LOW);
  }

  setup_wifi();
  mqttClient.setServer(mqtt_server, 1883);
  mqttClient.setCallback(callback);
}

void setup_wifi() {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.println("\nWiFi Connected");
}

void callback(char* topic, byte* payload, unsigned int length) {
  String msg = "";
  for (int i = 0; i < length; i++) msg += (char)payload[i];
  String strTopic = String(topic);

  if (strTopic == "alex9ufo/Token") telegramToken = msg;
  else if (strTopic == "alex9ufo/chatID") telegramChatID = msg;
  else {
    for (int i = 1; i <= 6; i++) {
      if (strTopic == "alex9ufo/relay" + String(i)) relayMessages[i-1] = msg;
    }
  }

  // 配置成功檢查
  if (telegramToken.length() > 20 && telegramChatID.length() > 5) {
    digitalWrite(ledPin, HIGH);
    secured_client.setInsecure();
    if (bot != nullptr) delete bot;
    bot = new UniversalTelegramBot(telegramToken, secured_client);
    botInitialized = true;
    Serial.println("Telegram Bot Initialized");
  }
}

void reconnect() {
  while (!mqttClient.connected()) {
    if (mqttClient.connect("ESP32_Alex_Monitor")) {
      mqttClient.subscribe("alex9ufo/Token");
      mqttClient.subscribe("alex9ufo/chatID");
      for(int i=1; i<=6; i++) mqttClient.subscribe(("alex9ufo/relay" + String(i)).c_str());
    } else { delay(5000); }
  }
}

void loop() {
  if (!mqttClient.connected()) reconnect();
  mqttClient.loop();

  // 每 5 秒執行一次檢查
  if (millis() - lastCheckTime > checkInterval) {
    lastCheckTime = millis();
    Serial.println("--- 5秒自動檢查中 ---");

    for (int i = 0; i < 6; i++) {
      if (digitalRead(inputPins[i]) == HIGH) {
        // 1. 點亮對應 Relay/LED
        digitalWrite(relayPins[i], HIGH);
        Serial.printf("Pin %d is HIGH. Relay %d ON.\n", inputPins[i], i+1);

        // 2. 如果 Telegram 已配置,發送對應訊息
        if (botInitialized) {
          if (bot->sendMessage(telegramChatID, relayMessages[i], "")) {
            Serial.println("Telegram sent: " + relayMessages[i]);
          } else {
            Serial.println("Telegram send failed.");
          }
        }
      } else {
        // 如果輸入為 LOW,則關閉 Relay
        digitalWrite(relayPins[i], LOW);
      }
    }
  }
}

Python程式

import tkinter as tk
from tkinter import messagebox
import paho.mqtt.client as mqtt

# MQTT 設定
MQTT_BROKER = "broker.mqtt-dashboard.com"
MQTT_PORT = 1883

class MqttGui:
    def __init__(self, root):
        self.root = root
        self.root.title("MQTT Telegram 遠端配置工具")
        self.root.geometry("550x680")

        # --- 連線狀態區域 ---
        self.status_frame = tk.Frame(root)
        self.status_frame.pack(pady=10)
        self.status_label = tk.Label(self.status_frame, text="MQTT 狀態: 嘗試中...", fg="black")
        self.status_label.pack(side=tk.LEFT)
        self.canvas = tk.Canvas(self.status_frame, width=20, height=20)
        self.status_light = self.canvas.create_oval(5, 5, 15, 15, fill="red")
        self.canvas.pack(side=tk.LEFT)

        # --- Telegram 基本設定 ---
        tk.Label(root, text="[Step 1] 設定 Telegram 憑證", font=('Arial', 10, 'bold')).pack(pady=5)
        
        tk.Label(root, text="主題: alex9ufo/Token").pack()
        self.token_entry = tk.Entry(root, width=60)
        self.token_entry.insert(0, "80122700986:AAGym1ymK9_rd1eHcTGJWl3mtqHmilxB64_5Zw")
        self.token_entry.pack()

        tk.Label(root, text="主題: alex9ufo/chatID").pack(pady=(5,0))
        self.chatid_entry = tk.Entry(root, width=60)
        self.chatid_entry.insert(0, "7916522184369")
        self.chatid_entry.pack()

        # 修正後的分隔線 (使用 Frame 代替 Separator)
        line = tk.Frame(root, height=2, bd=1, relief=tk.SUNKEN)
        line.pack(fill='x', padx=10, pady=15)

        # --- Relay 訊息設定 ---
        tk.Label(root, text="[Step 2] 設定觸發訊息 (可變更)", font=('Arial', 10, 'bold')).pack(pady=5)
        
        self.relay_entries = []
        for i in range(1, 7):
            frame = tk.Frame(root)
            frame.pack(pady=2, anchor="w", padx=40)
            tk.Label(frame, text=f"Relay {i} 訊息:", width=12, anchor="e").pack(side=tk.LEFT)
            entry = tk.Entry(frame, width=35)
            entry.insert(0, f"輸入<{i}> 被觸動")
            entry.pack(side=tk.LEFT, padx=5)
            self.relay_entries.append(entry)

        # --- 按鈕 ---
        self.send_btn = tk.Button(root, text="發送所有設定到 ESP32", 
                                  bg="#4CAF50", fg="white", font=('Arial', 12, 'bold'),
                                  command=self.publish_all_config)
        self.send_btn.pack(pady=25)

        # 初始化 MQTT
        self.client = mqtt.Client()
        self.client.on_connect = self.on_connect
        try:
            # 這裡增加了一個 try-except 避免沒網路時程式崩潰
            self.client.connect(MQTT_BROKER, MQTT_PORT, 60)
            self.client.loop_start()
        except Exception as e:
            print(f"Connect error: {e}")

    def on_connect(self, client, userdata, flags, rc):
        if rc == 0:
            self.status_label.config(text="MQTT 狀態: 已連線", fg="green")
            self.canvas.itemconfig(self.status_light, fill="green")
        else:
            self.status_label.config(text="MQTT 狀態: 連線失敗", fg="red")

    def publish_all_config(self):
        try:
            # 發送 Token 與 ID (保留 retain 讓 ESP32 斷線回來也能抓到)
            self.client.publish("alex9ufo/Token", self.token_entry.get(), retain=True)
            self.client.publish("alex9ufo/chatID", self.chatid_entry.get(), retain=True)
            
            # 發送 6 個 Relay 的自定義訊息
            for i, entry in enumerate(self.relay_entries):
                topic = f"alex9ufo/relay{i+1}"
                self.client.publish(topic, entry.get(), retain=True)
                
            messagebox.showinfo("發送成功", "所有設定已透過 MQTT 更新!")
        except Exception as e:
            messagebox.showerror("錯誤", f"發送失敗: {e}")

if __name__ == "__main__":
    root = tk.Tk()
    app = MqttGui(root)
    root.mainloop()



沒有留言:

張貼留言

經由MQTT協定的2個WOKWI ESP32 雙向通訊 (ESP32 to ESP32 MQTT Communication )

 經由MQTT協定的2個WOKWI ESP32 雙向通訊  (ESP32  to ESP32 MQTT Communication ) 使用兩個 ESP32 建立一個遠端控制系統。 MQTT Broker: mqtt-dashboard.com Topic (主題): ale...