Adam DO*5 DI*12 + Python + Node-Red
為了讓您的系統穩定運行,建議的最終接線如下:
| 功能 | ADAM 端子 | 對接目標 |
| DI 輸入 (乾接點) | DI0 ~ DI11 | 開關的一端 |
| DI 共同端 | DI.COM | 開關的另一端 (接 GND) |
| DO 輸出 | DO0 ~ DO5 | 負載 (如繼電器、指示燈) |
| DO 共同端 | DO.COM | 外部電源的+極 (Open collector) |
Python 程式
import tkinter as tk
from tkinter import messagebox
from pyModbusTCP.client import ModbusClient
import paho.mqtt.client as mqtt
import json
import threading
import time
# --- Topic 定義 (兩者皆為發布) ---
TOPIC_DI_PUBLISH = "alex9ufo/adam6050/input" # 程式發行 DI 狀態
TOPIC_DO_PUBLISH = "alex9ufo/adam6050/output" # 程式發行 DO 狀態
PUBLISH_INTERVAL = 5 # 每 5 秒強制發行一次
class Adam6050DualPublishHmi:
def __init__(self, root):
self.root = root
self.root.title("ADAM-6050 雙向發行控制面板 (alex9ufo)")
self.root.geometry("400x450")
# 1. 通訊初始化
self.modbus_client = ModbusClient(port=502, timeout=2.0, auto_open=False)
try:
self.mqtt_client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1)
except:
self.mqtt_client = mqtt.Client()
self.mqtt_client.on_connect = self.on_mqtt_connect
self.mqtt_client.on_disconnect = self.on_mqtt_disconnect
# 2. 狀態與緩存
self.modbus_active = False
self.mqtt_active = False
self.last_di_data = [] # 用於偵測 DI 變化
self.last_pub_time = 0 # 上次發行時間
self.di_labels = []
self.do_vars = []
self.setup_ui()
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def setup_ui(self):
# --- 連線設定區域 ---
cfg_frame = tk.LabelFrame(self.root, text="通訊設定", padx=10, pady=10)
cfg_frame.pack(padx=15, pady=10, fill="x")
# Adam IP
tk.Label(cfg_frame, text="Adam IP:").grid(row=0, column=0)
self.adam_ip_entry = tk.Entry(cfg_frame, width=15)
self.adam_ip_entry.insert(0, "192.168.50.122")
self.adam_ip_entry.grid(row=0, column=1)
self.adam_btn = tk.Button(cfg_frame, text="連線 Adam", command=self.toggle_adam, width=10)
self.adam_btn.grid(row=0, column=2, padx=5)
self.adam_led = tk.Canvas(cfg_frame, width=15, height=15, highlightthickness=0)
self.adam_led.grid(row=0, column=3)
self.adam_circle = self.adam_led.create_oval(2,2,13,13, fill="gray")
# MQTT Broker
tk.Label(cfg_frame, text="Broker:").grid(row=1, column=0)
self.mqtt_entry = tk.Entry(cfg_frame, width=15)
self.mqtt_entry.insert(0, "broker.mqtt-dashboard.com")
self.mqtt_entry.grid(row=1, column=1)
self.mqtt_btn = tk.Button(cfg_frame, text="連線 MQTT", command=self.toggle_mqtt, width=10)
self.mqtt_btn.grid(row=1, column=2, padx=5)
self.mqtt_led = tk.Canvas(cfg_frame, width=15, height=15, highlightthickness=0)
self.mqtt_led.grid(row=1, column=3)
self.mqtt_circle = self.mqtt_led.create_oval(2,2,13,13, fill="gray")
# --- DI 顯示區域 ---
di_frame = tk.LabelFrame(self.root, text="DI 狀態 (乾接點: DI 短路至 GND 觸發)", padx=10, pady=10)
di_frame.pack(padx=15, pady=5, fill="both", expand=True)
for i in range(12):
lbl = tk.Label(di_frame, text=f"DI_{i:02d}", width=8, height=2, bg="#333333", fg="white", font=("Arial", 9, "bold"))
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 控制 (點擊即發行至 /output)", padx=10, pady=10)
do_frame.pack(padx=15, pady=5, fill="both", expand=True)
for i in range(6):
var = tk.BooleanVar()
# 當畫面上勾選框改變時,執行 publish_do_and_sync
chk = tk.Checkbutton(do_frame, text=f"DO_{i:02d}", variable=var, command=self.publish_do_and_sync)
chk.grid(row=i // 3, column=i % 3, padx=15, pady=10)
self.do_vars.append(var)
# --- Adam 控制邏輯 ---
def toggle_adam(self):
if not self.modbus_active:
self.modbus_client.host = self.adam_ip_entry.get().strip()
if self.modbus_client.open():
self.modbus_active = True
self.adam_btn.config(text="斷開 Adam", bg="#ffcccc")
threading.Thread(target=self.adam_polling, daemon=True).start()
else:
messagebox.showerror("錯誤", "無法連線到 ADAM-6050")
else:
self.modbus_active = False
self.modbus_client.close()
self.adam_btn.config(text="連線 Adam", bg="#f0f0f0")
self.set_led(self.adam_led, self.adam_circle, "gray")
def adam_polling(self):
"""偵測實體 DI 變化並發行"""
while self.modbus_active:
di_data = self.modbus_client.read_discrete_inputs(0, 12)
if di_data is not None:
self.set_led(self.adam_led, self.adam_circle, "#00FF00")
# UI 更新顏色
for i, val in enumerate(di_data):
self.di_labels[i].config(bg="#4CAF50" if val else "#333333")
# 偵測 DI 變化,若有變則立即發行
if di_data != self.last_di_data:
self.publish_status(TOPIC_DI_PUBLISH, "DI_STATUS", "di_all", di_data)
self.last_di_data = di_data
else:
self.set_led(self.adam_led, self.adam_circle, "#F44336")
time.sleep(0.5)
# --- MQTT 邏輯 ---
def toggle_mqtt(self):
if not self.mqtt_active:
try:
self.mqtt_client.connect(self.mqtt_entry.get().strip(), 1883, 60)
self.mqtt_client.loop_start()
# 啟動定時發行執行緒 (5秒一次)
threading.Thread(target=self.publish_timer, daemon=True).start()
except:
messagebox.showerror("錯誤", "無法連線到 MQTT Broker")
else:
self.mqtt_active = False
self.mqtt_client.loop_stop()
def publish_timer(self):
"""每 5 秒強制發行一次 DI 與 DO 的全狀態"""
while self.mqtt_active:
# 強制發行 DI
if self.last_di_data:
self.publish_status(TOPIC_DI_PUBLISH, "DI_SNAPSHOT", "di_all", self.last_di_data)
# 強制發行 DO
do_states = [var.get() for var in self.do_vars]
self.publish_status(TOPIC_DO_PUBLISH, "DO_SNAPSHOT", "do_all", do_states)
time.sleep(PUBLISH_INTERVAL)
def publish_do_and_sync(self):
"""當使用者操作 DO 時,同步實體設備並發行訊息"""
do_states = [var.get() for var in self.do_vars]
# 1. 實體 Modbus 控制
if self.modbus_active and self.modbus_client.is_open:
self.modbus_client.write_multiple_coils(16, do_states)
# 2. 發行到 alex9ufo/adam6050/output
self.publish_status(TOPIC_DO_PUBLISH, "DO_CHANGE", "do_all", do_states)
def publish_status(self, topic, msg_type, key_name, data_list):
"""通用的發行 JSON 函式"""
if self.mqtt_active:
payload = {
"adam_ip": self.adam_ip_entry.get().strip(),
"type": msg_type,
key_name: data_list,
"timestamp": time.strftime("%H:%M:%S")
}
self.mqtt_client.publish(topic, json.dumps(payload), qos=0)
print(f"Published to {topic}: {msg_type}")
# --- 通用處理 ---
def on_mqtt_connect(self, client, userdata, flags, rc):
if rc == 0:
self.mqtt_active = True
self.mqtt_btn.config(text="斷開 MQTT", bg="#ffcccc")
self.set_led(self.mqtt_led, self.mqtt_circle, "#00FF00")
else:
self.set_led(self.mqtt_led, self.mqtt_circle, "red")
def on_mqtt_disconnect(self, client, userdata, rc):
self.mqtt_active = False
self.mqtt_btn.config(text="連線 MQTT", bg="#f0f0f0")
self.set_led(self.mqtt_led, self.mqtt_circle, "gray")
def set_led(self, canvas, item, color):
canvas.itemconfig(item, fill=color)
def on_closing(self):
self.modbus_active = False
self.mqtt_active = False
self.modbus_client.close()
self.mqtt_client.disconnect()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = Adam6050DualPublishHmi(root)
root.mainloop()
Node-RED 邏輯流程設計
我們需要兩個核心邏輯:
DI 監測流:訂閱 .../input -> 解析 di_all 陣列 -> 分送至 12 個 LED。
DO 監測流:訂閱 .../output -> 解析 do_all 陣列 -> 分送至 6 個 LED。
Function 節點 (解析 DI):
使用一個 Function 節點將陣列拆分成多個輸出訊息。
UI LED 節點:
Function 節點 (解析 DO):
Node-Red程式
[{"id":"f1","type":"mqtt in","z":"5112f7572fa1920f","name":"adam6050/input","topic":"alex9ufo/adam6050/input","qos":"0","datatype":"auto","broker":"b9efc827e98bf7f9","nl":false,"rap":false,"inputs":0,"x":140,"y":140,"wires":[["f2"]]},{"id":"f2","type":"json","z":"5112f7572fa1920f","x":290,"y":140,"wires":[["f3","3a208bf522b5745c"]]},{"id":"f3","type":"function","z":"5112f7572fa1920f","name":"Split DI","func":"var di = msg.payload.di_all;\nvar msgs = [];\nfor(var i=0; i<12; i++){\n msgs.push({payload: di[i]});\n}\nreturn msgs;","outputs":12,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":440,"y":200,"wires":[["580305addee8e4c4"],["0db1a33ad8570805"],["a772dfc7c16ec169"],["8eddb5983ba4512c"],["8d93861f2f8ee252"],["88ec282010d86b05"],["72ab28bf459d00b2"],["ae9711eb3d65157e"],["1c95b913b6b10d7f"],["2807ee7c6bf9a8c6"],["3aa463e17a07dec5"],["33d0ba8d8a258180"]]},{"id":"580305addee8e4c4","type":"ui_led","z":"5112f7572fa1920f","order":1,"group":"f6558dccd7e411fa","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED00","x":650,"y":20,"wires":[]},{"id":"0db1a33ad8570805","type":"ui_led","z":"5112f7572fa1920f","order":2,"group":"f6558dccd7e411fa","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED01","x":650,"y":60,"wires":[]},{"id":"a772dfc7c16ec169","type":"ui_led","z":"5112f7572fa1920f","order":3,"group":"f6558dccd7e411fa","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED02","x":650,"y":100,"wires":[]},{"id":"8eddb5983ba4512c","type":"ui_led","z":"5112f7572fa1920f","order":4,"group":"f6558dccd7e411fa","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED03","x":650,"y":140,"wires":[]},{"id":"8d93861f2f8ee252","type":"ui_led","z":"5112f7572fa1920f","order":5,"group":"f6558dccd7e411fa","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED04","x":650,"y":180,"wires":[]},{"id":"88ec282010d86b05","type":"ui_led","z":"5112f7572fa1920f","order":6,"group":"f6558dccd7e411fa","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED05","x":650,"y":220,"wires":[]},{"id":"72ab28bf459d00b2","type":"ui_led","z":"5112f7572fa1920f","order":7,"group":"f6558dccd7e411fa","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED06","x":650,"y":260,"wires":[]},{"id":"ae9711eb3d65157e","type":"ui_led","z":"5112f7572fa1920f","order":8,"group":"f6558dccd7e411fa","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED07","x":650,"y":300,"wires":[]},{"id":"1c95b913b6b10d7f","type":"ui_led","z":"5112f7572fa1920f","order":9,"group":"f6558dccd7e411fa","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED08","x":650,"y":340,"wires":[]},{"id":"2807ee7c6bf9a8c6","type":"ui_led","z":"5112f7572fa1920f","order":10,"group":"f6558dccd7e411fa","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED09","x":650,"y":380,"wires":[]},{"id":"3aa463e17a07dec5","type":"ui_led","z":"5112f7572fa1920f","order":11,"group":"f6558dccd7e411fa","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED10","x":650,"y":420,"wires":[]},{"id":"33d0ba8d8a258180","type":"ui_led","z":"5112f7572fa1920f","order":12,"group":"f6558dccd7e411fa","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED11","x":650,"y":460,"wires":[]},{"id":"3a208bf522b5745c","type":"debug","z":"5112f7572fa1920f","name":"debug 383","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":410,"y":80,"wires":[]},{"id":"ef0830207b85bc22","type":"mqtt in","z":"5112f7572fa1920f","name":"adam6050/output","topic":"alex9ufo/adam6050/output","qos":"0","datatype":"auto","broker":"b9efc827e98bf7f9","nl":false,"rap":false,"inputs":0,"x":80,"y":560,"wires":[["29b887588c6862ec"]]},{"id":"29b887588c6862ec","type":"json","z":"5112f7572fa1920f","name":"","property":"payload","action":"","pretty":false,"x":230,"y":560,"wires":[["58421ec80caef2ce","613da72c26fb942a"]]},{"id":"58421ec80caef2ce","type":"function","z":"5112f7572fa1920f","name":"Split DI","func":"var do_list = msg.payload.do_all;\nvar messages = [];\nfor (var i = 0; i < 6; i++) {\n messages.push({ payload: do_list[i] });\n}\nreturn messages;","outputs":6,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":600,"wires":[["f283f70aa178403f"],["2838286b60cdb106"],["70c88c0f57063ea1"],["f5f1663737ebfde1"],["c57c3e353758a3da"],["4c3c827f598ab970"]]},{"id":"f283f70aa178403f","type":"ui_led","z":"5112f7572fa1920f","order":1,"group":"bcf9500e1cee76c6","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED_O00","x":540,"y":500,"wires":[]},{"id":"2838286b60cdb106","type":"ui_led","z":"5112f7572fa1920f","order":2,"group":"bcf9500e1cee76c6","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED_O01","x":540,"y":540,"wires":[]},{"id":"70c88c0f57063ea1","type":"ui_led","z":"5112f7572fa1920f","order":3,"group":"bcf9500e1cee76c6","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED_O02","x":540,"y":580,"wires":[]},{"id":"f5f1663737ebfde1","type":"ui_led","z":"5112f7572fa1920f","order":4,"group":"bcf9500e1cee76c6","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED_O03","x":540,"y":620,"wires":[]},{"id":"c57c3e353758a3da","type":"ui_led","z":"5112f7572fa1920f","order":5,"group":"bcf9500e1cee76c6","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED_O04","x":540,"y":660,"wires":[]},{"id":"4c3c827f598ab970","type":"ui_led","z":"5112f7572fa1920f","order":6,"group":"bcf9500e1cee76c6","width":2,"height":1,"label":"","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED_O05","x":540,"y":700,"wires":[]},{"id":"613da72c26fb942a","type":"debug","z":"5112f7572fa1920f","name":"debug 384","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":350,"y":440,"wires":[]},{"id":"b9efc827e98bf7f9","type":"mqtt-broker","name":"broker.mqtt-dashboard.com","broker":"broker.mqtt-dashboard.com","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"f6558dccd7e411fa","type":"ui_group","name":"in","tab":"83f1aa7afc1a6d1f","order":1,"disp":true,"width":8,"collapse":false,"className":""},{"id":"bcf9500e1cee76c6","type":"ui_group","name":"out","tab":"83f1aa7afc1a6d1f","order":2,"disp":true,"width":"6","collapse":false,"className":""},{"id":"83f1aa7afc1a6d1f","type":"ui_tab","name":"Adam6050","icon":"dashboard","disabled":false,"hidden":false}]
沒有留言:
張貼留言