Python Modbus 控制 ADAM-6050 18-ch Isolated Digital I/O Module
import tkinter as tk
from tkinter import messagebox
from pymodbus.client import ModbusTcpClient
import threading
import time
# --- 配置區 ---
ADAM_IP = '192.168.1.128'
ADAM_PORT = 502
DI_COUNT = 12
DO_COUNT = 6
DI_START_ADDR = 0
DO_START_ADDR = 16
class Adam6050App:
def __init__(self, root):
self.root = root
self.root.title("ADAM-6050 控制監控 (相容性修正版)")
self.root.geometry("450x480")
self.client = ModbusTcpClient(ADAM_IP, port=ADAM_PORT)
self.di_labels = []
self.do_vars = []
self.conn_status_var = tk.StringVar(value="連線檢查中...")
self.setup_ui()
self.running = True
self.thread = threading.Thread(target=self.update_loop, daemon=True)
self.thread.start()
def setup_ui(self):
# 狀態欄
status_frame = tk.Frame(self.root, bd=1, relief=tk.GROOVE, padx=10, pady=5)
status_frame.pack(side=tk.BOTTOM, fill=tk.X)
self.status_led = tk.Canvas(status_frame, width=15, height=15, highlightthickness=0)
self.status_led.pack(side=tk.LEFT, padx=5)
self.led_circle = self.status_led.create_oval(2, 2, 13, 13, fill="gray")
self.status_label = tk.Label(status_frame, textvariable=self.conn_status_var, font=("Arial", 9, "bold"))
self.status_label.pack(side=tk.LEFT)
# DI 顯示
di_frame = tk.LabelFrame(self.root, text="DI 狀態 (Digital Input)", padx=10, pady=10)
di_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
for i in range(DI_COUNT):
lbl = tk.Label(di_frame, text=f"DI_{i:02d}", width=8, height=2,
bg="#333333", fg="white", font=("Arial", 9))
lbl.grid(row=i // 4, column=i % 4, padx=5, pady=5)
self.di_labels.append(lbl)
# DO 控制
do_frame = tk.LabelFrame(self.root, text="DO 控制 (Digital Output)", padx=10, pady=10)
do_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
for i in range(DO_COUNT):
var = tk.BooleanVar()
# 注意:這裡使用 command 綁定時,先讀取目前畫面值再寫入
chk = tk.Checkbutton(do_frame, text=f"DO_{i:02d}", variable=var,
command=lambda idx=i: self.toggle_do(idx))
chk.grid(row=i // 3, column=i % 3, padx=15, pady=10)
self.do_vars.append(var)
def update_status_ui(self, is_connected):
if is_connected:
self.status_led.itemconfig(self.led_circle, fill="#00FF00")
self.conn_status_var.set(f"已連線: {ADAM_IP}")
self.status_label.config(fg="#006400")
else:
self.status_led.itemconfig(self.led_circle, fill="red")
self.conn_status_var.set(f"連線中斷: {ADAM_IP}")
self.status_label.config(fg="red")
for lbl in self.di_labels: lbl.config(bg="#333333")
def toggle_do(self, index):
if self.client.is_socket_open():
try:
val = self.do_vars[index].get()
# 關鍵:使用具名參數指定 address 和 value
self.client.write_coil(address=DO_START_ADDR + index, value=val)
except Exception as e:
print(f"寫入失敗: {e}")
else:
messagebox.showwarning("警告", "設備未連線")
def update_loop(self):
while self.running:
connected = self.client.connect()
self.update_status_ui(connected)
if connected:
try:
# 關鍵:強制使用具名參數 address=, count=
# 避免版本將第二個位置參數誤判為 slave/unit
di_res = self.client.read_discrete_inputs(address=DI_START_ADDR, count=DI_COUNT)
if di_res and not di_res.isError():
for i in range(DI_COUNT):
bg_color = "#4CAF50" if di_res.bits[i] else "#333333"
self.di_labels[i].config(bg=bg_color)
do_res = self.client.read_coils(address=DO_START_ADDR, count=DO_COUNT)
if do_res and not do_res.isError():
for i in range(DO_COUNT):
self.do_vars[i].set(do_res.bits[i])
except Exception as e:
print(f"輪詢通訊錯誤: {e}")
time.sleep(0.5)
def on_closing(self):
self.running = False
self.client.close()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = Adam6050App(root)
root.protocol("WM_DELETE_WINDOW", app.on_closing)
root.mainloop()
這段程式碼結合了 Tkinter(圖形介面)與 Pymodbus(工業通訊協定),實現了對 ADAM-6050 模組的即時監控與控制。
以下是逐行的詳細拆解說明:
1. 匯入庫與基礎配置
import tkinter as tk # 建立 GUI 視窗的標準庫
from tkinter import messagebox # 彈出警告對話框用
from pymodbus.client import ModbusTcpClient # 實作 Modbus TCP 通訊的主工具
import threading # 讓通訊在背景執行,避免視窗卡死
import time # 控制更新頻率(延時)
# --- 配置區 ---
ADAM_IP = '192.168.1.128' # ADAM 模組的 IP 地址
ADAM_PORT = 502 # Modbus TCP 標準通訊埠
DI_COUNT = 12 # ADAM-6050 有 12 個輸入點
DO_COUNT = 6 # ADAM-6050 有 6 個輸出點
DI_START_ADDR = 0 # DI 在 Modbus 中的起始地址
DO_START_ADDR = 16 # DO 在 Modbus 中的起始地址 (0x11 hex = 17, 0-based 為 16)
2. 初始化類別與連線
class Adam6050App:
def __init__(self, root):
self.root = root
self.root.title("ADAM-6050 控制監控 (相容性修正版)")
self.root.geometry("450x480")
# 建立 Modbus TCP 客戶端物件,但此時尚未真正連線
self.client = ModbusTcpClient(ADAM_IP, port=ADAM_PORT)
self.di_labels = [] # 儲存 12 個 DI 標籤物件以便後續改顏色
self.do_vars = [] # 儲存 6 個 Checkbutton 的布林狀態
self.conn_status_var = tk.StringVar(value="連線檢查中...") # 狀態欄文字
self.setup_ui() # 呼叫 UI 配置函式
# 啟動「多執行緒」:讓 update_loop 在背景跑,不影響 UI 操作
self.running = True
self.thread = threading.Thread(target=self.update_loop, daemon=True)
self.thread.start()
3. UI 界面配置 (setup_ui)
這裡將視窗分為三個部分:
DI 顯示區:用標籤(Label)當作指示燈。
DO 控制區:用勾選框(Checkbutton)切換開關。
狀態欄:顯示目前是否有連上設備。
def setup_ui(self):
# 狀態欄 (底部)
status_frame = tk.Frame(self.root, bd=1, relief=tk.GROOVE, padx=10, pady=5)
status_frame.pack(side=tk.BOTTOM, fill=tk.X)
# 繪製一個小圓圈當作 LED 燈
self.status_led = tk.Canvas(status_frame, width=15, height=15, highlightthickness=0)
self.status_led.pack(side=tk.LEFT, padx=5)
self.led_circle = self.status_led.create_oval(2, 2, 13, 13, fill="gray")
self.status_label = tk.Label(status_frame, textvariable=self.conn_status_var, font=("Arial", 9, "bold"))
self.status_label.pack(side=tk.LEFT)
# DI 顯示區 (Grid 佈局 3x4)
di_frame = tk.LabelFrame(self.root, text="DI 狀態 (Digital Input)", padx=10, pady=10)
di_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
for i in range(DI_COUNT):
lbl = tk.Label(di_frame, text=f"DI_{i:02d}", width=8, height=2,
bg="#333333", fg="white", font=("Arial", 9))
lbl.grid(row=i // 4, column=i % 4, padx=5, pady=5)
self.di_labels.append(lbl)
# DO 控制區 (Grid 佈局 2x3)
do_frame = tk.LabelFrame(self.root, text="DO 控制 (Digital Output)", padx=10, pady=10)
do_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
for i in range(DO_COUNT):
var = tk.BooleanVar() # 綁定 Checkbutton 狀態
# 當點擊勾選框時,觸發 toggle_do 並傳入編號
chk = tk.Checkbutton(do_frame, text=f"DO_{i:02d}", variable=var,
command=lambda idx=i: self.toggle_do(idx))
chk.grid(row=i // 3, column=i % 3, padx=15, pady=10)
self.do_vars.append(var)
4. 核心邏輯:控制與更新
這部分負責與硬體溝通。
更新連線狀態 UI:
def update_status_ui(self, is_connected):
if is_connected:
self.status_led.itemconfig(self.led_circle, fill="#00FF00") # 變綠燈
self.conn_status_var.set(f"已連線: {ADAM_IP}")
else:
self.status_led.itemconfig(self.led_circle, fill="red") # 變紅燈
self.conn_status_var.set(f"連線中斷: {ADAM_IP}")
控制 DO 輸出:
def toggle_do(self, index):
if self.client.is_socket_open():
try:
val = self.do_vars[index].get() # 取得 UI 上的勾選狀態 (True/False)
# write_coil: 寫入 Modbus 線圈 (Digital Output)
self.client.write_coil(address=DO_START_ADDR + index, value=val)
except Exception as e:
print(f"寫入失敗: {e}")
背景更新迴圈 (update_loop):
這是程式的大腦,每 0.5 秒跑一次。
def update_loop(self):
while self.running:
connected = self.client.connect() # 嘗試與 ADAM 連線
self.update_status_ui(connected)
if connected:
try:
# 讀取 DI (Discrete Inputs): 取得 00001 開始的 12 個狀態
di_res = self.client.read_discrete_inputs(address=DI_START_ADDR, count=DI_COUNT)
if di_res and not di_res.isError():
for i in range(DI_COUNT):
# bits[i] 為 True 代表有輸入,燈變綠色 (#4CAF50)
bg_color = "#4CAF50" if di_res.bits[i] else "#333333"
self.di_labels[i].config(bg=bg_color)
# 讀取 DO (Coils): 讀回目前 ADAM 的輸出狀態同步到 UI
do_res = self.client.read_coils(address=DO_START_ADDR, count=DO_COUNT)
if do_res and not do_res.isError():
for i in range(DO_COUNT):
self.do_vars[i].set(do_res.bits[i]) # 同步勾選框
except Exception as e:
print(f"輪詢通訊錯誤: {e}")
time.sleep(0.5) # 休息半秒,避免 CPU 負載過高
5. 安全結束
def on_closing(self):
self.running = False # 停止 while 迴圈
self.client.close() # 關閉 Modbus TCP 連線
self.root.destroy() # 關閉視窗
總結程式運作流程:
啟動:開啟 UI 視窗並開啟一個後台小幫手(執行緒)。
連線:後台小幫手不斷嘗試連線到
192.168.1.128。監控:連線成功後,每 0.5 秒問 ADAM:「DI 現在狀況如何?」、「DO 現在開還是關?」,然後更新畫面的顏色和勾選框。
控制:當你手動點擊 DO 勾選框,程式立刻發送指令告訴 ADAM:「把第 N 個輸出點打開/關閉」。


















沒有留言:
張貼留言