圖形介面(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 回傳的資料後,再將其顯示在畫面上。
總結
在您的實際應用中:
模擬軟體 執行後,在您的電腦上開啟了
502房間(建立 Server)。Python 程式 執行後,向自己電腦的
502房間敲門(建立 Client 連線)。兩者在同一台電腦(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()



沒有留言:
張貼留言