2026年4月25日 星期六

Adam DO*5 DI*12 + Python + Node-Red

 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 邏輯流程設計

我們需要兩個核心邏輯:

  1. DI 監測流:訂閱 .../input -> 解析 di_all 陣列 -> 分送至 12 個 LED。

  2. DO 監測流:訂閱 .../output -> 解析 do_all 陣列 -> 分送至 6 個 LED。

  • Function 節點 (解析 DI): 使用一個 Function 節點將陣列拆分成多個輸出訊息。

    JavaScript
    // 將 di_all 陣列內容分別傳送到 12 個輸出埠
    var di = msg.payload.di_all;
    var messages = [];
    for (var i = 0; i < 12; i++) {
        messages.push({ payload: di[i] });
    }
    return messages;
    
    • 重要設定:在該 Function 節點的設定下方,將 Outputs 數量改為 12

  • UI LED 節點

    • 拖入 12 個 ui_led 節點。

    • 將 Function 節點的 12 個輸出點分別連線到這 12 個 LED。

    • 設定 LED:顏色(ON: green, OFF: gray)。

  • Function 節點 (解析 DO)

    JavaScript
    var do_list = msg.payload.do_all;
    var messages = [];
    for (var i = 0; i < 6; i++) {
        messages.push({ payload: do_list[i] });
    }
    return messages;
    
    • Outputs 數量改為 6

    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}]

    沒有留言:

    張貼留言

    Adam DO*5 DI*12 + Python + Node-Red

     Adam DO*5 DI*12 + Python + Node-Red 為了讓您的系統穩定運行,建議的最終接線如下: 功能 ADAM 端子 對接目標 DI 輸入 (乾接點) DI0 ~ DI11 開關的一端 DI 共同端 DI.COM 開關的另一端 (接 GND) DO 輸出...