2026年3月30日 星期一

2026 作業4 WOKWI ESP32 DHT22+4LED & Node-Red , Telegram , MQTT 練習

2026 作業4   WOKWI ESP32 DHT22+4LED & Node-Red , Telegram , MQTT 練習


Node-Red Telegram 設定需修改  TOKEN API  與 CHATID  才會指向你的Telegram

 

IoT 遠端控制與監測系統。它是一個典型的「雲端代理(MQTT Broker)」架構,讓原本互相隔離的 Wokwi 模擬器、Node-RED 儀表板與 Telegram 手機端能夠即時溝通。

核心通訊流程:MQTT 作為調度中心

整個系統圍繞著 Mqttgo.io 這個 Broker 運作。所有的指令與數據都是透過「訂閱(Subscribe)」與「發布(Publish)」來傳遞。

1. 指令流 (從手機到硬體)

這是一條「下行控制」路徑,主要負責開關 LED:

  • 來源:您在 Telegram 輸入指令(如 /1on)或在 Node-RED Dashboard 按下開關。

  • 中轉:Node-RED 接收到 Telegram 的訊息後,將其轉換為簡單的字串(如 1on),並發布到 MQTT 主題:alex9ufo/ledcontrol

  • 執行Wokwi 中的 ESP32 因為訂閱了這個主題,會立刻收到訊息並透過 core0Task 驅動 GPIO 腳位,點亮對應的 LED。

2. 狀態回報流 (從硬體到手機/網頁)

這是一條「確認機制」路徑,確保指令確實執行:

  • 來源:ESP32 成功切換 LED 電位後,由 core1Task 將結果(如 LED1 ON)發布到:alex9ufo/ledstatus

  • 顯示Node-RED 訂閱此主題後,更新儀表板上的指示燈顏色,並將成功訊息回傳給 Telegram

3. 感測器數據流 (DHT22 監測)

這是一條「數據監測」路徑:

  • 觸發:在 Wokwi 序列監控視窗按 Enter,或從 Telegram 發送 readData

  • 數據:ESP32 讀取 DHT22 後,將字串(Temperature: 5.00C...)發布到:alex9ufo/temphumi

  • 應用:Node-RED 接收後,透過您寫的 Function 節點 拆分數據,顯示在儀表板的圓形進度條(Gauge)上。


關鍵技術亮點

組件主要角色圖片中呈現的功能
Wokwi (ESP32)執行器與感測端運行雙核心程式,負責 WiFi 連線、LED 硬體控制與 DHT22 採集。
Node-RED大腦與中間層處理邏輯判斷(如溫度 > 70 警報)、介面顯示與 Telegram 訊息轉譯。
Telegram遠端介面提供跨地域的文字指令操作,並接收高溫或狀態通知。
MQTT (Mqttgo)訊息郵局確保不同網域下的設備能透過 Topic 機制進行雙向通訊。

「手動方式要按 Enter」

這點非常重要。由於 Wokwi 環境的限制,DHT22 的數據並非每秒自動狂噴,而是採用被動式觸發

  1. 本地觸發:在模擬器的 Serial 視窗按 Enter。

  2. 遠端觸發:從 Telegram 發送 /readData。 這是一個很好的設計,可以避免 MQTT Broker 在模擬測試期間產生過多不必要的數據流量。


Telegram 控制指令

歡迎 Alex 使用 WOKWI&Node-red+Telegram控制系統 /1on : 開啟 LED1 /1off : 關閉 LED1 /2on : 開啟 LED2 /2off : 關閉 LED2 /3on : 開啟 LED3 /3off : 關閉 LED3 /4on : 開啟 LED4 /4off : 關閉 LED4 /allon : 開啟 LED1-LED4 /alloff : 關閉 LED1-LED4 /WOKWI 的 DHT22 輸出 採用 手動方式 要按enter


MQTT Broker與Topic 設定

const char* mqtt_broker = "mqttgo.io";

const int mqtt_port = 1883;

// MQTT 主題
const char* led_control_topic = "alex9ufo/ledcontrol";
const char* led_status_topic = "alex9ufo/ledstatus";
const char* temp_humi_topic = "alex9ufo/temphumi";

ESP32接腳

#define DHTPIN 4     // DHT22 連接到 ESP32 的 D4 腳位
// LED 腳位定義 (根據 diagram.json 設定)
const int ledPins[] = {13, 12, 14, 27}; // 假設連接到 D13, D12, D14, D27


WOKWIESP32程式




#include <WiFi.h>

#include <PubSubClient.h>
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <DHT_U.h>
#include <freertos/FreeRTOS.h> // 引入 FreeRTOS 庫
#include <freertos/task.h>     // 引入 FreeRTOS 任務相關庫

// Wi-Fi 憑證
const char* ssid = "Wokwi-GUEST";     // 替換成您的 Wi-Fi 名稱 (例如 Wokwi 的 "Wokwi-GUEST")
const char* password = "";            // 替換成您的 Wi-Fi 密碼 (例如 Wokwi 的空字串 "")

#define DHTPIN 4     // DHT22 連接到 ESP32 的 D4 腳位
#define DHTTYPE DHT22 // DHT 22 (AM2302), AM2321

DHT_Unified dht(DHTPIN, DHTTYPE);

const char* mqtt_broker = "mqttgo.io";
const int mqtt_port = 1883;
const char* mqtt_client_id = "alex9ufo-wokwi-client-dualcore";

// MQTT 主題
const char* led_control_topic = "alex9ufo/ledcontrol";
const char* led_status_topic = "alex9ufo/ledstatus";
const char* temp_humi_topic = "alex9ufo/temphumi";

// LED 腳位定義 (根據 diagram.json 設定)
const int ledPins[] = {13, 12, 14, 27}; // 假設連接到 D13, D12, D14, D27
const int NUM_LEDS = sizeof(ledPins) / sizeof(ledPins[0]);

WiFiClient espClient;
PubSubClient client(espClient);

// 用於跨任務通訊的佇列 (Queue)
// 我們將使用這個佇列來傳遞要發布的 MQTT 訊息
QueueHandle_t mqttPublishQueue;

// 共享變數,用於通知發布任務 DHT22 數據可用
volatile bool dhtReadTriggered = false;
volatile bool dhtReadData = false;
volatile float lastTemperature = 0.0;
volatile float lastHumidity = 0.0;

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 訂閱的回調函數 (在 Core 0 執行)
void mqttSubscribeCallback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  payload[length] = '\0'; // 確保字串結束
  String message = String((char*)payload);
  Serial.println(message);

  if (String(topic) == led_control_topic) {
    String publishMsg = ""; // 用於發布的訊息

    if (message == "1on") {
      digitalWrite(ledPins[0], HIGH);
      publishMsg = "LED1 ON";
      Serial.println("LED1 ON");
    } else if (message == "1off") {
      digitalWrite(ledPins[0], LOW);
      publishMsg = "LED1 OFF";
      Serial.println("LED1 OFF");
    } else if (message == "2on") {
      digitalWrite(ledPins[1], HIGH);
      publishMsg = "LED2 ON";
      Serial.println("LED2 ON");
    } else if (message == "2off") {
      digitalWrite(ledPins[1], LOW);
      publishMsg = "LED2 OFF";
      Serial.println("LED2 OFF");
    } else if (message == "3on") {
      digitalWrite(ledPins[2], HIGH);
      publishMsg = "LED3 ON";
      Serial.println("LED3 ON");
    } else if (message == "3off") {
      digitalWrite(ledPins[2], LOW);
      publishMsg = "LED3 OFF";
      Serial.println("LED3 OFF");
    } else if (message == "4on") {
      digitalWrite(ledPins[3], HIGH);
      publishMsg = "LED4 ON";
      Serial.println("LED4 ON");
    } else if (message == "4off") {
      digitalWrite(ledPins[3], LOW);
      publishMsg = "LED4 OFF";
      Serial.println("LED4 OFF");
    } else if (message == "allon") {
      for (int i = 0; i < NUM_LEDS; i++) {
        digitalWrite(ledPins[i], HIGH);
      }
      publishMsg = "ALL LEDs ON";
      Serial.println("ALL LEDs ON");
    } else if (message == "alloff") {
      for (int i = 0; i < NUM_LEDS; i++) {
        digitalWrite(ledPins[i], LOW);
      }
      publishMsg = "ALL LEDs OFF";
      Serial.println("ALL LEDs OFF");
    } else if (message == "readData") {
      Serial.println("讀取DHT22 Data 從Telehgram送出命令");
      publishMsg = "讀取DHT22 Data";
      dhtReadData = true;
    }  


    // 將要發布的訊息放入佇列,由發布任務處理
    if (publishMsg.length() > 0) {
      char msgBuffer[50];
      publishMsg.toCharArray(msgBuffer, sizeof(msgBuffer));
      xQueueSend(mqttPublishQueue, &msgBuffer, portMAX_DELAY);
    }
  }
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect(mqtt_client_id)) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      char initialMsg[] = "Wokwi LED controller connected (Dual Core)";
      xQueueSend(mqttPublishQueue, &initialMsg, portMAX_DELAY); // 將初始化訊息放入佇列
      // ... and resubscribe
      client.subscribe(led_control_topic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

// DHT22 讀取函數 (在 Core 0 觸發,數據由發布任務處理)
void readDHT22() {
  sensors_event_t event;
  dht.temperature().getEvent(&event);
  if (isnan(event.temperature)) {
    Serial.println(F("Error reading temperature!"));
  } else {
    lastTemperature = event.temperature; // 更新共享變數
    dht.humidity().getEvent(&event);
    if (isnan(event.relative_humidity)) {
      Serial.println(F("Error reading humidity!"));
    } else {
      lastHumidity = event.relative_humidity; // 更新共享變數
      dhtReadTriggered = true; // 設定旗標,通知發布任務有新數據
    }
  }
}

// Core 0 任務:處理 MQTT 訂閱和 Serial Monitor 輸入
void core0Task(void * parameter) {
  Serial.println("Core 0 Task running: Handling MQTT Subscriptions and Serial Input.");
  client.setCallback(mqttSubscribeCallback); // 設定訂閱回調函數

  while (true) {
    if (!client.connected()) {
      reconnect();
    }
    client.loop(); // 處理 MQTT 訂閱訊息

    // 監聽 Serial Monitor 輸入
    if (Serial.available()) {
      String command = Serial.readStringUntil('\n');
      command.trim(); // 移除前後空白

      if (command == "") { // 當使用者按下 Enter (輸入空字串)
        readDHT22(); // 讀取 DHT22
      }
    }
    if (dhtReadData) {
      dhtReadData=false;
      readDHT22(); // 讀取 DHT22
    }

    vTaskDelay(10 / portTICK_PERIOD_MS); // 短暫延遲,讓其他任務有執行機會
  }
}

// Core 1 任務:處理 MQTT 發布
void core1Task(void * parameter) {
  Serial.println("Core 1 Task running: Handling MQTT Publications.");
  char msgBuffer[50];

  while (true) {
    // 檢查是否有要從佇列發布的訊息
    if (xQueueReceive(mqttPublishQueue, &msgBuffer, 0) == pdTRUE) {
      if (client.connected()) {
        if (strcmp(msgBuffer, "LED1 ON") == 0 || strcmp(msgBuffer, "LED1 OFF") == 0 ||
            strcmp(msgBuffer, "LED2 ON") == 0 || strcmp(msgBuffer, "LED2 OFF") == 0 ||
            strcmp(msgBuffer, "LED3 ON") == 0 || strcmp(msgBuffer, "LED3 OFF") == 0 ||
            strcmp(msgBuffer, "LED4 ON") == 0 || strcmp(msgBuffer, "LED4 OFF") == 0 ||
            strcmp(msgBuffer, "ALL LEDs ON") == 0 || strcmp(msgBuffer, "ALL LEDs OFF") == 0) {
          client.publish(led_status_topic, msgBuffer);
          Serial.print("Published to led_status_topic: ");
          Serial.println(msgBuffer);
        }
      }
    }

    // 檢查是否有 DHT22 新數據要發布
    if (dhtReadTriggered) {
      if (client.connected()) {
        String temp_humi_msg = "Temperature: " + String(lastTemperature) + "C, Humidity: " + String(lastHumidity) + "%";
        client.publish(temp_humi_topic, temp_humi_msg.c_str());
        Serial.print("Published to temp_humi_topic: ");
        Serial.println(temp_humi_msg);
      }
      dhtReadTriggered = false; // 重置旗標
    }

    vTaskDelay(50 / portTICK_PERIOD_MS); // 適度延遲,避免佔用所有 CPU 資源
  }
}

void setup() {
  Serial.begin(115200);

  for (int i = 0; i < NUM_LEDS; i++) {
    pinMode(ledPins[i], OUTPUT);
    digitalWrite(ledPins[i], LOW); // 初始化所有 LED 為關閉
  }

  dht.begin();
  sensor_t sensor;
  dht.temperature().getSensor(&sensor);
  Serial.println(F("------------------------------------"));
  Serial.println(F("Unified Sensor Example"));
  Serial.print(F("Sensor:       ")); Serial.println(sensor.name);
  Serial.print(F("Driver Ver:   ")); Serial.println(sensor.version);
  Serial.print(F("Unique ID:    ")); Serial.println(sensor.sensor_id);
  Serial.print(F("Max Value:    ")); Serial.print(sensor.max_value); Serial.println(F(" C"));
  Serial.print(F("Min Value:    ")); Serial.print(sensor.min_value); Serial.println(F(" C"));
  Serial.print(F("Resolution:   ")); Serial.print(sensor.resolution); Serial.println(F(" C"));
  Serial.println(F("------------------------------------"));

  setup_wifi();
  client.setServer(mqtt_broker, mqtt_port);

  // 創建用於跨任務通訊的佇列
  mqttPublishQueue = xQueueCreate(10, sizeof(char[50])); // 佇列深度為 10,每個元素為 50 字元的 char 陣列

  // 創建 Core 0 任務 (處理訂閱)
  xTaskCreatePinnedToCore(
    core0Task,          // 任務函數
    "Core0Task",        // 任務名稱
    10000,              // 任務堆疊大小 (位元組)
    NULL,               // 傳遞給任務的參數
    1,                  // 任務優先級 (較高優先級)
    NULL,               // 任務句柄
    0                   // 運行在 Core 0
  );

  // 創建 Core 1 任務 (處理發布)
  xTaskCreatePinnedToCore(
    core1Task,          // 任務函數
    "Core1Task",        // 任務名稱
    10000,              // 任務堆疊大小 (位元組)
    NULL,               // 傳遞給任務的參數
    1,                  // 任務優先級
    NULL,               // 任務句柄
    1                   // 運行在 Core 1
  );
}

void loop() {
  // loop() 函數在 FreeRTOS 環境下,如果所有任務都已建立,通常會保持空閒。
  // 所有邏輯都將在各自的任務中運行。
  vTaskDelete(NULL); // 刪除 loop 任務本身,將控制權完全交給 FreeRTOS
}


這份程式碼是一個非常進階的 ESP32 雙核心 (Dual Core) 應用範例,利用了 ESP32 內部的兩個處理核心(Core 0 與 Core 1)並結合 FreeRTOS 作業系統來分工處理物聯網任務。

其核心邏輯是:Core 0 負責「聽」指令(MQTT 訂閱與 Serial 輸入),Core 1 負責「說」話(MQTT 數據發布)。


程式碼逐行詳解

1. 引用庫與定義 (Includes & Defines)

  • WiFi.h & PubSubClient.h: 處理 Wi-Fi 連線與 MQTT 協定。

  • DHT.h & DHT_U.h: 使用 Adafruit 的統一感測器庫來讀取 DHT22 溫濕度。

  • freertos/FreeRTOS.h & task.h: 這是關鍵,引入 FreeRTOS 核心功能,讓 ESP32 能夠執行多工任務。

  • DHTPIN 4: 定義 DHT22 接在 GPIO 4。

2. 全域變數與跨核心通訊

  • QueueHandle_t mqttPublishQueue: 這是 「佇列」。因為兩個核心不能直接隨意存取對方的區域變數,所以建立一個「傳聲筒」,Core 0 把要發出的訊息丟進去,Core 1 再從裡面拿出來發送。

  • volatile bool dhtReadTriggered: 加上 volatile 關鍵字,告訴編譯器這個變數會在不同的核心(任務)之間變動,確保讀取到的是最新值。

3. MQTT 回調函數 (處理接收到的指令)

  • mqttSubscribeCallback: 當 Node-RED 或 Telegram 傳送指令(如 1on, alloff)到 ledcontrol 主題時,這個函數會被觸發。

  • 指令判斷: 程式會比對 message,執行 digitalWrite 切換 LED 電位。

  • xQueueSend: 執行完動作後,將「LED1 ON」等回應文字塞進 mqttPublishQueue 佇列,交給 Core 1 去回報狀態。

4. 雙核心任務 (Tasks)

Core 0 任務:core0Task (處理「入」的資料)

  • client.loop(): 這是 MQTT 的心臟,負責檢查是否有新訊息進來。

  • Serial 監聽: 檢查電腦序列埠輸入。如果你在 Wokwi 按下 Enter(空字串),它會觸發 readDHT22()

  • dhtReadData: 如果從 Telegram 收到 readData 指令,也會觸發讀取。

Core 1 任務:core1Task (處理「出」的資料)

  • xQueueReceive: 持續檢查「傳聲筒(佇列)」裡有沒有訊息。如果有(如:初始化訊息或 LED 狀態),就執行 client.publish 發送出去。

  • dhtReadTriggered 檢查: 當 Core 0 完成 DHT22 讀取後會舉起這個旗標,Core 1 看到後就會將溫度與濕度組合成字串,發布到 temphumi 主題。

5. 初始化與核心分配 (setup)

  • xQueueCreate: 初始化佇列空間。

  • xTaskCreatePinnedToCore: 這是最重要的一步:

    • 建立 Core0Task 並指派給 Core 0

    • 建立 Core1Task 並指派給 Core 1

    • 這確保了當感測器讀取或 Wi-Fi 重連時,不會卡住 LED 的控制反應。

6. 主迴圈 (loop)

  • vTaskDelete(NULL): 在典型的 FreeRTOS 程式中,我們不在 loop() 裡寫程式。這行代碼會刪除原本 Arduino 預設的 loop 任務,釋放資源給我們自定義的兩個核心任務。


程式優點總結

  1. 非阻塞 (Non-blocking):傳統做法如果 Wi-Fi 斷線,整個程式會卡在 reconnect 裡,LED 就無法控制。但在這個版本中,Core 0 依然可以處理本地指令。

  2. 即時性:利用佇列(Queue)緩衝訊息,避免在高頻率傳輸時遺失數據。

  3. 靈活性:支援手動(Enter 鍵)與遠端(Telegram 指令)兩種方式觸發 DHT22 讀取。


Node-Red程式




[{"id":"aa38bbd69640e2ae","type":"telegram receiver","z":"8c2053d55f5883f8","name":"","bot":"457874f8aa8a857a","saveDataDir":"","filterCommands":true,"x":130,"y":40,"wires":[["eb8234cf9675d55a"],[]]},{"id":"60bd77976d9b25a8","type":"function","z":"8c2053d55f5883f8","name":"根據指令發佈 MQTT 訊息","func":"if (msg.payload === \"/1on\") {\n    msg.topic = \"alex9ufo/ledcontrol\";\n    msg.payload = \"1on\";  // 開啟 LED1\n    return [msg , null];\n} else if (msg.payload === \"/1off\") {\n    msg.topic = \"alex9ufo/ledcontrol\";\n    msg.payload = \"1off\"; // 關閉 LED1\n    return [msg , null];\n} else if (msg.payload === \"/2on\") {\n    msg.topic = \"alex9ufo/ledcontrol\";\n    msg.payload = \"2on\"; // 開啟 LED2\n    return [msg , null];\n} else if (msg.payload === \"/2off\") {\n    msg.topic = \"alex9ufo/ledcontrol\";\n    msg.payload = \"2off\"; // 關閉 LED2\n    return [msg , null];\n} else if (msg.payload === \"/3on\") {\n    msg.topic = \"alex9ufo/ledcontrol\";\n    msg.payload = \"3on\"; // 開啟 LED3\n    return [msg , null];\n} else if (msg.payload === \"/3off\") {\n    msg.topic = \"alex9ufo/ledcontrol\";\n    msg.payload = \"3off\"; // 關閉 LED3\n    return [msg , null];\n} else if (msg.payload === \"/4on\") {\n    msg.topic = \"alex9ufo/ledcontrol\";\n    msg.payload = \"4on\"; // 開啟 LED4\n    return [msg , null];\n} else if (msg.payload === \"/4off\") {\n    msg.topic = \"alex9ufo/ledcontrol\";\n    msg.payload = \"4off\"; // 關閉 LED4\n    return [msg , null];\n} else if (msg.payload === \"/allon\") {\n    msg.topic = \"alex9ufo/ledcontrol\";\n    msg.payload = \"allon\"; // 開啟 all LED1-LED4\n    return [msg , null];\n} else if (msg.payload === \"/alloff\") {\n    msg.topic = \"alex9ufo/ledcontrol\";\n    msg.payload = \"alloff\"; // 關閉 all LED1-LED4\n    return [msg , null];\n} else if  (msg.payload === \"/WOKWI\") {\n    msg.topic = \"alex9ufo/ledcontrol\";\n    msg.payload = \"readData\";  // 讀取DHT22溫度濕度\n    return [msg, null];\n} \n\n//function 根據指令來發佈 MQTT 訊息","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":40,"wires":[["2579e03bbb9008db","3f077821c764e91d"],[]]},{"id":"2579e03bbb9008db","type":"mqtt out","z":"8c2053d55f5883f8","name":"alex9ufo/ledcontrol","topic":"alex9ufo/ledcontrol","qos":"1","retain":"true","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"192c2b20bef1e71a","x":790,"y":60,"wires":[]},{"id":"f04e44b6863fba43","type":"mqtt in","z":"8c2053d55f5883f8","name":"","topic":"alex9ufo/ledstatus","qos":"2","datatype":"auto-detect","broker":"192c2b20bef1e71a","nl":false,"rap":true,"rh":0,"inputs":0,"x":130,"y":260,"wires":[["c4c0948da43caba8","81ada557b64dc604","5df891b22fcf0d78","d20c7a1c503d7b74","9a49877e65ede3d6"]]},{"id":"eb8234cf9675d55a","type":"function","z":"8c2053d55f5883f8","name":"function","func":"var content= msg.payload.content;\nmsg.payload=content;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":40,"wires":[["60bd77976d9b25a8","85fff350d422330f"]]},{"id":"7285f33b22258e53","type":"ui_led","z":"8c2053d55f5883f8","order":1,"group":"9123b13576984e5e","width":3,"height":3,"label":"LED1","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED1","x":570,"y":180,"wires":[]},{"id":"c4c0948da43caba8","type":"function","z":"8c2053d55f5883f8","name":"function","func":"var content = msg.payload;\n\nif (content === 'LED1 ON') {\n    msg.payload = true;\n    // Route to output 1\n    return [msg, null, null, null ];\n}\nif (content === 'LED1 OFF') {\n    msg.payload = false;\n    // Route to output 1\n    return [msg, null, null, null];\n}\nif (content === 'LED2 ON') {   \n    msg.payload = true;\n    // Route to output 2\n    return [null, msg, null, null];\n}\nif (content === 'LED2 OFF') {\n    msg.payload = false;\n    // Route to output 2\n    return [null, msg, null, null];\n}\nif (content === 'LED3 ON') {   \n    msg.payload = true;\n    // Route to output 3\n    return [null,  null, msg, null];\n}\nif (content === 'LED3 OFF') {\n    msg.payload = false;\n    // Route to output 3\n    return [null, null, msg, null];\n}\nif (content === 'LED4 ON') {\n    msg.payload = true;\n    // Route to output 4\n    return [null, null, null, msg];\n}\nif (content === 'LED4 OFF') {\n    msg.payload = false;\n    // Route to output 4\n    return [null, null, null, msg];\n}\n\nif (content === 'ALL LEDs ON') {\n    msg.payload = true;\n    // Route to output 1-4\n    return [msg, msg, msg, msg];\n}\nif (content === 'ALL LEDs OFF') {\n    msg.payload = false;\n    // Route to output 1-4\n    return [msg, msg, msg, msg];\n}\n\n\n\nreturn null;","outputs":4,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":260,"wires":[["7285f33b22258e53"],["38f4e014de0658f6"],["41c06dba6c2f63a0"],["be68d0fe89c4fb10"]]},{"id":"b7fcc81b89152e02","type":"telegram sender","z":"8c2053d55f5883f8","name":"","bot":"457874f8aa8a857a","haserroroutput":false,"outputs":1,"x":510,"y":360,"wires":[[]]},{"id":"ede1cfbc300148a2","type":"debug","z":"8c2053d55f5883f8","name":"debug 371","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":490,"y":400,"wires":[]},{"id":"81ada557b64dc604","type":"template","z":"8c2053d55f5883f8","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\"chatId\": 7965218469,\n\"type\":\"message\",\n\"content\":\"{{payload}}\"}","output":"json","x":290,"y":360,"wires":[["b7fcc81b89152e02","ede1cfbc300148a2"]]},{"id":"e3993085c203bdba","type":"comment","z":"8c2053d55f5883f8","name":"MQTT 設定","info":"**const char* mqtt_broker = \"mqttgo.io\";\nconst int mqtt_port = 1883;\nconst char* mqtt_client_id = \"alex9ufo-wokwi-client-dualcore\";\n\n// MQTT 主題\nconst char* led_control_topic = \"alex9ufo/ledcontrol\";\nconst char* led_status_topic = \"alex9ufo/ledstatus\";\nconst char* temp_humi_topic = \"alex9ufo/temphumi\"; ","x":110,"y":360,"wires":[]},{"id":"38f4e014de0658f6","type":"ui_led","z":"8c2053d55f5883f8","order":2,"group":"9123b13576984e5e","width":3,"height":3,"label":"LED2","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED2","x":570,"y":220,"wires":[]},{"id":"41c06dba6c2f63a0","type":"ui_led","z":"8c2053d55f5883f8","order":3,"group":"9123b13576984e5e","width":3,"height":3,"label":"LED3","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED3","x":570,"y":260,"wires":[]},{"id":"be68d0fe89c4fb10","type":"ui_led","z":"8c2053d55f5883f8","order":4,"group":"9123b13576984e5e","width":3,"height":3,"label":"LED4","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"LED4","x":570,"y":300,"wires":[]},{"id":"3f077821c764e91d","type":"debug","z":"8c2053d55f5883f8","name":"debug 372","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":770,"y":20,"wires":[]},{"id":"5df891b22fcf0d78","type":"ui_text","z":"8c2053d55f5883f8","group":"9123b13576984e5e","order":5,"width":0,"height":0,"name":"","label":"LED狀態","format":"{{msg.payload}}","layout":"row-left","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":340,"y":320,"wires":[]},{"id":"fa7ececa4e716634","type":"mqtt in","z":"8c2053d55f5883f8","name":"alex9ufo/temphumi","topic":"alex9ufo/temphumi","qos":"1","datatype":"auto-detect","broker":"192c2b20bef1e71a","nl":false,"rap":true,"rh":0,"inputs":0,"x":110,"y":560,"wires":[["3a41c8a502d904a4","bdfcb0188f2a45d1","093cc1f43bb2193f"]]},{"id":"3a41c8a502d904a4","type":"template","z":"8c2053d55f5883f8","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\"chatId\": 7965218469,\n\"type\":\"message\",\n\"content\":\"{{payload}}\"}","output":"json","x":530,"y":560,"wires":[["6aa0d1e45462c3ef"]]},{"id":"6aa0d1e45462c3ef","type":"telegram sender","z":"8c2053d55f5883f8","name":"","bot":"457874f8aa8a857a","haserroroutput":false,"outputs":1,"x":710,"y":560,"wires":[[]]},{"id":"d20c7a1c503d7b74","type":"debug","z":"8c2053d55f5883f8","name":"debug 373","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":330,"y":200,"wires":[]},{"id":"bdfcb0188f2a45d1","type":"function","z":"8c2053d55f5883f8","name":" 分離 溫度 濕度","func":"// 取得原始字串,例如 \"Temperature: 5.00C, Humidity: 88.50%\"\nvar input = msg.payload.toString();\n\n// 使用正規表示法抓取數值\n// ([-+]?\\d*\\.?\\d+) 負責抓取包含正負號、整數與小數點的數字\nvar tempMatch = input.match(/Temperature:\\s*([-+]?\\d*\\.?\\d+)/);\nvar humiMatch = input.match(/Humidity:\\s*(\\d*\\.?\\d+)/);\n\nvar msgTemp = null;\nvar msgHumi = null;\n\n// 處理溫度 (輸出端 1)\nif (tempMatch) {\n    var temp = parseFloat(tempMatch[1]);\n    // 檢查是否在指定範圍內 -40 到 +80\n    if (temp >= -40 && temp <= 80) {\n        msgTemp = { \n            payload: temp, \n            unit: \"C\",\n            topic: \"sensor/temperature\" \n        };\n    } else {\n        node.warn(\"溫度數值異常: \" + temp);\n    }\n}\n\n// 處理濕度 (輸出端 2)\nif (humiMatch) {\n    var humi = parseFloat(humiMatch[1]);\n    msgHumi = { \n        payload: humi, \n        unit: \"%\",\n        topic: \"sensor/humidity\" \n    };\n}\n\n// 依照 [輸出 1, 輸出 2] 的順序回傳\nreturn [msgTemp, msgHumi];","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":760,"wires":[["253c856683bddece","fef1f76b83d92299"],["f8e06b4a74159f3b","4ec634057e02a3b6"]]},{"id":"253c856683bddece","type":"ui_text","z":"8c2053d55f5883f8","group":"873c37d1e2856b34","order":1,"width":3,"height":1,"name":"","label":"溫度:","format":"{{msg.payload}}℃","layout":"row-left","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":530,"y":720,"wires":[]},{"id":"f8e06b4a74159f3b","type":"ui_text","z":"8c2053d55f5883f8","group":"873c37d1e2856b34","order":2,"width":3,"height":1,"name":"","label":"濕度","format":"{{msg.payload}} %","layout":"row-left","className":"","style":false,"font":"","fontSize":16,"color":"#000000","x":530,"y":800,"wires":[]},{"id":"fef1f76b83d92299","type":"ui_gauge","z":"8c2053d55f5883f8","name":"","group":"873c37d1e2856b34","order":3,"width":6,"height":3,"gtype":"compass","title":"溫度","label":"℃","format":"{{value}}","min":"-40","max":"80","colors":["#00b500","#e6e600","#ca3838"],"seg1":"20","seg2":"40","diff":false,"className":"","x":530,"y":680,"wires":[]},{"id":"4ec634057e02a3b6","type":"ui_gauge","z":"8c2053d55f5883f8","name":"","group":"873c37d1e2856b34","order":4,"width":6,"height":3,"gtype":"gage","title":"濕度:","label":"%","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"45","seg2":"60","diff":false,"className":"","x":530,"y":840,"wires":[]},{"id":"b4f570bf787abca2","type":"function","z":"8c2053d55f5883f8","name":"(溫度 > 30°C 與 > 70°C) ","func":"// 1. 取得原始字串:Temperature: 75.00C, Humidity: 88.50%\nvar input = msg.payload;\nvar message = \"\";\n\n// 2. 使用正規表示法抓取溫度數值\nvar tempMatch = input.match(/Temperature: ([\\d.]+)/);\nvar humiMatch = input.match(/Humidity: ([\\d.]+)/);\n\nif (tempMatch) {\n    var temp1 = parseFloat(tempMatch[1]); // 將字串轉為數字 75.00\n   \n    // 3. 判斷溫度門檻 (先判斷最高的,避免重複觸發)\n    if (temp1 > 70) {\n        message = \"🔥 【緊急火警警報】偵測到極高溫!\\n目前溫度:\" + temp1 + \"°C\\n請立刻採取行動!\";\n    }\n    else if (temp1 > 30) {\n        message = \"⚠️ 【高溫提醒】環境溫度過高。\\n目前溫度:\" + temp1 + \"°C\\n請注意散熱。\";\n    }\n}\nif (humiMatch) {\n    var temp2 = parseFloat(humiMatch[1]); // 將字串轉為數字 75.00\n\n    // 3. 判斷溫度門檻 (先判斷最高的,避免重複觸發)\n    if (temp2 > 80) {\n        message = \"🌧️ 【緊急警報】偵測到濕度不正常!\\n目前濕度:\" + temp2 + \"%\\n請立刻採取行動!\";\n    }\n    else if (temp2 <10) {\n        message = \"🌧️ 【緊急警報】偵測到濕度不正常。\\n目前濕度:\" + temp2 + \"%\\n請注意散熱。\";\n    }\n}\n\nmsg.payload=message;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":500,"y":620,"wires":[["3a41c8a502d904a4","24a5e9920dbfad71"]]},{"id":"093cc1f43bb2193f","type":"delay","z":"8c2053d55f5883f8","name":"","pauseType":"delay","timeout":"2","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":300,"y":620,"wires":[["b4f570bf787abca2"]]},{"id":"24a5e9920dbfad71","type":"play audio","z":"8c2053d55f5883f8","name":"","voice":"","x":710,"y":640,"wires":[]},{"id":"d5ee749083c5c5a2","type":"telegram sender","z":"8c2053d55f5883f8","name":"","bot":"457874f8aa8a857a","haserroroutput":false,"outputs":1,"x":530,"y":500,"wires":[[]]},{"id":"86f6b5808a905449","type":"template","z":"8c2053d55f5883f8","name":"Telegram 歡迎詞","field":"payload","fieldType":"msg","format":"handlebars","syntax":"plain","template":"{\"chatId\": 7965218469,\n\"type\":\"message\",\n\"content\":\"歡迎 Alex 使用 WOKWI&Node-red+Telegram控制系統\\n/1on  : 開啟 LED1\\n/1off : 關閉 LED1\\n/2on  : 開啟 LED2\\n/2off : 關閉 LED2\\n/3on  : 開啟 LED3\\n/3off : 關閉 LED3\\n/4on  : 開啟 LED4\\n/4off : 關閉 LED4\\n/allon  : 開啟 LED1-LED4\\n/alloff : 關閉 LED1-LED4\\n/WOKWI 的 DHT22 輸出 採用 手動方式 要按enter\\n\"}","output":"json","x":300,"y":500,"wires":[["d5ee749083c5c5a2"]]},{"id":"47e254e9be34dfbc","type":"inject","z":"8c2053d55f5883f8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":500,"wires":[["86f6b5808a905449"]]},{"id":"85fff350d422330f","type":"debug","z":"8c2053d55f5883f8","name":"debug 375","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":470,"y":80,"wires":[]},{"id":"9a49877e65ede3d6","type":"play audio","z":"8c2053d55f5883f8","name":"","voice":"","x":330,"y":160,"wires":[]},{"id":"457874f8aa8a857a","type":"telegram bot","botname":"@alextest999_bot","usernames":"","chatids":"7965218469","baseapiurl":"","testenvironment":false,"updatemode":"polling","pollinterval":"300","usesocks":false,"sockshost":"","socksprotocol":"socks5","socksport":"6667","socksusername":"anonymous","sockspassword":"","bothost":"","botpath":"","localbothost":"0.0.0.0","localbotport":"8443","publicbotport":"8443","privatekey":"","certificate":"","useselfsignedcertificate":false,"sslterminated":false,"verboselogging":false},{"id":"192c2b20bef1e71a","type":"mqtt-broker","name":"mqttgo","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":"9123b13576984e5e","type":"ui_group","name":"LED","tab":"ed8b3742f5900395","order":2,"disp":true,"width":"6","collapse":false,"className":""},{"id":"873c37d1e2856b34","type":"ui_group","name":"DHT22","tab":"ed8b3742f5900395","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"ed8b3742f5900395","type":"ui_tab","name":"2026_09_EX4","icon":"dashboard","disabled":false,"hidden":false}]




它專門負責接收來自 Telegram 的文字指令,並將其轉換成 MQTT 訊息發送給 Wokwi 裡的 ESP32。

以下是該流程(Flow)的詳細解析:


程式節點解析

1. 指令接收端 (Telegram Receiver)

  • 節點名稱telegram receiver

  • 功能:監聽您的 Telegram 機器人(@alextest999_bot)。當您在手機輸入任何文字時,這個節點會抓取訊息物件。

  • 關鍵屬性filterCommands: true。這代表它會特別留意以 / 開頭的指令。

2. 資料預處理 (function 節點 - eb8234cf9675d55a)

  • 功能:提取純文字內容。

  • 邏輯:Telegram 傳來的原始資料是一個複雜的物件(包含使用者名稱、時間、ID等),這段代碼 var content = msg.payload.content; 只取出您輸入的指令文字(例如 /1on),並將其重新存入 msg.payload 中,方便後續判斷。


if (msg.payload === "/1on") {
    msg.topic = "alex9ufo/ledcontrol";
    msg.payload = "1on";  // 開啟 LED1
    return [msg , null];
} else if (msg.payload === "/1off") {
    msg.topic = "alex9ufo/ledcontrol";
    msg.payload = "1off"; // 關閉 LED1
    return [msg , null];
} else if (msg.payload === "/2on") {
    msg.topic = "alex9ufo/ledcontrol";
    msg.payload = "2on"; // 開啟 LED2
    return [msg , null];
} else if (msg.payload === "/2off") {
    msg.topic = "alex9ufo/ledcontrol";
    msg.payload = "2off"; // 關閉 LED2
    return [msg , null];
} else if (msg.payload === "/3on") {
    msg.topic = "alex9ufo/ledcontrol";
    msg.payload = "3on"; // 開啟 LED3
    return [msg , null];
} else if (msg.payload === "/3off") {
    msg.topic = "alex9ufo/ledcontrol";
    msg.payload = "3off"; // 關閉 LED3
    return [msg , null];
} else if (msg.payload === "/4on") {
    msg.topic = "alex9ufo/ledcontrol";
    msg.payload = "4on"; // 開啟 LED4
    return [msg , null];
} else if (msg.payload === "/4off") {
    msg.topic = "alex9ufo/ledcontrol";
    msg.payload = "4off"; // 關閉 LED4
    return [msg , null];
} else if (msg.payload === "/allon") {
    msg.topic = "alex9ufo/ledcontrol";
    msg.payload = "allon"; // 開啟 all LED1-LED4
    return [msg , null];
} else if (msg.payload === "/alloff") {
    msg.topic = "alex9ufo/ledcontrol";
    msg.payload = "alloff"; // 關閉 all LED1-LED4
    return [msg , null];
} else if  (msg.payload === "/WOKWI") {
    msg.topic = "alex9ufo/ledcontrol";
    msg.payload = "readData";  // 讀取DHT22溫度濕度
    return [msg, null];
}

//function 根據指令來發佈 MQTT 訊息

3. 指令判斷中心 (function 節點 - 60bd77976d9b25a8)

這是整個流程的大腦,負責**「翻譯」**工作。它將 Telegram 的斜線指令轉換成 ESP32 能理解的簡單字串:

Telegram 指令發佈到 MQTT 的內容 (Payload)動作說明
/1on/4on1on ... 4on開啟指定的 LED 1~4
/1off/4off1off ... 4off關閉指定的 LED 1~4
/allonallon開啟所有 LED
/alloffalloff關閉所有 LED
/WOKWIreadData觸發 ESP32 讀取 DHT22 數據
  • 輸出設計:這個節點設有 2 個輸出端,但目前程式碼主要回傳 [msg, null],代表訊息會從第一個輸出端流向 MQTT。

4. 指令發送端 (MQTT Out)

  • 主題 (Topic)alex9ufo/ledcontrol

  • 功能:將翻譯好的指令(如 readData)送到 mqttgo.io 伺服器。

  • 設定qos: 1 確保指令至少送達一次,retain: true 則是讓之後連線的 ESP32 也能收到最後一次的狀態指令。

程式碼是系統中的**「狀態反饋與顯示中心」**。它的主要工作是監聽來自 ESP32 的狀態回報訊息(LED 到底是開還是關),然後更新 Dashboard 上的燈號,並同步傳送訊息到您的 Telegram 手機上。

這是一個非常完整的雙向溝通循環。以下是逐行與逐節點的詳細說明:


1. 數據入口:MQTT In 節點

  • 節點名稱alex9ufo/ledstatus

  • 功能:這是系統的耳朵。它訂閱了 MQTT Broker (mqttgo.io) 上的主題。

  • 運作:當 Wokwi 的 ESP32 成功切換 LED 後,會發送如 "LED1 ON""ALL LEDs OFF" 的字串,這個節點會第一時間接收到這些文字。


2. 核心邏輯:Function 節點 (c4c0948da43caba8)

這是此流程的大腦,負責將文字訊號轉換為布林值 (True/False) 並分流:

  • 分流設計:它設定了 4 個輸出端,分別對應 4 顆 LED。

  • 邏輯範例

    • 收到 "LED1 ON":將 msg.payload 設為 true,並從 第 1 個 輸出端送出。

    • 收到 "LED1 OFF":將 msg.payload 設為 false,從 第 1 個 輸出端送出。

  • 全開/全關處理

    • 收到 "ALL LEDs ON":會將 true 同時發送到 所有 4 個輸出端,讓介面上的燈一次全亮。

  • 關鍵代碼return [msg, null, null, null] 代表只讓第一路有訊號,其餘保持靜默。

var content = msg.payload;

if (content === 'LED1 ON') {
    msg.payload = true;
    // Route to output 1
    return [msg, null, null, null ];
}
if (content === 'LED1 OFF') {
    msg.payload = false;
    // Route to output 1
    return [msg, null, null, null];
}
if (content === 'LED2 ON') {  
    msg.payload = true;
    // Route to output 2
    return [null, msg, null, null];
}
if (content === 'LED2 OFF') {
    msg.payload = false;
    // Route to output 2
    return [null, msg, null, null];
}
if (content === 'LED3 ON') {  
    msg.payload = true;
    // Route to output 3
    return [null,  null, msg, null];
}
if (content === 'LED3 OFF') {
    msg.payload = false;
    // Route to output 3
    return [null, null, msg, null];
}
if (content === 'LED4 ON') {
    msg.payload = true;
    // Route to output 4
    return [null, null, null, msg];
}
if (content === 'LED4 OFF') {
    msg.payload = false;
    // Route to output 4
    return [null, null, null, msg];
}

if (content === 'ALL LEDs ON') {
    msg.payload = true;
    // Route to output 1-4
    return [msg, msg, msg, msg];
}
if (content === 'ALL LEDs OFF') {
    msg.payload = false;
    // Route to output 1-4
    return [msg, msg, msg, msg];
}



return null;

3. 視覺化顯示:UI LED 節點

  • 包含節點LED1 (7285f33b), LED2, LED3, LED4

  • 功能:在您的 Node-RED Dashboard 網頁上顯示圓形指示燈。

  • 設定

    • 當收到 true:顯示為綠色 (#008000)。

    • 當收到 false:顯示為紅色 (#ff0000)。

  • 群組:全部歸類在名為 LED 的群組,放在 2026_09_EX4 分頁中。


4. Telegram 同步:Template + Telegram Sender

這是讓您的手機同步收到狀態通知的部分:

  • Template 節點 (81ada557b64dc604)

    • 將 MQTT 傳來的純文字(如 "LED1 ON")包裝成 Telegram 專用的 JSON 格式。

    • 設定 chatId: 7965218469,確保訊息傳給您正確的帳號。

  • Telegram Sender 節點 (b7fcc81b89152e02)

    • 負責將包裝好的 JSON 訊息正式發送到您的手機。


5. 輔助功能節點

  • UI Text 節點 (5df891b22fcf0d78):在 Dashboard 上用文字直接顯示「LED1 ON」等字樣,方便閱讀。

  • Play Audio 節點 (9a49877e65ede3d6):這很有趣!每當狀態改變時,您的瀏覽器會發出聲音提醒,這就是您之前想做的「警報聲」基礎。

  • Debug 節點:在側邊欄顯示 Log,方便您檢查 MQTT 連線是否正常。


流程總結與圖解

  1. ESP32 發送 "LED1 ON"

  2. MQTT In 接收訊息。

  3. Function 判斷這是 LED1,將其轉為 true 並從第 1 路輸出。

  4. Dashboard 上的 LED1 燈泡由紅轉綠。

  5. Telegram 同步彈出視窗顯示:「LED1 ON」。



程式碼的功能是系統的**「自動歡迎與說明書發佈器」**。每當 Node-RED 程式啟動或您重新部署(Deploy)時,它會自動發送一則包含完整指令清單的訊息到您的 Telegram 手機上,讓您不需要背誦指令。

以下是針對這四個節點的逐行與運作邏輯說明:


1. 觸發源:Inject 節點 (47e254e9be34dfbc)

  • 關鍵設定once: trueonceDelay: 0.1

  • 功能說明:這是流程的開關。

    • 正常情況下,Inject 節點需要手動點擊才會執行。

    • 但因為您勾選了「啟動後立即執行(Inject once after 0.1 seconds)」,所以每當 Node-RED 伺服器啟動,它就會自動發出一個訊號給下一個節點。


2. 內容產生器:Template 節點 (86f6b5808a905449)

  • 名稱Telegram 歡迎詞

  • 格式JSON 輸出。

  • 功能說明:這是訊息的「樣板」。它將您想看到的文字包裝成 Telegram 節點能讀懂的格式。

  • 內容解析

    • chatId: 7965218469(確保訊息精確傳送到 Alex 的手機)。

    • type: "message"(告知這是一則純文字訊息)。

    • content: 這是最核心的部分,包含了:

      • 歡迎詞歡迎 Alex 使用 WOKWI&Node-red+Telegram控制系統

      • 指令表:列出從 /1on/alloff 的所有可用指令。

      • 提示語:提醒您 Wokwi 端的 DHT22 讀取需要按 Enter。

    • 換行符號:代碼中的 \n 代表在手機顯示時會自動換行,讓排版整齊、易於閱讀。


3. 發送執行端:Telegram Sender 節點 (d5ee749083c5c5a2)

  • 功能說明:這是最後的發射台。

  • 運作邏輯:它接收 Template 節點產生的 JSON 物件,並透過您設定好的機器人帳號(@alextest999_bot),經由網際網路將這份「操作手冊」發送到您的 Telegram App 中。


4. 機器人設定:Telegram Bot 節點 (457874f8aa8a857a)

  • 功能說明:這不是畫布上的實體流程節點,而是背景的配置檔(Configuration Node)。

  • 設定細節

    • 儲存了您的 Bot Token(金鑰)。

    • 設定了 Polling Interval(300ms),代表機器人每 0.3 秒會去 Telegram 伺服器檢查有沒有人傳新訊息。


流程運作圖解

  1. Node-RED 啟動

  2. Inject 節點 倒數 0.1 秒後發出訊號。

  3. Template 節點 準備好「歡迎 Alex...」這串長文字。

  4. Telegram Sender 透過網路將文字推送到您的手機。

  5. Alex 的手機 響起「叮」一聲,完整的指令選單出現在畫面。


Node-RED 程式碼是您系統中的**「數據監測與警報中心」**。它負責接收溫濕度、進行數據解析(拆分數字)、顯示在 Dashboard 儀表板,並在數值異常(過熱或太濕)時觸發 Telegram 與聲音報警。

以下是針對每個區塊的逐行解析:


1. 數據入口:MQTT In 節點 (fa7ececa4e716634)

  • 主題 (Topic)alex9ufo/temphumi

  • 功能:監聽 ESP32 發送的溫濕度字串,格式通常為:Temperature: 25.00C, Humidity: 60.00%


2. 視覺化核心:數據分離 Function (bdfcb0188f2a45d1)

這是負責將原始字串拆解為「純數字」的關鍵節點,設定為 2 個輸出端

  • 正規表示法解析:使用 input.match 抓取 Temperature:Humidity: 後面的數字。

  • 範圍限制:設定溫度必須在 -40°C 到 +80°C 之間才算有效,否則會發出 node.warn 警告。

  • 輸出 1:傳送純溫度數字給 Dashboard 的 溫度計 (Gauge)文字 (Text) 節點。

  • 輸出 2:傳送純濕度數字給 濕度計


3. 警報決策中心:高溫/濕度判斷 Function (b4f570bf787abca2)

這個節點負責檢查數值是否超標:

  • 溫度邏輯

    • > 70°C:產生「🔥 緊急火警警報」文字。

    • > 30°C:產生「⚠️ 高溫提醒」文字。

  • 濕度邏輯

    • > 80% 或 < 10%:產生「🌧️ 濕度不正常」警報文字。

  • 延遲處理 (093cc1f43bb2193f):在判斷前加了 2 秒延遲,避免數據剛進來時太過頻繁觸發警報。


4. 警報發佈端

  • Telegram 同步 (3a41c8a502d904a4):將產生的警告文字封裝成 JSON 格式,並傳送給 Telegram Sender

  • 聲音報警 (24a5e9920dbfad71):當判斷 Function 有訊息輸出時,同步觸發 Play Audio,讓您的電腦喇叭響起(這就是您想要的救護車/警報音效觸發點)。


5. Dashboard 介面元件 (UI)

  • 溫度 Gauge (fef1f76b83d92299):採用 Compass (羅盤型) 顯示溫度,範圍 -40 到 80。

  • 濕度 Gauge (4ec634057e02a3b6):採用標準儀表型,範圍 0 到 100,並用顏色區分(綠、黃、紅)。

  • UI Text 節點:在儀表下方顯示精確的數字與單位。


流程運作圖解

  1. ESP32 發送:Temperature: 75.00C, Humidity: 85.00%

  2. 分離 Function:把數字 75 給溫度計,數字 85 給濕度計。

  3. 判斷 Function

    • 偵測到 75 > 70,生成「火警警報」訊息。

    • 偵測到 85 > 80,生成「濕度異常」訊息。

  4. 執行警報:手機 Telegram 響起通知,電腦喇叭發出警報聲。



沒有留言:

張貼留言

2026 作業4 WOKWI ESP32 DHT22+4LED & Node-Red , Telegram , MQTT 練習

2026 作業4   WOKWI ESP32 DHT22+4LED & Node-Red , Telegram , MQTT 練習 Node-Red Telegram 設定需修改  TOKEN API  與 CHATID  才會指向你的Telegram   IoT 遠端控...