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()
沒有留言:
張貼留言