2025年10月13日 星期一

Node-Red 控制一週時間ON,OFF + WOKWI ESP32

Node-Red 控制一週時間ON,OFF + WOKWI ESP32






  1.  Node-RED Dashboard UI:可設定每週(週一~週日)的時間區間(開始/結束 時:分:秒)
  2. 會自動顯示目前時間
  3. 自動比對目前時間是否在設定區間內 → 輸出 1 (ON) 或 0 (OFF)
  4. 儲存/讀取設定到 file 節點 (JSON 檔)
  5. 顯示在 ui-table
  6. 同步發佈狀態到 MQTT topic(例:alex9ufo/schedule/status

功能節點說明
設定每週時間ui_form可選星期與開始/結束時間
儲存設定file存成 /data/schedule.json
顯示表格ui_table顯示所有設定
比對目前時間每秒執行,判斷當前是否在設定區間
狀態顯示ui_text顯示目前時間與 ON/OFF
MQTT 發佈topic: alex9ufo/schedule/status

WOKWI程式

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

//const char* ssid = "你的WiFi名稱";
//const char* password = "你的WiFi密碼";
const char* ssid = "Wokwi-GUEST";
const char* password = "";

const char* mqtt_server = "broker.mqttgo.io";

WiFiClient espClient;
PubSubClient client(espClient);

const int LED_PIN = 2;

void callback(char* topic, byte* message, unsigned int length) {
  String msg;
  for (int i = 0; i < length; i++) msg += (char)message[i];
  Serial.print("收到主題: "); Serial.println(topic);
  Serial.print("訊息: "); Serial.println(msg);
  if (msg == "1") {
    digitalWrite(LED_PIN, HIGH);
    client.publish("alex9ufo/schedule/led", "ON");
  } else {
    digitalWrite(LED_PIN, LOW);
    client.publish("alex9ufo/schedule/led", "OFF");
  }
}

void reconnect() {
  while (!client.connected()) {
    if (client.connect("ESP32_TimerClient")) {
      client.subscribe("alex9ufo/schedule/status");
    } else {
      delay(2000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

void loop() {
  if (!client.connected()) reconnect();
  client.loop();
}


Node-Red程式

[ { "id": "d84e42b6dbd0994e", "type": "ui_form", "z": "c7fde6141e82d752", "name": "時間設定表單", "label": "每週時間設定", "group": "e5af8077ea7bc427", "order": 8, "width": 11, "height": 1, "options": [ { "label": "星期", "value": "weekday", "type": "text", "required": false, "rows": null }, { "label": "開始時間 (HH:MM:SS)", "value": "start", "type": "text", "required": false, "rows": null }, { "label": "結束時間 (HH:MM:SS)", "value": "end", "type": "text", "required": false, "rows": null } ], "formValue": { "weekday": "", "start": "", "end": "" }, "payload": "", "submit": "儲存設定", "cancel": "", "topic": "schedule/add", "topicType": "str", "splitLayout": false, "className": "", "x": 120, "y": 120, "wires": [ [ "e4ab434ed5e4c543" ] ] }, { "id": "e4ab434ed5e4c543", "type": "function", "z": "c7fde6141e82d752", "name": "新增時間設定 + 儲存檔案", "func": "let list = flow.get('scheduleList') || [];\nlet d = msg.payload;\nlet found = false;\nfor (let i=0;i<list.length;i++) {\n if (list[i].weekday === d.weekday) {\n list[i] = d;\n found = true;\n break;\n }\n}\nif (!found) list.push(d);\nflow.set('scheduleList', list);\nreturn [{payload: JSON.stringify(list, null, 2)}, {payload: list}];", "outputs": 2, "x": 350, "y": 120, "wires": [ [ "8dad8d745a795a75" ], [ "692e2f012e2919fe" ] ] }, { "id": "8dad8d745a795a75", "type": "file", "z": "c7fde6141e82d752", "name": "寫入 schedule.json", "filename": "/data/schedule.json", "appendNewline": false, "createDir": true, "overwriteFile": "true", "encoding": "none", "x": 590, "y": 100, "wires": [ [] ] }, { "id": "692e2f012e2919fe", "type": "ui_table", "z": "c7fde6141e82d752", "group": "3b8f4b37e0b6b986", "name": "顯示清單", "order": 1, "width": 8, "height": 6, "columns": [ { "field": "weekday", "title": "星期", "width": "20%", "align": "left", "formatter": "plaintext", "formatterParams": { "target": "_blank" } }, { "field": "start", "title": "開始時間", "width": "40%", "align": "left", "formatter": "plaintext", "formatterParams": { "target": "_blank" } }, { "field": "end", "title": "結束時間", "width": "40%", "align": "left", "formatter": "plaintext", "formatterParams": { "target": "_blank" } } ], "outputs": 0, "cts": false, "x": 800, "y": 120, "wires": [] }, { "id": "9635e178025ac66d", "type": "inject", "z": "c7fde6141e82d752", "name": "每秒更新時間", "props": [ { "p": "payload" } ], "repeat": "1", "once": true, "topic": "", "payloadType": "date", "x": 140, "y": 260, "wires": [ [ "4c7f615aae1de37f" ] ] }, { "id": "4c7f615aae1de37f", "type": "function", "z": "c7fde6141e82d752", "name": "比對時間輸出 on/off", "func": "let now = new Date();\nlet wd = now.getDay();\nlet dayMap = ['日','一','二','三','四','五','六'];\nlet weekday = dayMap[wd];\nlet nowTime = now.toTimeString().split(' ')[0];\nlet list = flow.get('scheduleList') || [];\nlet manual = flow.get('manualOverride') || 'auto';\nlet status = 0;\n\nif (manual === 'forceOn') status = 1;\nelse if (manual === 'forceOff') status = 0;\nelse {\n for (let s of list) {\n if (s.weekday === weekday && nowTime >= s.start && nowTime <= s.end) {\n status = 1;\n break;\n }\n }\n}\nmsg.payload = {\n now: now.toLocaleString(),\n weekday: weekday,\n status: status,\n mode: manual\n};\nflow.set('currentStatus', status);\nreturn [msg, {payload: status}];", "outputs": 2, "x": 330, "y": 260, "wires": [ [ "f5470011762e1185" ], [ "3ae28406032feb5a" ] ] }, { "id": "f5470011762e1185", "type": "ui_text", "z": "c7fde6141e82d752", "group": "e5af8077ea7bc427", "order": 1, "width": 11, "height": 1, "name": "目前時間顯示", "label": "目前時間", "format": "{{msg.payload.now}} ({{msg.payload.weekday}}) 狀態:{{msg.payload.status == 1 ? '🟢ON' : '⚪OFF'}} 模式:{{msg.payload.mode}}", "layout": "row-spread", "className": "", "style": false, "font": "", "fontSize": "", "color": "#000000", "x": 540, "y": 240, "wires": [] }, { "id": "3ae28406032feb5a", "type": "mqtt out", "z": "c7fde6141e82d752", "name": "發佈 MQTT 狀態", "topic": "alex9ufo/schedule/status", "qos": "0", "retain": "true", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": "192c2b20bef1e71a", "x": 540, "y": 280, "wires": [] }, { "id": "df251f90a866b0da", "type": "mqtt in", "z": "c7fde6141e82d752", "name": "接收 LED 狀態", "topic": "alex9ufo/schedule/led", "qos": "2", "datatype": "auto-detect", "broker": "192c2b20bef1e71a", "nl": false, "rap": false, "inputs": 0, "x": 120, "y": 400, "wires": [ [ "1f5d4d36e2781dd9" ] ] }, { "id": "1f5d4d36e2781dd9", "type": "ui_text", "z": "c7fde6141e82d752", "group": "e5af8077ea7bc427", "order": 2, "width": 11, "height": 1, "name": "LED 回傳狀態", "label": "ESP32 LED 狀態", "format": "LED 狀態:{{msg.payload}}", "layout": "row-spread", "className": "", "style": false, "font": "", "fontSize": "", "color": "#000000", "x": 320, "y": 400, "wires": [] }, { "id": "ed18247f43c38979", "type": "ui_button", "z": "c7fde6141e82d752", "name": "手動開啟", "group": "e5af8077ea7bc427", "order": 3, "width": 3, "height": 1, "passthru": false, "label": "手動 ON", "tooltip": "", "color": "white", "bgcolor": "green", "className": "", "icon": "", "payload": "forceOn", "payloadType": "str", "topic": "", "topicType": "str", "x": 100, "y": 480, "wires": [ [ "c36d8266accd7860" ] ] }, { "id": "da607f611829c027", "type": "ui_button", "z": "c7fde6141e82d752", "name": "手動關閉", "group": "e5af8077ea7bc427", "order": 5, "width": 3, "height": 1, "passthru": false, "label": "手動 OFF", "tooltip": "", "color": "", "bgcolor": "red", "className": "", "icon": "", "payload": "forceOff", "payloadType": "str", "topic": "", "topicType": "str", "x": 100, "y": 520, "wires": [ [ "c36d8266accd7860" ] ] }, { "id": "cb4161ada8aae76e", "type": "ui_button", "z": "c7fde6141e82d752", "name": "自動模式", "group": "e5af8077ea7bc427", "order": 7, "width": 3, "height": 1, "passthru": false, "label": "自動模式", "tooltip": "", "color": "", "bgcolor": "gray", "className": "", "icon": "", "payload": "auto", "payloadType": "str", "topic": "", "topicType": "str", "x": 100, "y": 560, "wires": [ [ "c36d8266accd7860" ] ] }, { "id": "c36d8266accd7860", "type": "function", "z": "c7fde6141e82d752", "name": "設定手動模式", "func": "flow.set('manualOverride', msg.payload);\nnode.status({text:`模式切換為 ${msg.payload}`});\nreturn null;", "outputs": 1, "x": 280, "y": 520, "wires": [ [] ] }, { "id": "c48496254c3bb193", "type": "inject", "z": "c7fde6141e82d752", "name": "啟動時載入設定", "props": [], "repeat": "", "once": true, "onceDelay": "0.5", "topic": "", "x": 140, "y": 40, "wires": [ [ "f4a48dbf26d3f4ac" ] ] }, { "id": "f4a48dbf26d3f4ac", "type": "file in", "z": "c7fde6141e82d752", "name": "讀取 schedule.json", "filename": "/data/schedule.json", "format": "utf8", "x": 330, "y": 40, "wires": [ [ "fc3b727c53522c13" ] ] }, { "id": "fc3b727c53522c13", "type": "function", "z": "c7fde6141e82d752", "name": "初始化排程資料", "func": "if (!msg.payload) return null;\ntry {\n let list = JSON.parse(msg.payload);\n flow.set('scheduleList', list);\n return {payload: list};\n} catch (err) {\n return null;\n}", "outputs": 1, "x": 560, "y": 40, "wires": [ [ "692e2f012e2919fe" ] ] }, { "id": "e5af8077ea7bc427", "type": "ui_group", "name": "控制模式與時間", "tab": "b271b1b4b9c3e8c8", "order": 2, "disp": true, "width": 11, "collapse": false, "className": "" }, { "id": "3b8f4b37e0b6b986", "type": "ui_group", "name": "每週排程設定", "tab": "b271b1b4b9c3e8c8", "order": 1, "disp": true, "width": 8, "collapse": false }, { "id": "192c2b20bef1e71a", "type": "mqtt-broker", "name": "mqttgo", "broker": "broker.mqttgo.io", "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": "b271b1b4b9c3e8c8", "type": "ui_tab", "name": "時間控制", "icon": "dashboard", "order": 1 } ]

沒有留言:

張貼留言

ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite

 ESP32 (ESP-IDF in VS Code) MFRC522 + MQTT + PYTHON TKinter +SQLite  ESP32 VS Code 程式 ; PlatformIO Project Configuration File ; ;   Build op...