2025年9月20日 星期六

第二次作業 模擬 RFID(要按PB) 輸出UID至MQTT Broker 後給Python TKinter 暨 Node-Red (Sqlite資料庫) 另一路至Telegram

第二次作業   

模擬 RFID(要按PB) 輸出UID至MQTT Broker 後給Python TKinter 暨 Node-Red (Sqlite資料庫) 另一路至Telegram 



結果  https://www.youtube.com/watch?v=U-_k7d53rYI&t=10s

Telegram 安裝


參考  https://alex9ufoexploer.blogspot.com/2025/04/telegram.html

Telegram 完全教學攻略:從註冊、中文化、設定到各種應用技巧一次學會
https://www.techbang.com/posts/76688-telegram-complete-lying-on-teaching-strategies-from-registration-chinese-setting-stoking-to-a-variety-of-application-techniques-learned-at-once
iOS下載網址:https://reurl.cc/VaKlNQ
Android下載網址:https://reurl.cc/alQALY
PC/Mac/Linux下載網址:https://reurl.cc/lL8Ag6
macOS下載網址:https://reurl.cc/EKkeeK
網頁版連結:https://reurl.cc/mdaAAM







WOKWI硬體 利用PB壓下後產生亂數模擬RFID UID碼




(需修改 )

// Telegram
#define BOTtoken "8023906815:AAE0KApbm5Ng00VCvO57JWA_XKtbx6a4IXM"
#define CHAT_ID "7965218469"
否則您的telegram 會無法收到訊息
Wokwi程式 

#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 "802131906815:AAE0KApbm5Ng100VCvO57JWA1_XKtbx6a4IXM"
#define CHAT_ID "796152218469"
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);  // 防止連續觸發
    }
  }
}




# 程式中 資料庫路徑

DB_PATH = r"D:\2025RFID\2025-09下學期\EX2\rfid.db"

請修改為您存放的路徑

Python程式


import tkinter as tk

from tkinter import ttk

import sqlite3

import paho.mqtt.client as mqtt

from datetime import datetime


# 資料庫路徑

DB_PATH = r"D:\2025RFID\2025-09下學期\EX2\rfid.db"

MQTT_BROKER = "broker.mqttgo.io"

MQTT_PORT = 1883

MQTT_TOPIC = "alex9ufo/esp32/RFID"


# 建立資料表(如果尚未存在)

def init_db():

    conn = sqlite3.connect(DB_PATH)

    cursor = conn.cursor()

    cursor.execute("""

        CREATE TABLE IF NOT EXISTS rfid_logs (

            id INTEGER PRIMARY KEY AUTOINCREMENT,

            uid TEXT NOT NULL,

            date TEXT NOT NULL,

            time TEXT NOT NULL

        )

    """)

    conn.commit()

    conn.close()


# 插入新資料

def insert_uid(uid):

    now = datetime.now()

    date_str = now.strftime("%Y-%m-%d")

    time_str = now.strftime("%H:%M:%S")

    conn = sqlite3.connect(DB_PATH)

    cursor = conn.cursor()

    cursor.execute("INSERT INTO rfid_logs (uid, date, time) VALUES (?, ?, ?)", (uid, date_str, time_str))

    conn.commit()

    conn.close()

    refresh_table()


# 讀取資料並更新表格

def refresh_table():

    for row in tree.get_children():

        tree.delete(row)

    conn = sqlite3.connect(DB_PATH)

    cursor = conn.cursor()

    cursor.execute("SELECT * FROM rfid_logs ORDER BY id DESC")

    rows = cursor.fetchall()

    for row in rows:

        tree.insert("", tk.END, values=row)

    conn.close()


# MQTT 訊息處理

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

    client.subscribe(MQTT_TOPIC)


def on_message(client, userdata, msg):

    uid = msg.payload.decode()

    insert_uid(uid)


# 建立 GUI

root = tk.Tk()

root.title("RFID 資料顯示")

root.geometry("600x400")


columns = ("id", "uid", "date", "time")

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

for col in columns:

    tree.heading(col, text=col)

    tree.column(col, width=100)

tree.pack(fill=tk.BOTH, expand=True)


# 初始化資料庫與表格

init_db()

refresh_table()


# 啟動 MQTT 客戶端

client = mqtt.Client()

client.on_connect = on_connect

client.on_message = on_message

client.connect(MQTT_BROKER, MQTT_PORT, 60)

client.loop_start()


# 啟動 GUI 主迴圈

root.mainloop()


模擬一個 RFID 打卡記錄系統的桌面端監控程式。

  1. MQTT 即時監聽 (Real-time Listener): 程式作為一個 MQTT 客戶端,持續連線到 broker.mqttgo.io:1883 並訂閱 alex9ufo/esp32/RFID 主題。

  2. 數據處理與儲存 (Data Logging): 一旦收到來自 MQTT 的訊息(被視為 RFID 卡號 UID),程式會自動獲取當前的日期和時間,將 UID、日期、時間 一起寫入本地的 rfid.db 資料庫中的 rfid_logs 資料表。

  3. 圖形化介面顯示 (GUI): 使用 Tkinter 創建一個簡單的視窗,內含一個表格 (ttk.Treeview),用於即時顯示資料庫中的所有打卡記錄。每次有新的記錄插入時,表格會自動更新,確保使用者看到最新的數據。

import tkinter as tk

from tkinter import ttk

import sqlite3

import paho.mqtt.client as mqtt

from datetime import datetime


# ==============================================================================

# 程式配置區塊 (Configuration Block)

# ==============================================================================


# 資料庫路徑

DB_PATH = r"D:\2025RFID\2025-09下學期\EX2\rfid.db"

# MQTT Broker 伺服器位址

MQTT_BROKER = "broker.mqttgo.io"

# MQTT Broker 連接埠 (標準未加密連線)

MQTT_PORT = 1883

# MQTT 訂閱及發佈的主題名稱

MQTT_TOPIC = "alex9ufo/esp32/RFID"


# ==============================================================================

# 資料庫操作函式 (Database Functions)

# ==============================================================================


# 建立資料表(如果尚未存在)

def init_db():

    # 連接到 SQLite 資料庫檔案

    conn = sqlite3.connect(DB_PATH)

    cursor = conn.cursor()

    # 執行 SQL 語句:建立 rfid_logs 資料表

    # 包含 id (主鍵, 自動遞增)、uid (卡號)、date (日期)、time (時間)

    cursor.execute("""

        CREATE TABLE IF NOT EXISTS rfid_logs (

            id INTEGER PRIMARY KEY AUTOINCREMENT,

            uid TEXT NOT NULL,

            date TEXT NOT NULL,

            time TEXT NOT NULL

        )

    """)

    conn.commit() # 提交變更

    conn.close() # 關閉連線


# 插入新資料 (由 MQTT 觸發)

def insert_uid(uid):

    # 獲取當前時間

    now = datetime.now()

    # 格式化日期為 YYYY-MM-DD

    date_str = now.strftime("%Y-%m-%d")

    # 格式化時間為 HH:MM:SS

    time_str = now.strftime("%H:%M:%S")

    

    conn = sqlite3.connect(DB_PATH)

    cursor = conn.cursor()

    # 執行 INSERT 語句,使用 '?' 作為佔位符防止 SQL 注入

    cursor.execute("INSERT INTO rfid_logs (uid, date, time) VALUES (?, ?, ?)", (uid, date_str, time_str))

    conn.commit()

    conn.close()

    

    # 插入新數據後,立即更新 GUI 表格顯示

    refresh_table()


# 讀取資料並更新表格 (GUI 介面)

def refresh_table():

    # 1. 清空 Treeview 中現有的所有行

    for row in tree.get_children():

        tree.delete(row)

        

    # 2. 從資料庫讀取最新數據

    conn = sqlite3.connect(DB_PATH)

    cursor = conn.cursor()

    # 查詢所有數據,並按 ID 倒序排列 (DESC),最新的記錄在最上面

    cursor.execute("SELECT * FROM rfid_logs ORDER BY id DESC")

    rows = cursor.fetchall() # 獲取所有結果

    conn.close()

    

    # 3. 將數據插入到 Treeview

    for row in rows:

        # tree.insert(父節點, 位置, 值)

        tree.insert("", tk.END, values=row)


# ==============================================================================

# MQTT 客戶端回調函式 (MQTT Client Callbacks)

# ==============================================================================


# 連線成功時的回調函式

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

    # rc = 0 代表連線成功

    if rc == 0:

        # 連線成功後,訂閱指定的主題

        client.subscribe(MQTT_TOPIC)

        # 可以添加 print("MQTT 連線成功並已訂閱") 進行除錯

    

# 收到 MQTT 訊息時的回調函式

def on_message(client, userdata, msg):

    # 將收到的二進制 payload 轉換為字串 (即 RFID UID)

    uid = msg.payload.decode()

    # 呼叫函式將此 UID 寫入資料庫

    insert_uid(uid)


# ==============================================================================

# GUI 介面設定 (GUI Setup)

# ==============================================================================


# 建立主視窗

root = tk.Tk()

root.title("RFID 資料顯示")

root.geometry("600x400") # 設定視窗初始大小


# 定義表格的欄位名稱

columns = ("id", "uid", "date", "time")

# 建立 Treeview 表格組件

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


# 配置每個欄位

for col in columns:

    tree.heading(col, text=col) # 設定欄位標題

    tree.column(col, width=100) # 設定欄位寬度

    

# 將表格組件放置到主視窗中,fill=tk.BOTH 擴展填充,expand=True 允許隨視窗縮放

tree.pack(fill=tk.BOTH, expand=True)


# ==============================================================================

# 初始化與啟動 (Initialization and Start)

# ==============================================================================


# 執行資料庫初始化 (建立資料表)

init_db()

# 首次讀取資料庫數據並顯示在表格中

refresh_table()


# 啟動 MQTT 客戶端

client = mqtt.Client()

# 設定連線成功和收到訊息時的回調函式

client.on_connect = on_connect

client.on_message = on_message

# 連接到 Broker

client.connect(MQTT_BROKER, MQTT_PORT, 60)

# 以非阻塞方式在後台運行網路循環,確保 GUI 不會被 MQTT 循環阻塞

client.loop_start()


# 啟動 GUI 主迴圈 (程式從此進入事件驅動模式,等待用戶操作或 MQTT 訊息)

root.mainloop()



//========================================

Node-RED 安裝步驟暨執行node-red程式

https://alex9ufoexploer.blogspot.com/2025/07/node-red-node-red.html

Node-Red安裝 與 dashboard 安裝與使用範例

https://alex9ufoexploer.blogspot.com/2023/10/node-red-dashboard_18.html

Node-Red安裝 與 dashboard 安裝

https://sites.google.com/site/wenyunotify/12-node-red/01-%E5%85%A5%E9%96%80%E7%AF%87

DB Browser for SQLite 的安裝

https://alex9ufoexploer.blogspot.com/2025/07/db-browser-for-sqlite.html





(安裝node-red , dashboard , sqlitedb, telegrambot 節點 才能匯入程式 )

留意 rfid.db 存放位置 D:\2025RFID\2025-09下學期\EX2\rfid.db 

請修改為您存放的路徑

Node-Red程式 

[

    {

        "id": "ec87aef83806bf77",

        "type": "tab",

        "label": "RFID_2025",

        "disabled": false,

        "info": "",

        "env": []

    },

    {

        "id": "5318e15b20b2d057",

        "type": "mqtt in",

        "z": "ec87aef83806bf77",

        "name": "RFID UID In",

        "topic": "alex9ufo/esp32/RFID",

        "qos": "1",

        "datatype": "auto-detect",

        "broker": "450dc18180d64bb4",

        "nl": false,

        "rap": true,

        "rh": 0,

        "inputs": 0,

        "x": 110,

        "y": 380,

        "wires": [

            [

                "43e89ebe38071896",

                "d189cd151fb44415"

            ]

        ]

    },

    {

        "id": "09f57e8989839b3f",

        "type": "ui_text",

        "z": "ec87aef83806bf77",

        "group": "01def7a22d7e0b47",

        "order": 1,

        "width": 4,

        "height": 1,

        "name": "UID",

        "label": "UID",

        "format": "{{msg.payload}} ",

        "layout": "row-left",

        "className": "",

        "style": false,

        "font": "",

        "fontSize": "",

        "color": "#000000",

        "x": 390,

        "y": 420,

        "wires": []

    },

    {

        "id": "a5f9d8b1a41e041a",

        "type": "ui_text",

        "z": "ec87aef83806bf77",

        "group": "01def7a22d7e0b47",

        "order": 2,

        "width": 4,

        "height": 1,

        "name": "Current Time",

        "label": "",

        "format": "<span style=\"font-weight:bold;\">{{msg.payload}}</span>",

        "layout": "row-spread",

        "className": "",

        "style": false,

        "font": "",

        "fontSize": "",

        "color": "#000000",

        "x": 530,

        "y": 660,

        "wires": []

    },

    {

        "id": "3276e8ef114117e3",

        "type": "inject",

        "z": "ec87aef83806bf77",

        "name": "Current Time",

        "props": [

            {

                "p": "payload"

            },

            {

                "p": "topic",

                "vt": "str"

            }

        ],

        "repeat": "1",

        "crontab": "",

        "once": true,

        "onceDelay": "0.1",

        "topic": "",

        "payload": "",

        "payloadType": "date",

        "x": 120,

        "y": 660,

        "wires": [

            [

                "b69f3cf69db70f15"

            ]

        ]

    },

    {

        "id": "b69f3cf69db70f15",

        "type": "function",

        "z": "ec87aef83806bf77",

        "name": "Format Date/Time",

        "func": "const now = new Date(msg.payload);\nconst date = now.toLocaleDateString();\nconst time = now.toLocaleTimeString();\nmsg.payload = `${date} ${time}`;\nreturn msg;",

        "outputs": 1,

        "timeout": "",

        "noerr": 0,

        "initialize": "",

        "finalize": "",

        "libs": [],

        "x": 330,

        "y": 660,

        "wires": [

            [

                "a5f9d8b1a41e041a"

            ]

        ]

    },

    {

        "id": "c43ba373c649a667",

        "type": "function",

        "z": "ec87aef83806bf77",

        "name": "Log UID",

        "func": "const now = new Date();\nconst rdate = now.toISOString().split('T')[0];\nconst rtime = now.toTimeString().split(' ')[0];\nvar rtemp= msg.payload ;\nmsg.topic = \"INSERT INTO rfid_logs (uid ,date, time) VALUES ($temp , $date, $time )\";\nmsg.payload = [rtemp ,rdate, rtime ];\nreturn msg;\n\n//CREATE TABLE IF NOT EXISTS rfid_logs \n//(id INTEGER PRIMARY KEY AUTOINCREMENT,\n//uid TEXT NOT NULL,\n//date TEXT NOT NULL,\n//time TEXT NOT NULL)",

        "outputs": 1,

        "timeout": "",

        "noerr": 0,

        "initialize": "",

        "finalize": "",

        "libs": [],

        "x": 400,

        "y": 380,

        "wires": [

            [

                "eaf68880f7e1e8a3"

            ]

        ]

    },

    {

        "id": "121608f3028a5a08",

        "type": "sqlite",

        "z": "ec87aef83806bf77",

        "mydb": "9c0419f225af3721",

        "sqlquery": "msg.topic",

        "sql": "{{msg.topic}}",

        "name": "RFID SQLite DB",

        "x": 500,

        "y": 320,

        "wires": [

            [

                "306c76656d27a7b8"

            ]

        ]

    },

    {

        "id": "5616bc8197f9d0b3",

        "type": "inject",

        "z": "ec87aef83806bf77",

        "name": "Refresh DB",

        "props": [

            {

                "p": "payload"

            },

            {

                "p": "topic",

                "vt": "str"

            }

        ],

        "repeat": "",

        "crontab": "",

        "once": true,

        "onceDelay": "0.1",

        "topic": "SELECT id, date, time, event FROM events ORDER BY id DESC",

        "payload": "",

        "payloadType": "date",

        "x": 130,

        "y": 560,

        "wires": [

            [

                "0963eb3d0d4dcbd0"

            ]

        ]

    },

    {

        "id": "9fad4c0dccccf994",

        "type": "ui_table",

        "z": "ec87aef83806bf77",

        "group": "01def7a22d7e0b47",

        "name": "Database View",

        "order": 3,

        "width": 8,

        "height": 12,

        "columns": [

            {

                "field": "id",

                "title": "ID碼",

                "width": "",

                "align": "center",

                "formatter": "plaintext",

                "formatterParams": {

                    "target": "_blank"

                }

            },

            {

                "field": "uid",

                "title": "UID號碼",

                "width": "",

                "align": "left",

                "formatter": "plaintext",

                "formatterParams": {

                    "target": "_blank"

                }

            },

            {

                "field": "date",

                "title": "日期",

                "width": "",

                "align": "center",

                "formatter": "plaintext",

                "formatterParams": {

                    "target": "_blank"

                }

            },

            {

                "field": "time",

                "title": "時間",

                "width": "",

                "align": "center",

                "formatter": "plaintext",

                "formatterParams": {

                    "target": "_blank"

                }

            }

        ],

        "outputs": 0,

        "cts": false,

        "x": 700,

        "y": 560,

        "wires": []

    },

    {

        "id": "0963eb3d0d4dcbd0",

        "type": "sqlite",

        "z": "ec87aef83806bf77",

        "mydb": "9c0419f225af3721",

        "sqlquery": "msg.topic",

        "sql": "{{msg.topic}}",

        "name": "RFID Query DB",

        "x": 480,

        "y": 560,

        "wires": [

            [

                "9fad4c0dccccf994"

            ]

        ]

    },

    {

        "id": "f3f1c42856e97243",

        "type": "inject",

        "z": "ec87aef83806bf77",

        "name": "Create Table Once",

        "props": [

            {

                "p": "payload"

            },

            {

                "p": "topic",

                "vt": "str"

            }

        ],

        "repeat": "",

        "crontab": "",

        "once": true,

        "onceDelay": "2",

        "topic": "CREATE TABLE IF NOT EXISTS rfid_logs (id INTEGER PRIMARY KEY AUTOINCREMENT,uid TEXT NOT NULL,date TEXT NOT NULL,time TEXT NOT NULL)",

        "payload": "",

        "payloadType": "date",

        "x": 170,

        "y": 320,

        "wires": [

            [

                "121608f3028a5a08"

            ]

        ]

    },

    {

        "id": "306c76656d27a7b8",

        "type": "debug",

        "z": "ec87aef83806bf77",

        "name": "debug ",

        "active": true,

        "tosidebar": true,

        "console": false,

        "tostatus": false,

        "complete": "payload",

        "targetType": "msg",

        "statusVal": "",

        "statusType": "auto",

        "x": 710,

        "y": 320,

        "wires": []

    },

    {

        "id": "eaf68880f7e1e8a3",

        "type": "sqlite",

        "z": "ec87aef83806bf77",

        "mydb": "9c0419f225af3721",

        "sqlquery": "msg.topic",

        "sql": "{{msg.topic}}",

        "name": "RFID SQLite DB",

        "x": 560,

        "y": 380,

        "wires": [

            [

                "306c76656d27a7b8"

            ]

        ]

    },

    {

        "id": "e7e7a5822097d8ec",

        "type": "inject",

        "z": "ec87aef83806bf77",

        "name": "Current Time",

        "props": [

            {

                "p": "payload"

            },

            {

                "p": "topic",

                "vt": "str"

            }

        ],

        "repeat": "5",

        "crontab": "",

        "once": true,

        "onceDelay": "0.1",

        "topic": "",

        "payload": "",

        "payloadType": "date",

        "x": 120,

        "y": 600,

        "wires": [

            [

                "d1656423a801e0d6"

            ]

        ]

    },

    {

        "id": "d1656423a801e0d6",

        "type": "function",

        "z": "ec87aef83806bf77",

        "name": "function  query",

        "func": "msg.topic = \"SELECT id, uid, date, time FROM rfid_logs ORDER BY id DESC\";\nreturn msg;",

        "outputs": 1,

        "timeout": 0,

        "noerr": 0,

        "initialize": "",

        "finalize": "",

        "libs": [],

        "x": 300,

        "y": 600,

        "wires": [

            [

                "0963eb3d0d4dcbd0"

            ]

        ]

    },

    {

        "id": "c35bdfe07b11b681",

        "type": "comment",

        "z": "ec87aef83806bf77",

        "name": "RFID_uid",

        "info": "CREATE TABLE IF NOT EXISTS rfid_logs (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    uid TEXT NOT NULL,\n    date TEXT NOT NULL,\n    time TEXT NOT NULL\n)\n\nCREATE TABLE \"rfid_logs\" (\n\t\"id\"\tINTEGER,\n\t\"uid\"\tTEXT NOT NULL,\n\t\"date\"\tTEXT NOT NULL,\n\t\"time\"\tTEXT NOT NULL,\n\tPRIMARY KEY(\"id\" AUTOINCREMENT)\n);",

        "x": 480,

        "y": 280,

        "wires": []

    },

    {

        "id": "43e89ebe38071896",

        "type": "debug",

        "z": "ec87aef83806bf77",

        "name": "debug 345",

        "active": true,

        "tosidebar": true,

        "console": false,

        "tostatus": false,

        "complete": "false",

        "statusVal": "",

        "statusType": "auto",

        "x": 210,

        "y": 480,

        "wires": []

    },

    {

        "id": "d189cd151fb44415",

        "type": "json",

        "z": "ec87aef83806bf77",

        "name": "",

        "property": "payload",

        "action": "str",

        "pretty": false,

        "x": 250,

        "y": 380,

        "wires": [

            [

                "c43ba373c649a667",

                "09f57e8989839b3f",

                "fd411fc6c641a651"

            ]

        ]

    },

    {

        "id": "fd411fc6c641a651",

        "type": "debug",

        "z": "ec87aef83806bf77",

        "name": "debug 346",

        "active": true,

        "tosidebar": true,

        "console": false,

        "tostatus": false,

        "complete": "false",

        "statusVal": "",

        "statusType": "auto",

        "x": 430,

        "y": 480,

        "wires": []

    },

    {

        "id": "450dc18180d64bb4",

        "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": "1",

        "birthPayload": "",

        "birthMsg": {},

        "closeTopic": "",

        "closeQos": "0",

        "closePayload": "",

        "closeMsg": {},

        "willTopic": "",

        "willQos": "0",

        "willPayload": "",

        "willMsg": {},

        "userProps": "",

        "sessionExpiry": ""

    },

    {

        "id": "01def7a22d7e0b47",

        "type": "ui_group",

        "name": "Default",

        "tab": "2fcd675506c05c36",

        "order": 1,

        "disp": true,

        "width": 8,

        "collapse": false,

        "className": ""

    },

    {

        "id": "9c0419f225af3721",

        "type": "sqlitedb",

        "db": "D:\\2025RFID\\2025-09下學期\\EX2\\rfid.db",

        "mode": "RWC"

    },

    {

        "id": "2fcd675506c05c36",

        "type": "ui_tab",

        "name": "RFID",

        "icon": "dashboard",

        "disabled": false,

        "hidden": false

    }

]

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 方法:這些方法負責解析收到的字串數據,將其轉換為浮點數,並更新介面上的文字顯示。


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

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