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

    2026年4月22日 星期三

    ESP32 and Node-RED with MQTT (Publish and Subscribe)

     

    ESP32 and Node-RED with MQTT (Publish and Subscribe)

    參考來源  https://randomnerdtutorials.com/esp8266-and-node-red-with-mqtt/



    mqtt_server = "broker.mqtt-dashboard.com"

    client.publish("alex9ufo/Thinkitive/temp"

    client.publish("alex9ufo/Thinkitive/hum"

    client.subscribe("alex9ufo/room/lamp");








    WOKWI ESP32程式

    #include <WiFi.h>  
    #include <PubSubClient.h>
    #include <DHTesp.h>

    // Uncomment one of the lines bellow for whatever DHT sensor type you're using!
    #define DHTTYPE DHT22   // DHT 22  (AM2302), AM2321
    const int DHT_PIN = 15;  // DHT 感測器的連接 PIN
    const int lamp = 4;
    DHTesp dht;
    const char* ssid = "Wokwi-GUEST"; // WiFi SSID
    const char* password = ""; // WiFi 密碼
    const char* mqtt_server = "broker.mqtt-dashboard.com"; // MQTT 服務器地址

    WiFiClient espClient;
    PubSubClient client(espClient);
    unsigned long lastMsg = 0; // 記錄上一次消息發送的時間
    float temp = 0; // 溫度變量
    float hum = 0; // 濕度變量

    // 設置 WiFi 連接
    void setup_wifi() {
      delay(10);
      Serial.println();
      Serial.print("Connecting to ");
      Serial.println(ssid);

      WiFi.begin(ssid, password);

      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
      }

      Serial.println("");
      Serial.println("WiFi connected");
      Serial.println("IP address: ");
      Serial.println(WiFi.localIP());
    }

    // MQTT 消息回調函數
    // This function is executed when some device publishes a message to a topic that your ESP8266 is subscribed to
    // Change the function below to add logic to your program, so when a device publishes a message to a topic that
    // your ESP8266 is subscribed you can actually do something
    //==============================================================
    void callback(char* topic, byte* payload, unsigned int length) {
      payload[length] = '\0';
      String message = String((char*)payload);
      Serial.print("Message arrived in topic: ");
      Serial.println(topic);
      Serial.print("Message: ");
      Serial.println( message);

      if (String(topic) == "alex9ufo/room/lamp") {

      if (message == "on")
        digitalWrite(lamp, HIGH); // LOW to activate

      if (message == "off")
        digitalWrite(lamp, LOW); // LOW to activate
      Serial.println();
      Serial.println("-----------------------");
      }
    }
    //==============================================================

    // This functions reconnects your ESP8266 to your MQTT broker
    // Change the function below if you want to subscribe to more topics with your ESP8266

    // 重連 MQTT 服務器
    void reconnect() {
      while (!client.connected()) {
        Serial.print("Attempting MQTT connection...");
        String clientId = "ESP32Client-";
        clientId += String(random(0xffff), HEX);
        if (client.connect(clientId.c_str())) {
          Serial.println("Connected");
         
          // 關鍵修正:必須訂閱你想要接收的主題
          client.subscribe("alex9ufo/room/lamp");
         
          // 這是您原本訂閱的主題(如果沒用到可以移除)
          client.subscribe("alex9ufo/ThinkIOT/Subscribe");
         
          client.publish("alex9ufo/ThinkIOT/Publish", "Welcome");
        } else {
          Serial.print("failed, rc=");
          Serial.print(client.state());
          Serial.println(" try again in 5 seconds");
          delay(5000);
        }
      }
    }

    // 初始化設置
    void setup() {
      pinMode(lamp, OUTPUT);
      Serial.begin(115200);
      setup_wifi();
      client.setServer(mqtt_server, 1883);
      client.setCallback(callback);
      dht.setup(DHT_PIN, DHTesp::DHT22); // 初始化 DHT22
    }

    // 主循環
    void loop() {
      // 如果未連接 MQTT,則嘗試重新連接
      if (!client.connected()) {
        reconnect();
      }
      client.loop();

      unsigned long now = millis();
      if (now - lastMsg > 5000) { // 每 2 秒發布一次數據
        lastMsg = now;
        TempAndHumidity  data = dht.getTempAndHumidity();

        String temp = String(data.temperature, 2);
        client.publish("alex9ufo/Thinkitive/temp", temp.c_str()); // 發布溫度數據
        String hum = String(data.humidity, 1);
        client.publish("alex9ufo/Thinkitive/hum", hum.c_str());   // 發布濕度數據

        Serial.print("Temperature: ");
        Serial.println(temp);
        Serial.print("Humidity: ");
        Serial.println(hum);
      }
    }

    Node-Red 程式

    [{"id":"3322eee9576783df","type":"mqtt in","z":"4b78f7e5d13e6604","name":"濕度","topic":"alex9ufo/Thinkitive/hum","qos":"1","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":230,"y":120,"wires":[["c955bb867c3764eb"]]},{"id":"e10e2c7d299e3c2c","type":"mqtt out","z":"4b78f7e5d13e6604","name":"燈泡","topic":"alex9ufo/room/lamp","qos":"1","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"b9efc827e98bf7f9","x":410,"y":180,"wires":[]},{"id":"df3ef8083edb59a3","type":"mqtt in","z":"4b78f7e5d13e6604","name":"溫度","topic":"alex9ufo/Thinkitive/temp","qos":"1","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":230,"y":60,"wires":[["88fc71e9f52b3d9e"]]},{"id":"4619b3053d03f1a8","type":"ui_switch","z":"4b78f7e5d13e6604","name":"","label":"switch","tooltip":"","group":"3ac0cc1879cac657","order":0,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"topic","topicType":"msg","style":"","onvalue":"on","onvalueType":"str","onicon":"","oncolor":"","offvalue":"off","offvalueType":"str","officon":"","offcolor":"","animate":false,"className":"","x":230,"y":180,"wires":[["e10e2c7d299e3c2c"]]},{"id":"88fc71e9f52b3d9e","type":"ui_chart","z":"4b78f7e5d13e6604","name":"溫度","group":"9cb87f6334be2cce","order":1,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"step","nodata":"","dot":false,"ymin":"0","ymax":"40","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":410,"y":60,"wires":[[]]},{"id":"c955bb867c3764eb","type":"ui_gauge","z":"4b78f7e5d13e6604","name":"濕度'","group":"9cb87f6334be2cce","order":2,"width":0,"height":0,"gtype":"gage","title":"Humidity","label":"%","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":410,"y":120,"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":"3ac0cc1879cac657","type":"ui_group","name":"Lamp","tab":"0c1ba72ccee260d2","order":2,"disp":true,"width":"6","collapse":false,"className":""},{"id":"9cb87f6334be2cce","type":"ui_group","name":"Sensor","tab":"0c1ba72ccee260d2","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"0c1ba72ccee260d2","type":"ui_tab","name":"Room","icon":"dashboard","disabled":false,"hidden":false}]

    Node Red MQTT

    Node Red MQTT

    https://hackmd.io/@joe94113/node-red_mqtt_esp32_and_open_api




























    wokwi程式 修改mqtt_server = "broker.mqtt-dashboard.com"; // MQTT 服務器地址

    #include <WiFi.h>  
    #include <PubSubClient.h>
    #include <DHTesp.h>

    const int DHT_PIN = 15;  // DHT 感測器的連接 PIN
    DHTesp dht;
    const char* ssid = "Wokwi-GUEST"; // WiFi SSID
    const char* password = ""; // WiFi 密碼
    const char* mqtt_server = "broker.mqtt-dashboard.com"; // MQTT 服務器地址

    WiFiClient espClient;
    PubSubClient client(espClient);
    unsigned long lastMsg = 0; // 記錄上一次消息發送的時間
    float temp = 0; // 溫度變量
    float hum = 0; // 濕度變量

    // 設置 WiFi 連接
    void setup_wifi() {
      delay(10);
      Serial.println();
      Serial.print("Connecting to ");
      Serial.println(ssid);

      WiFi.mode(WIFI_STA);
      WiFi.begin(ssid, password);

      // 等待 WiFi 連接
      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
      }

      randomSeed(micros());

      Serial.println("");
      Serial.println("WiFi connected");
      Serial.println("IP address: ");
      Serial.println(WiFi.localIP());
    }

    // MQTT 消息回調函數
    void callback(char* topic, byte* payload, unsigned int length) {
      Serial.print("Message arrived [");
      Serial.print(topic);
      Serial.print("] ");
      for (int i = 0; i < length; i++) {
        Serial.print((char)payload[i]);
      }
    }

    // 重連 MQTT 服務器
    void reconnect() {
      while (!client.connected()) {
        Serial.print("Attempting MQTT connection...");
        String clientId = "ESP32Client-";
        clientId += String(random(0xffff), HEX);
        if (client.connect(clientId.c_str())) {
          Serial.println("Connected");
          client.publish("/ThinkIOT/Publish", "Welcome");
          client.subscribe("/ThinkIOT/Subscribe");
        } else {
          Serial.print("failed, rc=");
          Serial.print(client.state());
          Serial.println(" try again in 5 seconds");
          delay(5000);
        }
      }
    }

    // 初始化設置
    void setup() {
      pinMode(2, OUTPUT);    
      Serial.begin(115200);
      setup_wifi();
      client.setServer(mqtt_server, 1883);
      client.setCallback(callback);
      dht.setup(DHT_PIN, DHTesp::DHT22); // 初始化 DHT22
    }

    // 主循環
    void loop() {
      // 如果未連接 MQTT,則嘗試重新連接
      if (!client.connected()) {
        reconnect();
      }
      client.loop();

      unsigned long now = millis();
      if (now - lastMsg > 2000) { // 每 2 秒發布一次數據
        lastMsg = now;
        TempAndHumidity  data = dht.getTempAndHumidity();

        String temp = String(data.temperature, 2);
        client.publish("/Thinkitive/temp", temp.c_str()); // 發布溫度數據
        String hum = String(data.humidity, 1);
        client.publish("/Thinkitive/hum", hum.c_str());   // 發布濕度數據

        Serial.print("Temperature: ");
        Serial.println(temp);
        Serial.print("Humidity: ");
        Serial.println(hum);
      }
    }



    看到這個提示別擔心,這通常是因為瀏覽器基於安全性考量暫時關閉了腳本功能。只要幾個簡單的步驟就能重新開啟。

    以下是針對目前主流瀏覽器的開啟教學:


    1. Google Chrome (電腦版)

    1. 點擊瀏覽器右上角的 「三個點」 圖示,選擇 「設定」

    2. 在左側選單點擊 「隱私權和安全性」

    3. 點擊 「網站設定」

    4. 向下滑動找到「內容」區塊,點擊 「JavaScript」

    5. 勾選 「網站可以使用 JavaScript」

    6. 回到原本的網頁並 重新整理 (F5)


    2. Microsoft Edge

    1. 點擊右上角的 「三個點」 圖示,選擇 「設定」

    2. 在左側選單選擇 「Cookie 和網站權限」

    3. 在「所有權限」清單中找到並點擊 「JavaScript」

    4. 「允許 (建議)」 的開關切換為 開啟狀態

    5. 重新整理網頁。



    Node-RED 中安裝 MQTT broker 的步驟,並可以開始使用 MQTT 進行消息傳輸

    [{"id":"073cac6e5e434d41","type":"mqtt out","z":"de93e29c07cca82f","name":"","topic":"sensors/livingroom/temp","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"841df58d.ee5e98","x":310,"y":180,"wires":[]},{"id":"ceb8068819adc757","type":"inject","z":"de93e29c07cca82f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"","payload":"22","payloadType":"num","x":110,"y":180,"wires":[["073cac6e5e434d41"]]},{"id":"5fc97feb1b566004","type":"mqtt in","z":"de93e29c07cca82f","name":"","topic":"sensors/livingroom/temp","qos":"2","datatype":"auto-detect","broker":"841df58d.ee5e98","nl":false,"rap":false,"inputs":0,"x":150,"y":260,"wires":[["6c6dc58f97ab8f86"]]},{"id":"6c6dc58f97ab8f86","type":"debug","z":"de93e29c07cca82f","name":"","active":true,"console":"false","complete":"false","x":350,"y":260,"wires":[]},{"id":"bcca028dd9cde448","type":"debug","z":"de93e29c07cca82f","name":"debug 19","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":340,"y":40,"wires":[]},{"id":"20f600cc71d280e5","type":"debug","z":"de93e29c07cca82f","name":"debug 20","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":340,"y":100,"wires":[]},{"id":"a4ee27d876e6c883","type":"aedes broker","z":"de93e29c07cca82f","name":"","mqtt_port":"1884","mqtt_ws_bind":"port","mqtt_ws_port":"","mqtt_ws_path":"","cert":"","key":"","ca":"","certname":"","keyname":"","caname":"","persistence_bind":"memory","dburl":"","persist_to_file":false,"usetls":false,"x":130,"y":80,"wires":[["bcca028dd9cde448"],["20f600cc71d280e5"]]},{"id":"841df58d.ee5e98","type":"mqtt-broker","name":"","broker":"broker.hivemq.com","port":"1883","clientid":"","autoConnect":true,"usetls":false,"compatmode":false,"protocolVersion":"4","keepalive":"15","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""}]


    Node-RED 中配置 MQTT 並訂閱 ESP32 發送資料

    [{"id":"06616414d9f0a6f3","type":"aedes broker","z":"3b843e74e7ab6ef8","name":"","mqtt_port":"1884","mqtt_ws_bind":"port","mqtt_ws_port":"","mqtt_ws_path":"","cert":"","key":"","certname":"","keyname":"","dburl":"","usetls":false,"x":130,"y":80,"wires":[["866a2384f6992b64"],["b013bf4fca5a488e"]]},{"id":"866a2384f6992b64","type":"debug","z":"3b843e74e7ab6ef8","name":"debug 19","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":340,"y":40,"wires":[]},{"id":"b013bf4fca5a488e","type":"debug","z":"3b843e74e7ab6ef8","name":"debug 20","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":340,"y":100,"wires":[]},{"id":"ceb3112cd35e970e","type":"mqtt in","z":"3b843e74e7ab6ef8","name":"","topic":"/Thinkitive/temp","qos":"0","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":120,"y":180,"wires":[["7c6bdec976aab81d"]]},{"id":"a51c937face296dc","type":"mqtt in","z":"3b843e74e7ab6ef8","name":"","topic":"/Thinkitive/hum","qos":"0","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":120,"y":240,"wires":[["f06f1d6381b63b69"]]},{"id":"7c6bdec976aab81d","type":"ui_gauge","z":"3b843e74e7ab6ef8","name":"TRMP","group":"70d1d491db52e4fe","order":7,"width":6,"height":4,"gtype":"gage","title":"ESP32溫度","label":"","format":"{{value}}","min":0,"max":"150","colors":["#00b500","#e6e600","#ec2727"],"seg1":"","seg2":"","diff":false,"className":"","x":330,"y":180,"wires":[]},{"id":"f06f1d6381b63b69","type":"ui_gauge","z":"3b843e74e7ab6ef8","name":"","group":"70d1d491db52e4fe","order":12,"width":6,"height":4,"gtype":"gage","title":"ESP32濕度","label":"","format":"{{value}}","min":0,"max":"60","colors":["#15cb15","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":350,"y":240,"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":"70d1d491db52e4fe","type":"ui_group","name":"esp32圖表","tab":"2156c1cf7f043d72","order":2,"disp":true,"width":"6","collapse":false,"className":""},{"id":"2156c1cf7f043d72","type":"ui_tab","name":"天氣圖表","icon":"dashboard","disabled":false,"hidden":false}]

    Node-RED 中連接到一個公開的氣象資料 API,並抓取各地區的溫度等氣象資訊

    [{"id":"84974e3b6b184603","type":"aedes broker","z":"ee949a679aeaac8d","name":"","mqtt_port":"1883","mqtt_ws_bind":"port","mqtt_ws_port":"","mqtt_ws_path":"","cert":"","key":"","ca":"","certname":"","keyname":"","caname":"","persistence_bind":"memory","dburl":"","persist_to_file":false,"usetls":false,"x":110,"y":60,"wires":[["d3d0db98037eb5a0"],["221c64cb507138ba"]]},{"id":"d3d0db98037eb5a0","type":"debug","z":"ee949a679aeaac8d","name":"debug 19","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":320,"y":40,"wires":[]},{"id":"221c64cb507138ba","type":"debug","z":"ee949a679aeaac8d","name":"debug 20","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":320,"y":80,"wires":[]},{"id":"7a50625e9de576f5","type":"mqtt out","z":"ee949a679aeaac8d","name":"","topic":"sensors/livingroom/temp","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"9aa0e63a7fe55694","x":590,"y":160,"wires":[]},{"id":"bf5d80bdee7081a3","type":"mqtt in","z":"ee949a679aeaac8d","name":"","topic":"sensors/livingroom/temp","qos":"2","datatype":"auto-detect","broker":"9aa0e63a7fe55694","nl":false,"rap":false,"inputs":0,"x":130,"y":220,"wires":[["b5a377175f3a3413","80fc5a39dcdee086","90567bb2394770f0","a54071ebf5b7899e","8210955ad042a18f"]]},{"id":"b5a377175f3a3413","type":"debug","z":"ee949a679aeaac8d","name":"mqTT_In","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":420,"y":220,"wires":[]},{"id":"ea0a92334afe9e73","type":"ui_dropdown","z":"ee949a679aeaac8d","name":"","label":"選擇地區","tooltip":"","place":"點擊選擇","group":"f1ec0c040bdd2739","order":37,"width":6,"height":1,"passthru":true,"multiple":false,"options":[{"label":"桃園","value":"桃園","type":"str"},{"label":"苗栗","value":"苗栗","type":"str"},{"label":"南投","value":"南投","type":"str"},{"label":"彰化","value":"埤頭","type":"str"},{"label":"台中","value":"中坑","type":"str"},{"label":"雲林","value":"斗六","type":"str"},{"label":"嘉義","value":"水上","type":"str"},{"label":"台南","value":"安南","type":"str"},{"label":"屏東","value":"九如","type":"str"},{"label":"台東","value":"延平","type":"str"},{"label":"花蓮","value":"豐濱","type":"str"},{"label":"宜蘭","value":"三星","type":"str"},{"label":"高雄","value":"三民","type":"str"},{"label":"台北","value":"平等","type":"str"}],"payload":"","topic":"topic","topicType":"msg","className":"","x":80,"y":160,"wires":[["1610a92a9b0c39de"]]},{"id":"1610a92a9b0c39de","type":"http request","z":"ee949a679aeaac8d","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0001-001?Authorization=CWB-40C25FFF-1224-4250-B9D9-3735AAE17DBF&limit=10&format=JSON&StationName={{payload}}&WeatherElement=Weather,Now,WindSpeed,AirTemperature,RelativeHumidity,AirPressure&GeoInfo=CountyName","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":240,"y":160,"wires":[["aa1c124b347003ba"]]},{"id":"aa1c124b347003ba","type":"json","z":"ee949a679aeaac8d","name":"","property":"payload","action":"","pretty":false,"x":390,"y":160,"wires":[["7a50625e9de576f5"]]},{"id":"80fc5a39dcdee086","type":"ui_gauge","z":"ee949a679aeaac8d","name":"","group":"f1ec0c040bdd2739","order":39,"width":6,"height":4,"gtype":"gage","title":"氣溫","label":"度","format":"{{payload.records.Station[0].WeatherElement.AirTemperature}}","min":0,"max":"40","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":410,"y":260,"wires":[]},{"id":"90567bb2394770f0","type":"ui_gauge","z":"ee949a679aeaac8d","name":"","group":"f1ec0c040bdd2739","order":40,"width":6,"height":4,"gtype":"gage","title":"風速","label":"m/s","format":"{{payload.records.Station[0].WeatherElement.WindSpeed}}","min":0,"max":"10","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":410,"y":300,"wires":[]},{"id":"a54071ebf5b7899e","type":"ui_gauge","z":"ee949a679aeaac8d","name":"","group":"f1ec0c040bdd2739","order":41,"width":6,"height":4,"gtype":"gage","title":"相對濕度","label":"%","format":"{{payload.records.Station[0].WeatherElement.RelativeHumidity}}","min":"50","max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":420,"y":340,"wires":[]},{"id":"8210955ad042a18f","type":"ui_gauge","z":"ee949a679aeaac8d","name":"","group":"f1ec0c040bdd2739","order":42,"width":6,"height":4,"gtype":"gage","title":"空氣壓力","label":"atm","format":"{{payload.records.Station[0].WeatherElement.AirPressure}}","min":"900","max":"1100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":420,"y":380,"wires":[]},{"id":"9aa0e63a7fe55694","type":"mqtt-broker","name":"","broker":"localhost","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"f1ec0c040bdd2739","type":"ui_group","name":"天氣","tab":"2156c1cf7f043d72","order":1,"disp":true,"width":12,"collapse":false,"className":""},{"id":"2156c1cf7f043d72","type":"ui_tab","name":"天氣圖表","icon":"dashboard","disabled":false,"hidden":false}]

    兩個圖表一起顯示 Node-Red

    [{"id":"229d489a86aaea93","type":"mqtt out","z":"8c31de38cd0a6981","name":"","topic":"sensors/livingroom/temp","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"78f412356543fd12","x":630,"y":220,"wires":[]},{"id":"adaec6df5a6580fb","type":"mqtt in","z":"8c31de38cd0a6981","name":"","topic":"sensors/livingroom/temp","qos":"2","datatype":"auto-detect","broker":"78f412356543fd12","nl":false,"rap":false,"inputs":0,"x":630,"y":260,"wires":[["ae695b84fba5fc23","9ed7c60684d641e1","518b5e5ea5836bdd","61ae7765a9f9edf5","0b113fc7b52cbfdb"]]},{"id":"ae695b84fba5fc23","type":"debug","z":"8c31de38cd0a6981","name":"mqTT_In","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":880,"y":200,"wires":[]},{"id":"e2e6628b7bde7a1b","type":"aedes broker","z":"8c31de38cd0a6981","name":"","mqtt_port":1883,"mqtt_ws_bind":"port","mqtt_ws_port":"","mqtt_ws_path":"","cert":"","key":"","certname":"","keyname":"","dburl":"","usetls":false,"x":150,"y":60,"wires":[["f0ae9594e8fe0786"],["0243ee57c33cba12"]]},{"id":"f0ae9594e8fe0786","type":"debug","z":"8c31de38cd0a6981","name":"debug 19","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":360,"y":40,"wires":[]},{"id":"0243ee57c33cba12","type":"debug","z":"8c31de38cd0a6981","name":"debug 20","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":360,"y":80,"wires":[]},{"id":"f50edc83dba4da17","type":"ui_dropdown","z":"8c31de38cd0a6981","name":"","label":"選擇地區","tooltip":"","place":"點擊選擇","group":"f1ec0c040bdd2739","order":2,"width":6,"height":1,"passthru":true,"multiple":false,"options":[{"label":"桃園","value":"桃園","type":"str"},{"label":"苗栗","value":"苗栗","type":"str"},{"label":"南投","value":"南投","type":"str"},{"label":"彰化","value":"埤頭","type":"str"},{"label":"台中","value":"中坑","type":"str"},{"label":"雲林","value":"斗六","type":"str"},{"label":"嘉義","value":"水上","type":"str"},{"label":"台南","value":"安南","type":"str"},{"label":"屏東","value":"九如","type":"str"},{"label":"台東","value":"延平","type":"str"},{"label":"花蓮","value":"豐濱","type":"str"},{"label":"宜蘭","value":"三星","type":"str"},{"label":"高雄","value":"三民","type":"str"},{"label":"台北","value":"平等","type":"str"}],"payload":"","topic":"topic","topicType":"msg","className":"","x":120,"y":220,"wires":[["14e3088564d30c0c","cb6bc9284f648b45"]]},{"id":"e51d55930ce8853e","type":"json","z":"8c31de38cd0a6981","name":"","property":"payload","action":"","pretty":false,"x":430,"y":220,"wires":[["229d489a86aaea93"]]},{"id":"9ed7c60684d641e1","type":"ui_gauge","z":"8c31de38cd0a6981","name":"","group":"f1ec0c040bdd2739","order":6,"width":5,"height":3,"gtype":"gage","title":"氣溫","label":"度","format":"{{payload.records.Station[0].WeatherElement.AirTemperature}}","min":0,"max":"40","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":870,"y":240,"wires":[]},{"id":"518b5e5ea5836bdd","type":"ui_gauge","z":"8c31de38cd0a6981","name":"","group":"f1ec0c040bdd2739","order":4,"width":5,"height":3,"gtype":"gage","title":"風速","label":"m/s","format":"{{payload.records.Station[0].WeatherElement.WindSpeed}}","min":0,"max":"10","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":870,"y":280,"wires":[]},{"id":"61ae7765a9f9edf5","type":"ui_gauge","z":"8c31de38cd0a6981","name":"","group":"f1ec0c040bdd2739","order":10,"width":5,"height":3,"gtype":"gage","title":"相對濕度","label":"%","format":"{{payload.records.Station[0].WeatherElement.RelativeHumidity}}","min":"50","max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":880,"y":320,"wires":[]},{"id":"0b113fc7b52cbfdb","type":"ui_gauge","z":"8c31de38cd0a6981","name":"","group":"f1ec0c040bdd2739","order":12,"width":5,"height":3,"gtype":"gage","title":"空氣壓力","label":"atm","format":"{{payload.records.Station[0].WeatherElement.AirPressure}}","min":"900","max":"1100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":880,"y":360,"wires":[]},{"id":"14e3088564d30c0c","type":"debug","z":"8c31de38cd0a6981","name":"debug 22","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":280,"y":140,"wires":[]},{"id":"d17279b29e0e15aa","type":"http in","z":"8c31de38cd0a6981","name":"","url":"/weather-app","method":"get","upload":false,"swaggerDoc":"","x":150,"y":320,"wires":[["19625023b6920da9"]]},{"id":"19625023b6920da9","type":"template","z":"8c31de38cd0a6981","name":"JS版本氣象html","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Weather Display</title>\n    <style>\n        body {\n            font-family: 'Arial', sans-serif;\n            margin: 0;\n            padding: 0;\n            background-color: #f4f4f4;\n            color: #333;\n        }\n\n        .weather-container {\n            margin-top: 20px;\n            background-color: white;\n            padding: 20px;\n            border-radius: 8px;\n            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n            width: 300px; /* 固定寬度 */\n        }\n\n        #stationSelect {\n            padding: 8px;\n            border-radius: 4px;\n            border: 1px solid #ddd;\n            margin-top: 20px;\n        }\n\n        h1, p {\n            margin: 10px 0;\n        }\n\n        #city {\n            font-size: 1.5em;\n            color: #0275d8;\n        }\n\n        #weather {\n            font-size: 1.2em;\n            color: #5cb85c;\n        }\n\n        #temperature {\n            font-size: 1.4em;\n            color: #f0ad4e;\n        }\n\n        /* 響應式布局 */\n        @media (max-width: 600px) {\n            .weather-container {\n                width: 90%;\n                margin: 20px auto;\n            }\n        }\n\n    </style>\n</head>\n<body>\n    <div>\n        <label for=\"stationSelect\">Choose a station:</label>\n        <select id=\"stationSelect\">\n            <option value=\"桃園\">桃園</option>\n            <option value=\"苗栗\">苗栗</option>\n            <option value=\"南投\">南投</option>\n            <option value=\"埤頭\">彰化</option>\n            <option value=\"中坑\">台中</option>\n            <option value=\"安南\">台南</option>\n            <!-- 更多選項 -->\n        </select>\n    </div>\n    <div class=\"weather-container\">\n        <h1 id=\"city\"></h1>\n        <p id=\"weather\"></p>\n        <p id=\"temperature\"></p>\n    </div>\n    <script>\n        document.getElementById('stationSelect').addEventListener('change', (event) => {\n            fetchWeatherData(event.target.value);\n        });\n\n        function fetchWeatherData(stationName) {\n            const apiUrl = `https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0001-001?Authorization=your-token&limit=10&format=JSON&StationName=${stationName}&WeatherElement=Weather,Now,WindSpeed,AirTemperature,RelativeHumidity,AirPressure&GeoInfo=CountyName`;\n\n            fetch(apiUrl)\n                .then(response => {\n                    if (!response.ok) {\n                        throw new Error('Network response was not ok');\n                    }\n                    return response.json();\n                })\n                .then(data => {\n                    console.log(data)\n                    updateWeatherDisplay(data);\n                })\n                .catch(error => {\n                    console.error('There has been a problem with your fetch operation:', error);\n                });\n        }\n\n        function updateWeatherDisplay(data) {\n            const temperature = data.records.Station[0]['WeatherElement']['AirTemperature'];\n            document.getElementById('temperature').textContent = temperature;\n            const weather = data.records.Station[0]['WeatherElement']['Weather'];\n            document.getElementById('weather').textContent = weather;\n            const city = data.records.Station[0]['GeoInfo']['CountyName'];\n            document.getElementById('city').textContent = city;\n        }\n\n        // 初始化,載入頁面時自動加載預設站點的天氣數據\n        fetchWeatherData(document.getElementById('stationSelect').value);\n\n    </script>\n</body>\n</html>","output":"str","x":340,"y":320,"wires":[["1e0d037f5b8791d1"]]},{"id":"1e0d037f5b8791d1","type":"http response","z":"8c31de38cd0a6981","name":"","statusCode":"","headers":{},"x":510,"y":320,"wires":[]},{"id":"b9319af08c19eec4","type":"mqtt in","z":"8c31de38cd0a6981","name":"","topic":"/Thinkitive/temp","qos":"0","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":140,"y":400,"wires":[["da1f699dede14640"]]},{"id":"9aca9e818558523e","type":"mqtt in","z":"8c31de38cd0a6981","name":"","topic":"/Thinkitive/hum","qos":"0","datatype":"auto-detect","broker":"b9efc827e98bf7f9","nl":false,"rap":true,"rh":0,"inputs":0,"x":140,"y":460,"wires":[["fff84846812b56a8"]]},{"id":"da1f699dede14640","type":"ui_gauge","z":"8c31de38cd0a6981","name":"TRMP","group":"70d1d491db52e4fe","order":1,"width":0,"height":0,"gtype":"gage","title":"ESP32溫度","label":"","format":"{{value}}","min":0,"max":"150","colors":["#00b500","#e6e600","#ec2727"],"seg1":"","seg2":"","diff":false,"className":"","x":350,"y":400,"wires":[]},{"id":"fff84846812b56a8","type":"ui_gauge","z":"8c31de38cd0a6981","name":"","group":"70d1d491db52e4fe","order":2,"width":0,"height":0,"gtype":"gage","title":"ESP32濕度","label":"","format":"{{value}}","min":0,"max":"60","colors":["#15cb15","#e6e600","#ca3838"],"seg1":"","seg2":"","diff":false,"className":"","x":370,"y":460,"wires":[]},{"id":"cb6bc9284f648b45","type":"http request","z":"8c31de38cd0a6981","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0001-001?Authorization=CWB-40C25FFF-1224-4250-B9D9-3735AAE17DBF&limit=10&format=JSON&StationName={{payload}}&WeatherElement=Weather,Now,WindSpeed,AirTemperature,RelativeHumidity,AirPressure&GeoInfo=CountyName","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":280,"y":220,"wires":[["e51d55930ce8853e"]]},{"id":"78f412356543fd12","type":"mqtt-broker","name":"","broker":"localhost","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"f1ec0c040bdd2739","type":"ui_group","name":"天氣","tab":"2156c1cf7f043d72","order":1,"disp":true,"width":12,"collapse":false,"className":""},{"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":"70d1d491db52e4fe","type":"ui_group","name":"esp32圖表","tab":"2156c1cf7f043d72","order":2,"disp":true,"width":"6","collapse":false,"className":""},{"id":"2156c1cf7f043d72","type":"ui_tab","name":"天氣圖表","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 輸出...