2026年4月9日 星期四

Python Modbus 控制 ADAM-6050 18-ch Isolated Digital I/O Module

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. 匯入庫與基礎配置

Python
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. 初始化類別與連線

Python
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)切換開關。

  • 狀態欄:顯示目前是否有連上設備。

Python
    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:

Python
    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 輸出:

Python
    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 秒跑一次。

Python
    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. 安全結束

Python
    def on_closing(self):
        self.running = False    # 停止 while 迴圈
        self.client.close()     # 關閉 Modbus TCP 連線
        self.root.destroy()     # 關閉視窗

總結程式運作流程:

  1. 啟動:開啟 UI 視窗並開啟一個後台小幫手(執行緒)。

  2. 連線:後台小幫手不斷嘗試連線到 192.168.1.128

  3. 監控:連線成功後,每 0.5 秒問 ADAM:「DI 現在狀況如何?」、「DO 現在開還是關?」,然後更新畫面的顏色和勾選框。

  4. 控制:當你手動點擊 DO 勾選框,程式立刻發送指令告訴 ADAM:「把第 N 個輸出點打開/關閉」。


沒有留言:

張貼留言

Python Modbus 控制 ADAM-6050 18-ch Isolated Digital I/O Module

Python Modbus 控制  ADAM-6050 18-ch Isolated Digital I/O Module import tkinter as tk from tkinter import messagebox from pymodbus.client impor...