2026年9月14日 星期一

ADAM 6050 ModBus TCP 控制程式

ADAM 6050  ModBus TCP 控制程式












一、 ADAM-6050 與 PC 網路設定說明

根據前面排查結果,您的硬體與網路參數如下:

  • ADAM-6050 IP192.168.50.122 / Port502

  • PC Wi-Fi IP192.168.50.136

  • 子網路遮罩 (Subnet Mask)255.255.255.0

  • 預設閘道 (Gateway)192.168.50.1

1. 網路實體與 IP 連線設定

  • 同網段原則:PC 的 IP (192.168.50.136) 與 ADAM-6050 (192.168.50.122) 前三碼必須完全相同 (192.168.50.x)。

  • 網卡切換建議

    • 若走 Wi-Fi 通訊:電腦已連上與 ADAM-6050 相同的 Wi-Fi 路由器,網路設定已正確。

    • 若用網線直連 PC 乙太網路卡:需進入「控制台 > 網路和共用中心 > 變更介面卡設定 > 乙太網路 2 > 內容 > IPv4」,將 PC 的實體網卡手動指定 IP(例如 192.168.50.200),避免出現 169.254.x.x (APIPA) 導致無法通訊。

2. Modbus 連線限制與常見卡關問題

  • 獨佔/連線數限制:ADAM-6050 支援的 Modbus TCP 連線數極少。執行 Python 程式前,請務必先關閉 Advantech ADAM Utility,否則 Utility 會佔用 Port 502,導致 Python 提示連線失敗或沒有回應。

  • DI (數位輸入) 接線特性

    • Dry Contact (無電壓接點):訊號線需短路閉合至 GND,邏輯電位才會轉變。

    • Wet Contact (有電壓接點):高電位 (ON) 需輸入 +10V ~ 30VDC 電壓;低電位 (OFF) 為 0 ~ 3VDC

二、 Python 程式內容與架構說明

程式包含三大核心層次:

1. 介面與切換 (GUI & Application State)

  • 採用 Tkinter 打造雙畫面架構:

    • 登入畫面 (Login Frame):驗證 Username (Admin) / Password (Admin),並提供動態修改 ADAM IP 與 Port 的輸入欄位。

    • 控制畫面 (Control Frame):顯示 12 組 DI 狀態燈、6 組 DO 狀態燈、全開/全關核取方塊(All Channels Output)、控制按鈕及退出的選項(Back to Login / Exit Application)。

2. 背景多線程輪詢 (Threading Polling Loop)

  • 避免 GUI 卡死:使用 threading.Thread 在背景開啟獨立線程,每隔 0.5 秒向 ADAM-6050 發送 Modbus 讀取請求。

  • 跨線程更新 UI:背景線程讀取到數據後,透過 self.after(0, self.update_gui_indicators) 將點燈與計數任務交還主 UI 線程執行,確保界面不會無回應。

3. Modbus TCP 映射與通訊協定 (Modbus Protocol & Mapping)

研華 ADAM-6050 的 Modbus Register 映射規則:

  • DI 0 ~ 11 (共 12 通道)

    • 使用 Modbus Function Code 02 (read_discrete_inputs) 或 Function Code 01 (read_coils)。

    • Modbus 位址:從 0 (對應 00001) 開始連續讀取 12 個 Bit。

  • DO 0 ~ 5 (共 6 通道)

    • 使用 Modbus Function Code 01 (read_coils) 與 Function Code 15 (write_coils)。

    • Modbus 位址:ADAM-6050 的 DO 在 Modbus 中有 16 位址偏移 (Offset 16),對應 00017~00022。讀取與寫入時傳遞的 address 必須帶入 16

import threading
import time
import tkinter as tk
from tkinter import messagebox

try:
    from pymodbus.client import ModbusTcpClient
except ImportError:
    ModbusTcpClient = None


class ADAM6050App(tk.Tk):

    def __init__(self):
        super().__init__()
        self.title("ADAM-6050 Modbus TCP Controller")
        self.geometry("750x700")
        self.configure(bg="#D4E6F1")

        # Modbus 設定
        self.host = "192.168.50.122"
        self.port = 502
        self.slave_id = 1
        self.client = None
        self.polling = False

        # 輪詢與錯誤計數
        self.poll_count = 0
        self.di_err_count = 0
        self.do_err_count = 0

        # 通道狀態 (12 個 DI, 6 個 DO)
        self.di_states = [False] * 12
        self.do_states = [False] * 6
        self.do_vars = [tk.BooleanVar(value=False) for _ in range(6)]
        self.all_channels_var = tk.BooleanVar(value=False)

        # 顯示登入畫面
        self.show_login_frame()

    # ================= 1. 登入畫面 =================
    def show_login_frame(self):
        self.clear_window()

        self.login_frame = tk.Frame(self, bg="#D4E6F1")
        self.login_frame.pack(expand=True)

        canvas = tk.Canvas(
            self.login_frame,
            width=90,
            height=90,
            bg="#D4E6F1",
            highlightthickness=0,
        )
        canvas.pack(pady=10)
        canvas.create_oval(5, 5, 85, 85, outline="#1B4F72", width=3)
        canvas.create_oval(30, 20, 60, 50, fill="#1B4F72", outline="")
        canvas.create_arc(
            15, 45, 75, 90, start=0, extent=180, fill="#1B4F72", outline=""
        )

        tk.Label(
            self.login_frame,
            text="ADAM-6050 IP",
            font=("Arial", 10, "bold"),
            bg="#D4E6F1",
        ).pack(anchor="w", padx=20)
        self.ip_entry = tk.Entry(
            self.login_frame, font=("Arial", 11), width=25, bd=2, relief="groove"
        )
        self.ip_entry.insert(0, self.host)
        self.ip_entry.pack(pady=(0, 8), padx=20)

        tk.Label(
            self.login_frame, text="Port", font=("Arial", 10, "bold"), bg="#D4E6F1"
        ).pack(anchor="w", padx=20)
        self.port_entry = tk.Entry(
            self.login_frame, font=("Arial", 11), width=25, bd=2, relief="groove"
        )
        self.port_entry.insert(0, str(self.port))
        self.port_entry.pack(pady=(0, 8), padx=20)

        tk.Label(
            self.login_frame, text="Username", font=("Arial", 10), bg="#D4E6F1"
        ).pack(anchor="w", padx=20)
        self.user_entry = tk.Entry(
            self.login_frame, font=("Arial", 11), width=25, bd=2, relief="groove"
        )
        self.user_entry.insert(0, "Admin")
        self.user_entry.pack(pady=(0, 8), padx=20)

        tk.Label(
            self.login_frame, text="Password", font=("Arial", 10), bg="#D4E6F1"
        ).pack(anchor="w", padx=20)
        self.pass_entry = tk.Entry(
            self.login_frame,
            font=("Arial", 11),
            show="*",
            width=25,
            bd=2,
            relief="groove",
        )
        self.pass_entry.insert(0, "Admin")
        self.pass_entry.pack(pady=(0, 15), padx=20)

        login_btn = tk.Button(
            self.login_frame,
            text="LOGIN",
            font=("Arial", 11, "bold"),
            bg="#1B4F72",
            fg="white",
            width=20,
            command=self.verify_login,
        )
        login_btn.pack(pady=5)

    def verify_login(self):
        username = self.user_entry.get().strip()
        password = self.pass_entry.get().strip()
        ip = self.ip_entry.get().strip()
        try:
            port = int(self.port_entry.get().strip())
        except ValueError:
            messagebox.showerror("Error", "Port 必須為數字!")
            return

        if username == "Admin" and password == "Admin":
            self.host = ip
            self.port = port

            if self.connect_modbus():
                self.show_control_frame()
            else:
                messagebox.showerror(
                    "Connection Error",
                    f"無法連線至 ADAM-6050 ({self.host}:{self.port})",
                )
        else:
            messagebox.showerror(
                "Error", "帳號或密碼錯誤!\n預設:Admin / Admin"
            )

    # ================= 2. ADAM-6050 控制畫面 =================
    def show_control_frame(self):
        self.clear_window()

        self.ctrl_frame = tk.Frame(self, bg="#C5D9E8", bd=2, relief="ridge")
        self.ctrl_frame.pack(fill="both", expand=True, padx=15, pady=15)

        # 1. DI Section
        di_header = tk.Label(
            self.ctrl_frame,
            text="Digital Input Values",
            font=("Arial", 12, "bold"),
            bg="#E67E22",
            fg="black",
        )
        di_header.pack(fill="x")

        self.di_status_lbl = tk.Label(
            self.ctrl_frame,
            text="Polling 0 times... DiErr2:0!",
            font=("Arial", 10),
            bg="#A9CCE3",
        )
        self.di_status_lbl.pack(fill="x")

        di_grid = tk.Frame(self.ctrl_frame, bg="#A9CCE3", bd=1, relief="solid")
        di_grid.pack(fill="x", padx=5, pady=5)

        self.di_canvas_list = []
        for i in range(12):
            col_frame = tk.Frame(
                di_grid, bg="#A9CCE3", bd=1, relief="solid", width=55, height=65
            )
            col_frame.grid(row=0, column=i, sticky="nsew", padx=1, pady=1)
            col_frame.grid_propagate(False)

            tk.Label(
                col_frame,
                text=f"DI\n{i}",
                font=("Arial", 9, "bold"),
                bg="#A9CCE3",
            ).pack()
            cv = tk.Canvas(
                col_frame,
                width=24,
                height=24,
                bg="#A9CCE3",
                highlightthickness=0,
            )
            cv.pack(pady=2)
            bulb = cv.create_oval(2, 2, 22, 22, fill="#BDC3C7", outline="#7F8C8D")
            self.di_canvas_list.append((cv, bulb))

        # 2. DO Read Section
        do_header = tk.Label(
            self.ctrl_frame,
            text="Digital Output Values",
            font=("Arial", 12, "bold"),
            bg="#E74C3C",
            fg="black",
        )
        do_header.pack(fill="x", pady=(10, 0))

        self.do_status_lbl = tk.Label(
            self.ctrl_frame,
            text="Polling 0 times... DoErr2:0!",
            font=("Arial", 10),
            bg="#A9CCE3",
        )
        self.do_status_lbl.pack(fill="x")

        do_grid = tk.Frame(self.ctrl_frame, bg="#A9CCE3", bd=1, relief="solid")
        do_grid.pack(fill="x", padx=5, pady=5)

        self.do_canvas_list = []
        for i in range(12):
            col_frame = tk.Frame(
                do_grid, bg="#A9CCE3", bd=1, relief="solid", width=55, height=65
            )
            col_frame.grid(row=0, column=i, sticky="nsew", padx=1, pady=1)
            col_frame.grid_propagate(False)

            if i < 6:
                tk.Label(
                    col_frame,
                    text=f"DO\n{i}",
                    font=("Arial", 9, "bold"),
                    bg="#A9CCE3",
                ).pack()
                cv = tk.Canvas(
                    col_frame,
                    width=24,
                    height=24,
                    bg="#A9CCE3",
                    highlightthickness=0,
                )
                cv.pack(pady=2)
                bulb = cv.create_oval(
                    2, 2, 22, 22, fill="#BDC3C7", outline="#7F8C8D"
                )
                self.do_canvas_list.append((cv, bulb))

        # 3. Output Control Section
        ctrl_header = tk.Frame(self.ctrl_frame, bg="#E74C3C")
        ctrl_header.pack(fill="x", pady=(10, 0))

        tk.Label(
            ctrl_header,
            text="All Channels Output:",
            font=("Arial", 11, "bold"),
            bg="#E74C3C",
            fg="white",
        ).pack(side="left", padx=10)
        chk_all = tk.Checkbutton(
            ctrl_header,
            variable=self.all_channels_var,
            bg="#E74C3C",
            activebackground="#E74C3C",
            command=self.toggle_all_channels,
        )
        chk_all.pack(side="left")

        do_ctrl_grid = tk.Frame(
            self.ctrl_frame, bg="#A9CCE3", bd=1, relief="solid"
        )
        do_ctrl_grid.pack(fill="x", padx=5, pady=5)

        for i in range(12):
            col_frame = tk.Frame(
                do_ctrl_grid,
                bg="#A9CCE3",
                bd=1,
                relief="solid",
                width=55,
                height=65,
            )
            col_frame.grid(row=0, column=i, sticky="nsew", padx=1, pady=1)
            col_frame.grid_propagate(False)

            if i < 6:
                chk = tk.Checkbutton(
                    col_frame, variable=self.do_vars[i], bg="#A9CCE3"
                )
                chk.pack()
                tk.Label(
                    col_frame,
                    text=f"DO\n{i}",
                    font=("Arial", 9, "bold"),
                    bg="#A9CCE3",
                ).pack()

        apply_btn = tk.Button(
            self.ctrl_frame,
            text="Apply Output",
            font=("Arial", 12, "bold"),
            bg="#E0E0E0",
            command=self.apply_output,
        )
        apply_btn.pack(pady=15, ipadx=20, ipady=5)

        nav_frame = tk.Frame(self.ctrl_frame, bg="#C5D9E8")
        nav_frame.pack(pady=5)

        back_btn = tk.Button(
            nav_frame,
            text="Back to Login",
            font=("Arial", 10, "underline"),
            fg="blue",
            bg="#C5D9E8",
            bd=0,
            cursor="hand2",
            command=self.logout,
        )
        back_btn.pack(side="left", padx=15)

        exit_btn = tk.Button(
            nav_frame,
            text="Exit Application",
            font=("Arial", 10, "bold"),
            fg="red",
            bg="#C5D9E8",
            bd=0,
            cursor="hand2",
            command=self.on_closing,
        )
        exit_btn.pack(side="left", padx=15)

    # ================= 3. MODBUS 修正邏輯 =================
    def connect_modbus(self):
        if ModbusTcpClient is None:
            return False
        try:
            self.client = ModbusTcpClient(
                host=self.host, port=self.port, timeout=2.0
            )
            if self.client.connect():
                self.polling = True
                self.poll_thread = threading.Thread(
                    target=self.poll_modbus_loop, daemon=True
                )
                self.poll_thread.start()
                return True
            return False
        except Exception:
            return False

    def _modbus_read_di(self):
        """相容不同 pymodbus 版本的 DI 讀取"""
        try:
            return self.client.read_discrete_inputs(
                address=0, count=12, slave=self.slave_id
            )
        except TypeError:
            try:
                return self.client.read_discrete_inputs(address=0, count=12)
            except Exception:
                return self.client.read_coils(address=0, count=12)

    def _modbus_read_do(self):
        """相容不同 pymodbus 版本的 DO 讀取 (ADAM-6050 DO 位址為 16)"""
        try:
            return self.client.read_coils(
                address=16, count=6, slave=self.slave_id
            )
        except TypeError:
            return self.client.read_coils(address=16, count=6)

    def _modbus_write_do(self, values):
        """相容不同 pymodbus 版本的 DO 寫入 (ADAM-6050 DO 位址為 16)"""
        try:
            return self.client.write_coils(
                address=16, values=values, slave=self.slave_id
            )
        except TypeError:
            return self.client.write_coils(address=16, values=values)

    def poll_modbus_loop(self):
        while self.polling:
            self.poll_count += 1

            if self.client and self.client.is_socket_open():
                try:
                    # 1. 讀取 DI
                    rr_di = self._modbus_read_di()
                    if rr_di and not rr_di.isError():
                        self.di_states = rr_di.bits[:12]
                    else:
                        self.di_err_count += 1

                    # 2. 讀取 DO
                    rr_do = self._modbus_read_do()
                    if rr_do and not rr_do.isError():
                        self.do_states = rr_do.bits[:6]
                    else:
                        self.do_err_count += 1

                except Exception:
                    self.di_err_count += 1
                    self.do_err_count += 1
            else:
                self.di_err_count += 1
                self.do_err_count += 1

            self.after(0, self.update_gui_indicators)
            time.sleep(0.5)

    def update_gui_indicators(self):
        if not hasattr(self, "di_status_lbl"):
            return

        self.di_status_lbl.config(
            text=f"Polling {self.poll_count} times... DiErr2:{self.di_err_count}!"
        )
        self.do_status_lbl.config(
            text=f"Polling {self.poll_count} times... DoErr2:{self.do_err_count}!"
        )

        for i, (cv, bulb) in enumerate(self.di_canvas_list):
            color = "#F1C40F" if self.di_states[i] else "#BDC3C7"
            cv.itemconfig(bulb, fill=color)

        for i, (cv, bulb) in enumerate(self.do_canvas_list):
            color = "#F1C40F" if self.do_states[i] else "#BDC3C7"
            cv.itemconfig(bulb, fill=color)

    def toggle_all_channels(self):
        state = self.all_channels_var.get()
        for var in self.do_vars:
            var.set(state)

    def apply_output(self):
        values = [var.get() for var in self.do_vars]
        if self.client and self.client.is_socket_open():
            try:
                res = self._modbus_write_do(values)
                if res and not res.isError():
                    messagebox.showinfo(
                        "Success", f"成功寫入 DO 狀態:\n{values}"
                    )
                else:
                    messagebox.showerror("Modbus Error", "寫入 DO 失敗!")
            except Exception as e:
                messagebox.showerror(
                    "Modbus Error", f"控制 ADAM-6050 發生錯誤:\n{e}"
                )
        else:
            messagebox.showwarning("Warning", "目前未連線至 ADAM-6050 設備!")

    def logout(self):
        self.polling = False
        if self.client:
            self.client.close()
        self.show_login_frame()

    def clear_window(self):
        for widget in self.winfo_children():
            widget.destroy()

    def on_closing(self):
        self.polling = False
        if self.client:
            self.client.close()
        self.destroy()


if __name__ == "__main__":
    app = ADAM6050App()
    app.protocol("WM_DELETE_WINDOW", app.on_closing)
    app.mainloop()

沒有留言:

張貼留言

EasyBuilder Pro 人機介面 HMI 編輯軟體)透過 Modbus TCP 控制研華 ADAM-6050

EasyBuilder Pro 人機介面 HMI 編輯軟體)透過 Modbus TCP 控制研華 ADAM-6050  EasyBuilder Pro(威綸通 Weintek 人機介面 HMI 編輯軟體)透過 Modbus TCP 控制研華 ADAM-6050 的完整設定步驟...