2026年7月5日 星期日

圖形介面(GUI)的即時數據監控儀表( Modbus TCP 客戶端 Python)

圖形介面(GUI)的即時數據監控儀表( Modbus TCP 客戶端 Python)


「打造一個具備圖形介面(GUI)的即時數據監控儀表板」,用來讀取並呈現 Modbus TCP 伺服器(Server)內部的暫存器與開關狀態。

簡單來說,它扮演的是 Modbus TCP 客戶端(Client / Master) 的角色,就像一個數位看門狗,每秒固定去向主機敲門、要資料,然後把冷冰冰的數據轉化為人類看得懂的視覺畫面。

以下為您拆解這段程式的三大核心意義與運作機制:

1. 建立與硬體/模擬器的通訊橋樑 (Modbus TCP)

程式中使用了 pyModbusTCP 套件,這是在工業自動化(如 PLC、感測器、智慧電表)中非常常見的通訊協定。

  • 目標連線:鎖定在本機端(127.0.0.1)的 502 通訊埠(Port),這正是工業上標準的 Modbus TCP 埠號。

  • 自動管理:設定 auto_open=True,代表程式只要發現連線斷開就會自動嘗試重新連線,不需人工介入。

2. 模擬與對齊實體畫面 (UI 排版意義)

利用 tkinter 庫,程式將資料分門別類,精準對應工業控制中常見的兩種資料型態:

  • 左半邊:Holding Registers (FC=03)

    • 意義:用來儲存數值型資料(例如:溫度、壓力、馬達轉速)。

    • 設計:排成 4 列 × 16 行,剛好對應地址 40001 到 40064。程式還貼心地做了 val -= 65536 的負數轉換,這是因為工業設備傳輸負數時常使用「二補數」表示法,如果不轉換,畫面上的 -3 可能會變成 65533

  • 右半邊:Coil Output (FC=01)

    • 意義:用來儲存二進位狀態(布林值)(例如:燈泡開關、警報觸發、閥門開啟或關閉)。

    • 設計:同樣是 64 個地址,程式利用 💡 ON⚫ OFF 的顏色交替,讓操作員一眼就能看出哪個設備正在運行、哪個正在休息。

3. 即時更新機制 (Real-time Monitoring)

這是整個程式的靈魂所在。如果程式只讀取一次,數值就不會變動。

  • 程式在最後使用了 self.root.after(1000, self.update_modbus_data)

  • 它的意義在於每隔 1000 毫秒(1秒),程式就會自動在背景重新執行一次「抓取資料 $\rightarrow$ 更新畫面」的動作。

  • 畫面上方同時設有狀態列。連線成功時顯示綠色的「狀態: 連線正常」;若網路斷線或伺服器關閉,則立刻變紅提示「連線失敗 / 超時」,防止操作員看到錯誤或過期的舊數據。

總結

這段程式的終極目的,就是把底層複雜的二進位通訊封包,轉化為一個直觀、整齊、每秒動態更新的儀表板,讓工程師或系統操作員能舒舒服服地坐在電腦前,監控遠端設備的所有 Holding Registers 與 Coils 狀態。



在工業自動化、物聯網(IoT)以及機電整合的領域中,「TCP 127.0.0.1:502」 是一個非常經典且標準的通訊組合。我們可以把它拆解為三個核心部分來理解:通訊協定(Modbus TCP)網路地址(127.0.0.1) 以及 通訊埠(502)

以下為您詳細說明這個設定在程式與軟體中的具體意義:

1. 什麼是 127.0.0.1?(本機迴環地址)

  • 定義:在電腦網路中,127.0.0.1 被稱為 Loopback Address(迴環地址),通常對應的主機名稱就是 localhost

  • 意義:當程式把連線目標設定為 127.0.0.1 時,代表「這台電腦自己要跟自己通訊」。封包不會透過實體網路線或 Wi-Fi 送到外面去,而是在作業系統內部的網路層直接折返。

  • 應用場景:這在軟體開發與測試階段非常重要。例如您在電腦上開啟了一個 Modbus 模擬軟體(如您第一張圖中的 ICDT 模擬程式)作為伺服器,然後又寫了一個 Python 程式去讀取它。這兩個軟體都跑在同一台電腦上,彼此就是透過 127.0.0.1 進行溝通。

2. 什麼是 Port 502?(Modbus TCP 的標準通訊埠)

  • 定義:通訊埠(Port)就像是通往電腦這棟大樓的「房間號碼」。一台電腦同時可以執行很多網路程式(網頁瀏覽器、LINE、遊戲等),不同的程式必須使用不同的 Port 來區分資料。

  • 意義:根據國際網際網路網路號碼分配局(IANA)的規定,502 是專門保留給 Modbus 通訊協定使用的標準連接埠

  • 程式行為:當您的 Python 程式指定連線到 502 時,作業系統就知道這個資料封包必須交給正在監聽 502 埠的「工業設備軟體」或「PLC 模擬器」,而不會跟其他的網路流量混淆。

3. 程式與軟體架構:Client 與 Server 的互動

當我們在程式中設定 IP=127.0.0.1:502 時,通常包含兩個角色(主從架構 Master/Slave,在 TCP 中稱為 Client/Server):

伺服器端 (Modbus TCP Server / Slave)

  • 角色:資料的擁有者(例如 PLC、智慧電表或您的模擬軟體)。

  • 行為:它會在電腦中「監聽(Listen)」127.0.0.1:502。它的任務是坐在那裡,等待別人來向它要資料,或者等待別人發送指令來改變暫存器數值。

客戶端 (Modbus TCP Client / Master)

  • 角色:資料的請求者(例如我們寫的 Python 程式、SCADA 中控系統或 HMI 人機介面)。

  • 行為:它會主動發起連線,向 127.0.0.1:502 發送請求封包(例如:「請給我 Holding Register 40001 到 40064 的數值」),收到 Server 回傳的資料後,再將其顯示在畫面上。

總結

在您的實際應用中:

  1. 模擬軟體 執行後,在您的電腦上開啟了 502 房間(建立 Server)。

  2. Python 程式 執行後,向自己電腦的 502 房間敲門(建立 Client 連線)。

  3. 兩者在同一台電腦(127.0.0.1)內,透過 Modbus TCP 通訊協定 實現了每秒的即時數據同步。這就是工業軟體開發中最標準的本機測試流程。

程式1

import tkinter as tk

from tkinter import ttk

from pyModbusTCP.client import ModbusClient


class ModbusViewerApp:

    def __init__(self, root):

        self.root = root

        self.root.title("ICDT MODBUS TCP Client 監視器 (64 Address 修正版)")

        self.root.geometry("1300x650")  # 增加寬度以容納第 5 列

        self.root.configure(bg="#f0f0f0")


        # 初始化 Modbus TCP 客戶端 (連線至本機 502 埠)

        self.client = ModbusClient(host="127.0.0.1", port=502, auto_open=True, auto_close=True)


        # 頂部狀態列

        top_frame = tk.Frame(self.root, bg="#e0e0e0", height=40)

        top_frame.pack(fill=tk.X, side=tk.TOP)

        

        lbl_info = tk.Label(top_frame, text="連線設定: IP=127.0.0.1 | Port=502", font=("Arial", 10, "bold"), bg="#e0e0e0")

        lbl_info.pack(side=tk.LEFT, padx=10, pady=5)

        

        self.lbl_status = tk.Label(top_frame, text="狀態: 未連線", fg="red", bg="#e0e0e0", font=("Arial", 10, "bold"))

        self.lbl_status.pack(side=tk.RIGHT, padx=15, pady=5)


        # 主內容框架

        main_frame = tk.Frame(self.root, bg="#f0f0f0")

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


        # --- 左半邊: Holding Registers (FC=03) ---

        hr_frame = tk.LabelFrame(main_frame, text=" FC=03 Holding Register 40001-40064 ", font=("Arial", 11, "bold"), fg="blue", bg="#f0f0f0")

        hr_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)


        self.hr_inputs = []  # 儲存 Entry 元件,方便後續更新數值

        

        # 修正:改為 5 列,每列 14 個,可完整顯示到 63 (共 64 個)

        for col in range(5):

            col_subframe = tk.Frame(hr_frame, bg="#f0f0f0")

            col_subframe.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5)

            

            for row in range(14):

                index = col * 14 + row

                if index >= 64:  # 超過 63 (第 64 個) 就停止建立

                    break

                

                row_frame = tk.Frame(col_subframe, bg="#f0f0f0")

                row_frame.pack(fill=tk.X, pady=2)

                

                # 顯示索引編號

                lbl_idx = tk.Label(row_frame, text=f"{index:2d}", width=3, anchor="e", bg="#f0f0f0", font=("Courier", 10))

                lbl_idx.pack(side=tk.LEFT, padx=2)

                

                # 數值顯示方塊

                val_entry = ttk.Entry(row_frame, width=7, justify="center")

                val_entry.insert(0, "0")

                val_entry.configure(state="readonly")

                val_entry.pack(side=tk.LEFT, padx=2)

                

                self.hr_inputs.append(val_entry)


        # --- 右半邊: Coil Output (FC=01) ---

        coil_frame = tk.LabelFrame(main_frame, text=" FC=01 Coil output 00001-00064 ", font=("Arial", 11, "bold"), fg="red", bg="#f0f0f0")

        coil_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5)


        self.coil_bulbs = []  # 儲存燈號 Label 元件

        

        # 修正:改為 5 列,每列 14 個,可完整顯示到 63 (共 64 個)

        for col in range(5):

            col_subframe = tk.Frame(coil_frame, bg="#f0f0f0")

            col_subframe.pack(side=tk.LEFT, fill=tk.Y, padx=10, pady=5)

            

            for row in range(14):

                index = col * 14 + row

                if index >= 64:  # 超過 63 (第 64 個) 就停止建立

                    break

                

                row_frame = tk.Frame(col_subframe, bg="#f0f0f0")

                row_frame.pack(fill=tk.X, pady=4)

                

                # 顯示索引編號

                lbl_idx = tk.Label(row_frame, text=f"{index:2d}", width=3, anchor="e", bg="#f0f0f0", font=("Courier", 10))

                lbl_idx.pack(side=tk.LEFT, padx=2)

                

                # 用文字與顏色模擬燈泡 (💡 表示 ON,⚫ 表示 OFF)

                lbl_bulb = tk.Label(row_frame, text="⚫ OFF", width=6, fg="gray", bg="#f0f0f0", font=("Arial", 10, "bold"), anchor="w")

                lbl_bulb.pack(side=tk.LEFT, padx=3)

                

                self.coil_bulbs.append(lbl_bulb)


        # 開始定時更新迴圈

        self.update_modbus_data()


    def update_modbus_data(self):

        # 1. 讀取 Holding Registers (從位址 0 開始讀 64 個,對應 40001-40064)

        regs_data = self.client.read_holding_registers(0, 64)

        

        # 2. 讀取 Coils (從位址 0 開始讀 64 個,對應 00001-00064)

        coils_data = self.client.read_coils(0, 64)


        if regs_data and coils_data:

            self.lbl_status.configure(text="狀態: 連線正常", fg="green")

            

            # 更新 Holding Registers 畫面

            for idx, val in enumerate(regs_data):

                if idx < len(self.hr_inputs):

                    # 處理 16-bit 有號整數負數顯示

                    if val > 32767: 

                        val -= 65536

                    

                    self.hr_inputs[idx].configure(state="normal")

                    self.hr_inputs[idx].delete(0, tk.END)

                    self.hr_inputs[idx].insert(0, str(val))

                    self.hr_inputs[idx].configure(state="readonly")

            

            # 更新 Coils 燈號畫面

            for idx, status in enumerate(coils_data):

                if idx < len(self.coil_bulbs):

                    if status:

                        self.coil_bulbs[idx].configure(text="💡 ON", fg="#FF9900") # 橘黃色亮燈

                    else:

                        self.coil_bulbs[idx].configure(text="⚫ OFF", fg="gray")   # 灰色滅燈

        else:

            self.lbl_status.configure(text="狀態: 連線失敗 / 超時", fg="red")


        # 每 1000 毫秒 (1秒) 重新呼叫自己一次

        self.root.after(1000, self.update_modbus_data)


if __name__ == "__main__":

    root = tk.Tk()

    app = ModbusViewerApp(root)

    root.mainloop()

程式2


import tkinter as tk
from tkinter import ttk
from pyModbusTCP.client import ModbusClient

class ModbusViewerApp:
    def __init__(self, root):
        self.root = root
        self.root.title("ICDT MODBUS TCP Client 監視器 (每列16個-共64個Address)")
        self.root.geometry("1200x680")  # 微調視窗大小以符合 16 列的高度與寬度
        self.root.configure(bg="#f0f0f0")

        # 初始化 Modbus TCP 客戶端 (連線至本機 502 埠)
        self.client = ModbusClient(host="127.0.0.1", port=502, auto_open=True, auto_close=True)

        # 頂部狀態列
        top_frame = tk.Frame(self.root, bg="#e0e0e0", height=40)
        top_frame.pack(fill=tk.X, side=tk.TOP)
        
        lbl_info = tk.Label(top_frame, text="連線設定: IP=127.0.0.1 | Port=502", font=("Arial", 10, "bold"), bg="#e0e0e0")
        lbl_info.pack(side=tk.LEFT, padx=10, pady=5)
        
        self.lbl_status = tk.Label(top_frame, text="狀態: 未連線", fg="red", bg="#e0e0e0", font=("Arial", 10, "bold"))
        self.lbl_status.pack(side=tk.RIGHT, padx=15, pady=5)

        # 主內容框架
        main_frame = tk.Frame(self.root, bg="#f0f0f0")
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # --- 左半邊: Holding Registers (FC=03) ---
        hr_frame = tk.LabelFrame(main_frame, text=" FC=03 Holding Register 40001-40064 ", font=("Arial", 11, "bold"), fg="blue", bg="#f0f0f0")
        hr_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)

        self.hr_inputs = []  # 儲存 Entry 元件,方便後續更新數值
        
        # 設定:4 列,每列 16 個 (16 * 4 = 64)
        for col in range(4):
            col_subframe = tk.Frame(hr_frame, bg="#f0f0f0")
            col_subframe.pack(side=tk.LEFT, fill=tk.Y, padx=8, pady=5)
            
            for row in range(16):
                index = col * 16 + row
                if index >= 64:
                    break
                
                row_frame = tk.Frame(col_subframe, bg="#f0f0f0")
                row_frame.pack(fill=tk.X, pady=2)
                
                # 顯示索引編號
                lbl_idx = tk.Label(row_frame, text=f"{index:2d}", width=3, anchor="e", bg="#f0f0f0", font=("Courier", 10))
                lbl_idx.pack(side=tk.LEFT, padx=2)
                
                # 數值顯示方塊
                val_entry = ttk.Entry(row_frame, width=7, justify="center")
                val_entry.insert(0, "0")
                val_entry.configure(state="readonly")
                val_entry.pack(side=tk.LEFT, padx=2)
                
                self.hr_inputs.append(val_entry)

        # --- 右半邊: Coil Output (FC=01) ---
        coil_frame = tk.LabelFrame(main_frame, text=" FC=01 Coil output 00001-00064 ", font=("Arial", 11, "bold"), fg="red", bg="#f0f0f0")
        coil_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5)

        self.coil_bulbs = []  # 儲存燈號 Label 元件
        
        # 設定:4 列,每列 16 個 (16 * 4 = 64)
        for col in range(4):
            col_subframe = tk.Frame(coil_frame, bg="#f0f0f0")
            col_subframe.pack(side=tk.LEFT, fill=tk.Y, padx=12, pady=5)
            
            for row in range(16):
                index = col * 16 + row
                if index >= 64:
                    break
                
                row_frame = tk.Frame(col_subframe, bg="#f0f0f0")
                row_frame.pack(fill=tk.X, pady=3)
                
                # 顯示索引編號
                lbl_idx = tk.Label(row_frame, text=f"{index:2d}", width=3, anchor="e", bg="#f0f0f0", font=("Courier", 10))
                lbl_idx.pack(side=tk.LEFT, padx=2)
                
                # 用文字與顏色模擬燈泡 (💡 表示 ON,⚫ 表示 OFF)
                lbl_bulb = tk.Label(row_frame, text="⚫ OFF", width=6, fg="gray", bg="#f0f0f0", font=("Arial", 10, "bold"), anchor="w")
                lbl_bulb.pack(side=tk.LEFT, padx=3)
                
                self.coil_bulbs.append(lbl_bulb)

        # 開始定時更新迴圈
        self.update_modbus_data()

    def update_modbus_data(self):
        # 1. 讀取 Holding Registers (從位址 0 開始讀 64 個)
        regs_data = self.client.read_holding_registers(0, 64)
        
        # 2. 讀取 Coils (從位址 0 開始讀 64 個)
        coils_data = self.client.read_coils(0, 64)

        if regs_data and coils_data:
            self.lbl_status.configure(text="狀態: 連線正常", fg="green")
            
            # 更新 Holding Registers 畫面
            for idx, val in enumerate(regs_data):
                if idx < len(self.hr_inputs):
                    # 處理 16-bit 有號整數負數顯示
                    if val > 32767: 
                        val -= 65536
                    
                    self.hr_inputs[idx].configure(state="normal")
                    self.hr_inputs[idx].delete(0, tk.END)
                    self.hr_inputs[idx].insert(0, str(val))
                    self.hr_inputs[idx].configure(state="readonly")
            
            # 更新 Coils 燈號畫面
            for idx, status in enumerate(coils_data):
                if idx < len(self.coil_bulbs):
                    if status:
                        self.coil_bulbs[idx].configure(text="💡 ON", fg="#FF9900") # 橘黃色亮燈
                    else:
                        self.coil_bulbs[idx].configure(text="⚫ OFF", fg="gray")   # 灰色滅燈
        else:
            self.lbl_status.configure(text="狀態: 連線失敗 / 超時", fg="red")

        # 每 1000 毫秒 (1秒) 重新呼叫自己一次
        self.root.after(1000, self.update_modbus_data)

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

2026年7月3日 星期五

MQTT 說明練習 EX1 (PC MQTTX Client , online mqttgo.io , 手機 Iot MQTT Panel)

MQTT 說明練習 EX1 (PC MQTTX Client , online mqttgo.io , 手機 Iot MQTT Panel) 



設定 MQTT 連線時,最核心的四個基本參數為:Broker(伺服器網址)、Port(連接埠)、Topic(主題)以及 Client ID(客戶端識別碼)。 
無論您是在設定手機 App(如 IoT MQTT Panel)、微控制器(如 ESP32、Arduino),或是智慧家庭系統(如 Home Assistant),都需要依照以下指南填入對應的資訊:
核心欄位設定指南
  • Broker / Host / Server:輸入您的 MQTT 伺服器 IP 位址或網址。如果是自建伺服器填入該電腦的區網 IP;如果是使用雲端測試平台,可填入免費的公共伺服器(例如 HiveMQ 公共測試伺服器 的網址 mqtt:// broker.emqx.io  ,   mqtt:// mqttgo.io ,  broker.hivemq.com   )。 
  • Port(連接埠)
    • 1883:一般的未加密 TCP 連線(最常用於測試)。
    • 8883:安全加密的 SSL/TLS 連線(生產環境建議使用)。
    • 8083 / 8084:網頁端 WebSocket 連線專用。 [1, 2]
  • Client ID:每個裝置在伺服器上的唯一識別碼,通常可以隨意設定(例如 my_esp32_01),但同一個伺服器下不可有兩個相同的 Client ID,否則會互相斷線擠下線。 
  • Username / Password:如果您的伺服器有開啟安全驗證,請在此輸入帳號與密碼;若使用匿名連線的公共測試伺服器,則保持空白即可
Topic(主題)命名與溝通規則
MQTT 是透過「發布(Publish)」與「訂閱(Subscribe)」主題來傳遞資訊。  
  • 命名格式:建議使用英文、數字命名,並使用斜線 / 來區分層級(例如:alex9ufo/temperature ,  alex9ufo/humidity , alex9ufo/ledstatus  , alex9ufo/ledcontrol   )。 
  • 連線邏輯
    • 負責傳送資料的裝置(如溫度感測器),必須對該主題進行發布 (Publish)
    • 負責接收資料的裝置(如手機 App 或螢幕),必須訂閱 (Subscribe) 相同名稱的主題才能收到資料。
高級進階參數(依需求調整)
  • Keep Alive(心跳時間):預設通常為 60 秒。這是裝置用來定時向伺服器報平安的時間間隔,避免因網路短暫閒置而被判定斷線。
  • QoS(服務質量)
    • 0:最多傳送一次(最快、最不佔頻寬,漏一兩筆沒關係的資料適用)。
    • 1:最少傳送一次(確保對方一定會收到,但可能重複接收)。
    • 2:剛好傳送一次(最嚴謹,通常用在扣款或不能重複的指令)。
  • Retain(訊息保留):若勾選或設為 True,伺服器會永久保留這個主題的最後一筆訊息。當有新裝置新訂閱這個主題時,會立刻收到最新的歷史狀態,不需等待下一次資料更新。
// --- 設定區 ---
const char* mqtt_server = "mqttgo.io"; // 請替換成你的 MQTT 伺服器網址 (例如 io.adafruit.com)
const int mqtt_port = 1883;

// --- MQTT 主題定義 ---
const char* topic_temp = "alex9ufo/temperature";
const char* topic_humi = "alex9ufo/humidity";
const char* topic_ctrl = "alex9ufo/ledcontrol";
const char* topic_status = "alex9ufo/ledstatus";

IoT MQTT Panel (手機)

MQTTX 下載點 (PC)

https://www.emqx.com/en/downloads/MQTTX


https://mqttgo.io/   (online)

WOKWI 程式 與 線路

🛠️ Wokwi 元件接線指南

在 Wokwi 中,請將元件依照以下引腳(Pin)連接:

  • DHT22: VCC -> 3V3, GND -> GND, SDA (Data) -> GPIO 15

  • I2C LCD 16x2: VCC -> 5V/3V3, GND -> GND, SDA -> GPIO 21, SCL -> GPIO 22

  • LED1 (紅): 陽極 (Anode) -> GPIO 12(記得串聯 220Ω 電阻到 GND)

  • LED2 (綠): 陽極 (Anode) -> GPIO 14(記得串聯 220Ω 電阻到 GND)







#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// --- 設定區 ---
const char* ssid = "Wokwi-GUEST";       // Wokwi 專用模擬 WiFi
const char* password = "";
const char* mqtt_server = "mqttgo.io"; // 請替換成你的 MQTT 伺服器網址 (例如 io.adafruit.com)
const int mqtt_port = 1883;
const char* mqtt_user = ""; // 如果需要認證,請填寫
const char* mqtt_pass = ""; // 如果需要認證,請填寫

// --- 引腳定義 ---
#define DHTPIN 15
#define DHTTYPE DHT22
#define LED1_PIN 12
#define LED2_PIN 14

// --- MQTT 主題定義 ---
const char* topic_temp = "alex9ufo/temperature";
const char* topic_humi = "alex9ufo/humidity";
const char* topic_ctrl = "alex9ufo/ledcontrol";
const char* topic_status = "alex9ufo/ledstatus";

// --- 初始化物件 ---
DHT dht(DHTPIN, DHTTYPE);
LiquidCrystal_I2C lcd(0x27, 16, 2); // 模擬器中 I2C 位址通常為 0x27
WiFiClient espClient;
PubSubClient client(espClient);

unsigned long lastMsg = 0;
const long interval = 5000; // 每 5 秒發行一次溫濕度

void setup_wifi() {
  delay(10);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Connecting WiFi");

  WiFi.begin(ssid, password);

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

  Serial.println("");
  Serial.println("WiFi connected");
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("WiFi Connected");
  delay(1000);
}

// 接收到訂閱訊息的回呼函式 (Callback)
void callback(char* topic, byte* payload, unsigned int length) {
  String messageTemp;
  Serial.print("Message arrived on topic: ");
  Serial.print(topic);
  Serial.print(". Message: ");

  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
    messageTemp += (char)payload[i];
  }
  Serial.println();

  // 顯示接收到的控制指令到 LCD
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("MQTT Recv:");
  lcd.setCursor(0, 1);
  lcd.print(messageTemp);

  // 判斷指令並控制 LED
  if (String(topic) == topic_ctrl) {
    if (messageTemp == "on1") {
      digitalWrite(LED1_PIN, HIGH);
      client.publish(topic_status, "ON1");
      Serial.println("LED1 turned ON");
    } else if (messageTemp == "off1") {
      digitalWrite(LED1_PIN, LOW);
      client.publish(topic_status, "OFF1");
      Serial.println("LED1 turned OFF");
    } else if (messageTemp == "on2") {
      digitalWrite(LED2_PIN, HIGH);
      client.publish(topic_status, "ON2");
      Serial.println("LED2 turned ON");
    } else if (messageTemp == "off2") {
      digitalWrite(LED2_PIN, LOW);
      client.publish(topic_status, "OFF2");
      Serial.println("LED2 turned OFF");
    }
  }
  delay(1500); // 讓使用者看清 LCD 上的控制訊息,隨後會被溫濕度刷掉
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Connect MQTT...");
   
    // 嘗試連接 (使用隨機 Client ID 避免衝突)
    String clientId = "ESP32Client-";
    clientId += String(random(0xffff), HEX);
   
    // 如果不需要帳密,請改為 client.connect(clientId.c_str())
    if (client.connect(clientId.c_str(), mqtt_user, mqtt_pass)) {
      Serial.println("connected");
      lcd.setCursor(0, 1);
      lcd.print("MQTT Connected!");
     
      // 訂閱控制主題
      client.subscribe(topic_ctrl);
      delay(1000);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      lcd.setCursor(0, 1);
      lcd.print("Failed! Retry...");
      delay(5000);
    }
  }
}

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

  // 初始化 LCD
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("Initializing...");

  // 初始化 DHT22
  dht.begin();

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

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  unsigned long now = millis();
  if (now - lastMsg > interval) {
    lastMsg = now;

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

    // 檢查讀取是否失敗
    if (isnan(h) || isnan(t)) {
      Serial.println("Failed to read from DHT sensor!");
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("DHT Read Error");
      return;
    }

    // 顯示於 Serial Monitor
    Serial.print("Humidity: ");
    Serial.print(h);
    Serial.print("%  Temperature: ");
    Serial.print(t);
    Serial.println("°C");

    // 顯示於 16x2 LCD
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Temp: ");
    lcd.print(t, 1);
    lcd.print((char)223); // 顯示 ° 符號
    lcd.print("C");
   
    lcd.setCursor(0, 1);
    lcd.print("Humi: ");
    lcd.print(h, 1);
    lcd.print("%");

    // 發行到 MQTT
    client.publish(topic_temp, String(t).c_str());
    client.publish(topic_humi, String(h).c_str());
   
    Serial.println("Data published to MQTT.");
  }
}

MQTTX 設定畫面










https://mqttgo.io/   設定畫面




IoT MQTT Panel   設定畫面

2個 switch , 2個 LED , 1個 Gauge , 1個 Line Graph

最後的畫面
元件設定畫面


裡面的Panel name 需修改成 SW1 , LED1 , SW2 ,LED2,溫度 濕度





















圖形介面(GUI)的即時數據監控儀表( Modbus TCP 客戶端 Python)

圖形介面(GUI)的即時數據監控儀表( Modbus TCP 客戶端 Python) 「打造一個具備圖形介面(GUI)的即時數據監控儀表板」,用來讀取並呈現 Modbus TCP 伺服器(Server)內部的暫存器與開關狀態。 簡單來說,它扮演的是 Modbus TCP 客戶端(...