2025年7月9日 星期三

ESP32 雙核心控制 LED 與 DHT22 溫濕度感測器 (Wokwi 模擬) EX6 -- Node-Red + SQLite

ESP32 雙核心控制 LED 與 DHT22 溫濕度感測器 (Wokwi 模擬) EX6 -- Node-Red + SQLite




WOKWI ESP32程式

#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <DHT_U.h> // Need to include DHT_U.h for DHT_Unified compatibility

// --- Wi-Fi Configuration ---
const char* ssid = "Wokwi-GUEST"; // For Wokwi simulation
const char* password = "";        // For Wokwi-GUEST, password is empty

// --- MQTT Broker Configuration ---
const char* mqtt_server = "broker.mqttgo.io"; // Or "mqtt.eclipseprojects.io"
const int mqtt_port = 1883;
// IMPORTANT: Use a unique MQTT client ID for your ESP32
const char* mqtt_client_id = "ESP32_Wokwi_IoT_YourName_001"; // <<< Change to your unique ID!

// --- MQTT Topics (MUST match Python Tkinter app) ---
const char* MQTT_TOPIC_LED_CONTROL = "esp32/led/control";   // Tkinter publishes LED commands here
const char* MQTT_TOPIC_TEMPERATURE = "esp32/dht/temperature"; // ESP32 publishes temperature here
const char* MQTT_TOPIC_HUMIDITY = "esp32/dht/humidity";     // ESP32 publishes humidity here
const char* MQTT_TOPIC_STATUS = "esp32/status";         // ESP32 publishes online status (optional)

// --- WiFi and MQTT Client Objects ---
WiFiClient espClient;
PubSubClient client(espClient);

// --- LED Configuration ---
const int ledPin = 2; // Connect to ESP32 GPIO 2
enum LedMode { OFF, ON, FLASH, TIMER }; // OFF as default/safe state
volatile LedMode currentLedMode = OFF;
volatile unsigned long timerStartTime = 0;
volatile bool ledState = false; // For flash mode

// --- DHT22 Sensor Configuration ---
#define DHTPIN 4      // Connect to ESP32 GPIO 4
#define DHTTYPE DHT22 // DHT 22 (AM2302)
DHT dht(DHTPIN, DHTTYPE);

// --- FreeRTOS Task Handles ---
TaskHandle_t TaskLEDControl = NULL;
TaskHandle_t TaskDHTSensor = NULL;

// --- Function Declarations ---
void setup_wifi();
void reconnect_mqtt();
void mqtt_callback(char* topic, byte* payload, unsigned int length);
void ledControlTask(void *pvParameters);
void dhtSensorTask(void *pvParameters);

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW); // Ensure LED is off initially

  Serial.println("\n--- ESP32 Starting Up ---");
  Serial.println("Connecting to Wi-Fi...");
  setup_wifi(); // Connect to Wi-Fi

  client.setServer(mqtt_server, mqtt_port); // Set MQTT Broker
  client.setCallback(mqtt_callback);       // Set MQTT message callback

  dht.begin(); // Initialize DHT sensor

  // Create LED Control Task on Core 0
  xTaskCreatePinnedToCore(
    ledControlTask,    /* Task function */
    "LED Control",     /* Task name */
    2048,              /* Stack size (bytes) */
    NULL,              /* Task parameters */
    1,                 /* Task priority */
    &TaskLEDControl,   /* Task handle */
    0                  /* Run on Core 0 */
  );
  Serial.println("LED Control task created on Core 0.");

  // Create DHT Sensor Task on Core 1
  xTaskCreatePinnedToCore(
    dhtSensorTask,     /* Task function */
    "DHT Sensor",      /* Task name */
    4096,              /* Stack size (bytes) */
    NULL,              /* Task parameters */
    1,                 /* Task priority */
    &TaskDHTSensor,    /* Task handle */
    1                  /* Run on Core 1 */
  );
  Serial.println("DHT Sensor task created on Core 1.");
  Serial.println("--- Setup Complete ---");
  Serial.println("Waiting for MQTT connection...");
}

void loop() {
  // Main loop keeps MQTT connection alive and processes messages
  if (!client.connected()) {
    reconnect_mqtt();
  }
  client.loop(); // Process incoming and outgoing MQTT messages
  delay(10); // Short delay to prevent busy-waiting
}

// --- Wi-Fi Connection Function ---
void setup_wifi() {
  delay(10);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi Connected!");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
}

// --- MQTT Reconnection Function ---
void reconnect_mqtt() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect(mqtt_client_id)) {
      Serial.println("Connected!");
      // Once connected, subscribe to LED control topic
      client.subscribe(MQTT_TOPIC_LED_CONTROL);
      Serial.print("Subscribed to: ");
      Serial.println(MQTT_TOPIC_LED_CONTROL);

      // Optional: Publish online status
      client.publish(MQTT_TOPIC_STATUS, "ESP32_online");
      Serial.println("Published ESP32 online status.");
      Serial.println("\n--- Tkinter Control Hints ---");
      Serial.println("Use the Tkinter application to send commands:");
      Serial.println("  'on' - Turn LED ON");
      Serial.println("  'off' - Turn LED OFF");
      Serial.println("  'flash' - Make LED flash");
      Serial.println("  'timer' - Turn LED ON for 10 seconds");
      Serial.println("----------------------------------");
    } else {
      Serial.print("Failed, rc=");
      Serial.print(client.state());
      Serial.println(" 5 seconds to retry...");
      delay(5000); // Wait 5 seconds before retrying
    }
  }
}

// --- MQTT Message Callback Function ---
// This function is called when a message is received on a subscribed topic
void mqtt_callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("\nMessage received [");
  Serial.print(topic);
  Serial.print("] ");
  String message = "";
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.println(message);

  // Check if it's an LED control command
  if (String(topic) == MQTT_TOPIC_LED_CONTROL) {
    if (message == "on") {
      currentLedMode = ON;
      digitalWrite(ledPin, HIGH);
      Serial.println("LED Control: ON (from Tkinter)");
    } else if (message == "off") {
      currentLedMode = OFF;
      digitalWrite(ledPin, LOW);
      Serial.println("LED Control: OFF (from Tkinter)");
    } else if (message == "flash") {
      currentLedMode = FLASH;
      Serial.println("LED Control: FLASH (from Tkinter)");
    } else if (message == "timer") {
      currentLedMode = TIMER;
      digitalWrite(ledPin, HIGH); // Turn on LED when timer mode starts
      timerStartTime = millis();
      Serial.println("LED Control: TIMER (10 seconds, from Tkinter)");
    } else {
      Serial.println("Unknown LED command from Tkinter.");
    }
  }
}

// --- LED Control Task (runs on Core 0) ---
void ledControlTask(void *pvParameters) {
  (void) pvParameters; // Avoid compiler warning

  for (;;) { // Infinite loop
    switch (currentLedMode) {
      case ON:
        // LED stays on, state set by mqtt_callback
        break;
      case OFF:
        // LED stays off, state set by mqtt_callback
        break;
      case FLASH:
        digitalWrite(ledPin, ledState); // Toggle LED state
        ledState = !ledState;           // Reverse state for next toggle
        vTaskDelay(pdMS_TO_TICKS(500)); // Delay for 500ms
        break;
      case TIMER:
        if (millis() - timerStartTime >= 10000) { // Check if 10 seconds have passed
          digitalWrite(ledPin, LOW);
          currentLedMode = OFF; // Switch to OFF mode after timer
          Serial.println("LED Timer ended. LED OFF.");
        }
        vTaskDelay(pdMS_TO_TICKS(10)); // Small delay to yield CPU
        break;
      default:
        digitalWrite(ledPin, LOW); // Default to off
        break;
    }
    vTaskDelay(pdMS_TO_TICKS(10)); // Short delay to allow other tasks to run
  }
}

// --- DHT Sensor Task (runs on Core 1) ---
void dhtSensorTask(void *pvParameters) {
  (void) pvParameters; // Avoid compiler warning

  for (;;) { // Infinite loop
    // Read and publish periodically
    float h = dht.readHumidity();
    float t = dht.readTemperature(); // Read temperature in Celsius

    // Check if any reads failed
    if (isnan(h) || isnan(t)) {
      Serial.println(F("Failed to read from DHT sensor! Retrying..."));
    } else {
      Serial.print(F("DHT Reading: Humidity: "));
      Serial.print(h);
      Serial.print(F("%  Temperature: "));
      Serial.print(t);
      Serial.println(F("°C"));

      // Publish Temperature
      char tempString[8];
      dtostrf(t, 4, 2, tempString); // Convert float to string (4 total digits, 2 after decimal)
      client.publish(MQTT_TOPIC_TEMPERATURE, tempString);

      // Publish Humidity
      char humString[8];
      dtostrf(h, 4, 2, humString);
      client.publish(MQTT_TOPIC_HUMIDITY, humString);
    }
    vTaskDelay(pdMS_TO_TICKS(20000)); // Publish data every 10 seconds (adjustable)
  }}

Node-Red程式

[{"id":"f55075677ed35293","type":"mqtt in","z":"bb5d172d168249ce","name":"Temperature In","topic":"esp32/dht/temperature","qos":"0","datatype":"auto-detect","broker":"450dc18180d64bb4","nl":false,"rap":true,"rh":0,"inputs":0,"x":140,"y":400,"wires":[["61b4d08432d56a31","c0d12e12613d965f"]]},{"id":"e44d567086ce7b54","type":"mqtt in","z":"bb5d172d168249ce","name":"Humidity In","topic":"esp32/dht/humidity","qos":"0","datatype":"auto-detect","broker":"450dc18180d64bb4","nl":false,"rap":true,"rh":0,"inputs":0,"x":120,"y":480,"wires":[["8929e5e3ae1f7377","2547cb81ca53ca94"]]},{"id":"b3e02f06b6b797fc","type":"ui_button","z":"bb5d172d168249ce","name":"LED ON","group":"2708b73a218d6a6a","order":1,"width":2,"height":1,"passthru":false,"label":"ON","tooltip":"","color":"","bgcolor":"green","icon":"","payload":"on","payloadType":"str","topic":"esp32/led/control","topicType":"str","x":100,"y":60,"wires":[["6b653a6976693822","2b611e3b624f923c"]]},{"id":"6b653a6976693822","type":"mqtt out","z":"bb5d172d168249ce","name":"LED Control Out","topic":"esp32/led/control","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"450dc18180d64bb4","x":340,"y":160,"wires":[]},{"id":"61b4d08432d56a31","type":"ui_text","z":"bb5d172d168249ce","group":"c1f20dd979d554a9","order":1,"width":4,"height":1,"name":"Temperature","label":"Temperature:","format":"{{msg.payload}} °C","layout":"row-spread","className":"","x":330,"y":440,"wires":[]},{"id":"8929e5e3ae1f7377","type":"ui_text","z":"bb5d172d168249ce","group":"c1f20dd979d554a9","order":2,"width":4,"height":1,"name":"Humidity","label":"Humidity:","format":"{{msg.payload}} %","layout":"row-spread","className":"","x":320,"y":520,"wires":[]},{"id":"b025d57d54e6015b","type":"ui_button","z":"bb5d172d168249ce","name":"LED OFF","group":"2708b73a218d6a6a","order":2,"width":2,"height":1,"passthru":false,"label":"OFF","tooltip":"","color":"","bgcolor":"red","icon":"","payload":"off","payloadType":"str","topic":"esp32/led/control","topicType":"str","x":100,"y":120,"wires":[["6b653a6976693822","dfa07159c3a3780a"]]},{"id":"1894d30b65f3f00e","type":"ui_button","z":"bb5d172d168249ce","name":"LED FLASH","group":"2708b73a218d6a6a","order":3,"width":2,"height":1,"passthru":false,"label":"FLASH","tooltip":"","color":"","bgcolor":"blue","icon":"","payload":"flash","payloadType":"str","topic":"esp32/led/control","topicType":"str","x":110,"y":180,"wires":[["6b653a6976693822","d9061099684784cf"]]},{"id":"764ef1a25ee82103","type":"ui_button","z":"bb5d172d168249ce","name":"LED TIMER","group":"2708b73a218d6a6a","order":4,"width":2,"height":1,"passthru":false,"label":"TIMER (10s)","tooltip":"","color":"","bgcolor":"orange","icon":"","payload":"timer","payloadType":"str","topic":"esp32/led/control","topicType":"str","x":110,"y":260,"wires":[["6b653a6976693822","e9569651a243e8ea"]]},{"id":"6f028889988229b4","type":"ui_text","z":"bb5d172d168249ce","group":"c711a31b7e5108ce","order":1,"width":8,"height":1,"name":"Current Time","label":"","format":"<span style=\"font-weight:bold;\">{{msg.payload}}</span>","layout":"row-spread","className":"","x":510,"y":680,"wires":[]},{"id":"fb5896a7d5705300","type":"inject","z":"bb5d172d168249ce","name":"Current Time","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":true,"onceDelay":"0.1","topic":"","payload":"","payloadType":"date","x":100,"y":680,"wires":[["b4482b5d4f208c14"]]},{"id":"b4482b5d4f208c14","type":"function","z":"bb5d172d168249ce","name":"Format Date/Time","func":"const now = new Date(msg.payload);\nconst date = now.toLocaleDateString();\nconst time = now.toLocaleTimeString();\nmsg.payload = `${date} ${time}`;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":680,"wires":[["6f028889988229b4"]]},{"id":"2b611e3b624f923c","type":"function","z":"bb5d172d168249ce","name":"Log LED ON","func":"const now = new Date();\nconst date = now.toISOString().split('T')[0];\nconst time = now.toTimeString().split(' ')[0];\nvar ledon=\"LED ON command sent\";\nmsg.topic = \"INSERT INTO events (date, time, event) VALUES ($date, $time ,$ledon)\";\nmsg.payload = [date, time, ledon];\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":60,"wires":[["c060205f27bb1c2c","a19001309855523f"]]},{"id":"dfa07159c3a3780a","type":"function","z":"bb5d172d168249ce","name":"Log LED OFF","func":"const now = new Date();\nconst date = now.toISOString().split('T')[0];\nconst time = now.toTimeString().split(' ')[0];\nvar ledoff = \"LED OFF command sent\";\nmsg.topic = \"INSERT INTO events (date, time, event) VALUES ($date, $time, $ledoff)\";\nmsg.payload = [date, time, ledoff];\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":120,"wires":[["c060205f27bb1c2c","a19001309855523f"]]},{"id":"d9061099684784cf","type":"function","z":"bb5d172d168249ce","name":"Log LED FLASH","func":"const now = new Date();\nconst date = now.toISOString().split('T')[0];\nconst time = now.toTimeString().split(' ')[0];\nvar ledflash = \"LED FLASH command sent\";\nmsg.topic = \"INSERT INTO events (date, time, event) VALUES ($date, $time, $ledflash)\";\nmsg.payload = [date, time, ledflash];\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":220,"wires":[["c060205f27bb1c2c","a19001309855523f"]]},{"id":"e9569651a243e8ea","type":"function","z":"bb5d172d168249ce","name":"Log LED TIMER","func":"const now = new Date();\nconst date = now.toISOString().split('T')[0];\nconst time = now.toTimeString().split(' ')[0];\nvar ledtime = \"LED TIMER (10s) command sent\";\nmsg.topic = \"INSERT INTO events (date, time, event) VALUES ($date, $time, $ledtime )\";\nmsg.payload = [date, time, ledtime ];\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":280,"wires":[["c060205f27bb1c2c","a19001309855523f"]]},{"id":"c0d12e12613d965f","type":"function","z":"bb5d172d168249ce","name":"Log Temperature","func":"const now = new Date();\nconst date = now.toISOString().split('T')[0];\nconst time = now.toTimeString().split(' ')[0];\nvar temp=\"Received Temperature: \" + msg.payload + \"°C\";\nmsg.topic = \"INSERT INTO events (date, time, event) VALUES ($date, $time, $temp)\";\nmsg.payload = [date, time, temp];\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":350,"y":400,"wires":[["a19001309855523f","2baf2855da70384e"]]},{"id":"f8101413a17e0b5a","type":"function","z":"bb5d172d168249ce","name":"Log Humidity","func":"const now = new Date();\nconst date = now.toISOString().split('T')[0];\nconst time = now.toTimeString().split(' ')[0];\nvar humid = \"Received Humidity:\"+msg.payload+\"%\" ;\nmsg.topic = \"INSERT INTO events (date, time, event) VALUES ($date, $time, $humid)\";\nmsg.payload = [date, time, humid ];\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":450,"y":480,"wires":[["a19001309855523f","2baf2855da70384e"]]},{"id":"c060205f27bb1c2c","type":"sqlite","z":"bb5d172d168249ce","mydb":"8e0edb919c30c852","sqlquery":"msg.topic","sql":"{{msg.topic}}","name":"SQLite DB","x":650,"y":340,"wires":[["6f2abbdf8483d7d0"]]},{"id":"a19001309855523f","type":"ui_text","z":"bb5d172d168249ce","group":"c711a31b7e5108ce","order":2,"width":8,"height":2,"name":"Event Log Display","label":"","format":"<pre>{{msg.payload}}</pre>","layout":"row-spread","className":"","x":630,"y":200,"wires":[]},{"id":"1f8e6c7020a6e2e5","type":"inject","z":"bb5d172d168249ce","name":"Refresh DB","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"0.1","topic":"SELECT id, date, time, event FROM events ORDER BY id DESC","payload":"","payloadType":"date","x":110,"y":580,"wires":[["4b600969d2753a47"]]},{"id":"63f4581ed6b567b4","type":"ui_table","z":"bb5d172d168249ce","group":"d0554c9c222ff4ed","name":"Database View","order":1,"width":10,"height":8,"columns":[{"field":"id","title":"ID碼","width":"","align":"left","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"date","title":"日期","width":"","align":"left","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"time","title":"時間","width":"","align":"left","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"event","title":"事件","width":"","align":"left","formatter":"plaintext","formatterParams":{"target":"_blank"}}],"outputs":0,"cts":false,"x":680,"y":580,"wires":[]},{"id":"4b600969d2753a47","type":"sqlite","z":"bb5d172d168249ce","mydb":"8e0edb919c30c852","sqlquery":"msg.topic","sql":"{{msg.topic}}","name":"Query DB","x":440,"y":580,"wires":[["63f4581ed6b567b4"]]},{"id":"060d4b1a8d05ddac","type":"inject","z":"bb5d172d168249ce","name":"Create Table Once","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"2","topic":"CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, time TEXT NOT NULL, event TEXT NOT NULL)","payload":"","payloadType":"date","x":150,"y":340,"wires":[["c060205f27bb1c2c"]]},{"id":"dca2454b18f096ba","type":"comment","z":"bb5d172d168249ce","name":"esp32.db TABLE","info":"CREATE TABLE events \n(id INTEGER PRIMARY KEY AUTOINCREMENT, \ndate TEXT NOT NULL, \ntime TEXT NOT NULL, \nevent TEXT NOT NULL)","x":640,"y":300,"wires":[]},{"id":"6f2abbdf8483d7d0","type":"debug","z":"bb5d172d168249ce","name":"debug 344","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":810,"y":340,"wires":[]},{"id":"2547cb81ca53ca94","type":"delay","z":"bb5d172d168249ce","name":"","pauseType":"delay","timeout":"3","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":300,"y":480,"wires":[["f8101413a17e0b5a"]]},{"id":"2baf2855da70384e","type":"sqlite","z":"bb5d172d168249ce","mydb":"8e0edb919c30c852","sqlquery":"msg.topic","sql":"{{msg.topic}}","name":"SQLite DB","x":650,"y":400,"wires":[[]]},{"id":"2a4e76d0edf315a6","type":"inject","z":"bb5d172d168249ce","name":"Current Time","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"5","crontab":"","once":true,"onceDelay":"0.1","topic":"","payload":"","payloadType":"date","x":100,"y":620,"wires":[["e1de99b9f8718bcf"]]},{"id":"e1de99b9f8718bcf","type":"function","z":"bb5d172d168249ce","name":"function  query","func":"msg.topic= \"SELECT id, date, time, event FROM events ORDER BY id DESC\";\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":280,"y":620,"wires":[["4b600969d2753a47"]]},{"id":"450dc18180d64bb4","type":"mqtt-broker","name":"broker.mqttgo.io","broker":"broker.mqttgo.io","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"2708b73a218d6a6a","type":"ui_group","name":"LED Control","tab":"273a502422f98f98","order":1,"disp":true,"width":4,"collapse":false,"className":""},{"id":"c1f20dd979d554a9","type":"ui_group","name":"DHT22 Sensor Data","tab":"273a502422f98f98","order":2,"disp":true,"width":4,"collapse":false,"className":""},{"id":"c711a31b7e5108ce","type":"ui_group","name":"Event Log","tab":"273a502422f98f98","order":3,"disp":true,"width":8,"collapse":false,"className":""},{"id":"8e0edb919c30c852","type":"sqlitedb","db":"esp32.db","mode":"RWC"},{"id":"d0554c9c222ff4ed","type":"ui_group","name":"Database Browser","tab":"273a502422f98f98","order":4,"disp":true,"width":10,"collapse":false,"className":""},{"id":"273a502422f98f98","type":"ui_tab","name":"ESP32 IoT Dashboard","icon":"dashboard","order":1,"disabled":false,"hidden":false}]

















 Node-RED 流程的 JSON 配置,清晰地展示了 Node-RED 與 ESP32 之間透過 MQTT Broker 進行的 雙向控制與數據交換關係


Node-RED 與 ESP32 的控制關係分析

這個 Node-RED 流程的核心是利用 MQTT (Message Queuing Telemetry Transport) 協定作為 ESP32 和 Node-RED 之間溝通的橋樑。MQTT 是一種輕量級的發布/訂閱 (Publish/Subscribe) 訊息協定,非常適合 IoT 應用。

1. MQTT Broker 配置

  • id: "450dc18180d64bb4"

  • type: "mqtt-broker"

  • name: "broker.mqttgo.io"

  • broker: "broker.mqttgo.io"

  • port: "1883"

    • 這是整個系統的中央通訊樞紐。Node-RED 和你的 ESP32 都會連接到這個 MQTT Broker (broker.mqttgo.io,預設端口 1883)。

    • MQTT Broker 負責接收來自發布者的訊息,並將這些訊息轉發給所有訂閱了相關主題的客戶端。

    • ESP32 的角色: 作為 MQTT 客戶端,它會發布 (Publish) DHT22 感測器數據到特定主題,並訂閱 (Subscribe) 特定主題來接收 LED 控制命令。

    • Node-RED 的角色: 同樣作為 MQTT 客戶端,它會訂閱 ESP32 發布的感測器數據主題,並發布 LED 控制命令到 ESP32 訂閱的主題。


2. 從 ESP32 接收數據 (感測器數據)

  • id: "f55075677ed35293" (Temperature In)

  • id: "e44d567086ce7b54" (Humidity In)

  • type: "mqtt in"

  • topic: "esp32/dht/temperature" / "esp32/dht/humidity"

    • 這兩個是 MQTT 輸入 (mqtt in) 節點,它們負責從 MQTT Broker 接收訊息。

    • Temperature In 節點訂閱了 esp32/dht/temperature 這個 MQTT 主題。當 ESP32 模擬器中的 DHT22 感測器發布溫度數據到這個主題時,Node-RED 就會接收到這個訊息。

    • Humidity In 節點訂閱了 esp32/dht/humidity 這個 MQTT 主題,接收濕度數據。

    • 控制關係 (數據流):

      • ESP32 發布 (Publish) 數據: ESP32 讀取 DHT22 溫濕度後,會將數據分別發布到 esp32/dht/temperatureesp32/dht/humidity

      • Node-RED 接收數據: Node-RED 中的這些 mqtt in 節點會訂閱這些主題,從而接收到 ESP32 發送過來的溫濕度值。

    • 後續處理:

      • 收到的數據會傳遞給 ui_text 節點 (61b4d08432d56a318929e5e3ae1f7377),用於在 Node-RED Dashboard 上實時顯示溫濕度數值。

      • 同時,數據也會傳遞給 function 節點 (c0d12e12613d965ff8101413a17e0b5a),這些 function 節點會處理數據並準備 SQL 語句,以便將溫濕度事件記錄到 SQLite 資料庫中(透過 sqlite 節點)。


3. 控制 ESP32 (LED 控制)

  • id: "b3e02f06b6b797fc" (LED ON)

  • id: "b025d57d54e6015b" (LED OFF)

  • id: "1894d30b65f3f00e" (LED FLASH)

  • id: "764ef1a25ee82103" (LED TIMER)

  • type: "ui_button"

  • topic: "esp32/led/control"

  • payload: "on" / "off" / "flash" / "timer"

    • 這些是 Node-RED Dashboard 上的 按鈕 (ui_button) 節點。當用戶在網頁儀表板上點擊這些按鈕時,它們會生成一個訊息 (msg)。

    • 每個按鈕都配置了特定的 payload (例如 "on", "off", "flash", "timer") 和相同的 topic (esp32/led/control)。

  • id: "6b653a6976693822" (LED Control Out)

  • type: "mqtt out"

  • topic: "esp32/led/control"

    • 這是一個 MQTT 輸出 (mqtt out) 節點,它負責將訊息發布到 MQTT Broker。

    • 當任何一個 LED 控制按鈕被點擊時,其訊息會傳遞到這個 mqtt out 節點。

    • mqtt out 節點會將訊息的 payload (即 "on", "off", "flash", "timer") 發布到 esp32/led/control 這個主題上。

    • 控制關係 (控制流):

      • Node-RED 發布 (Publish) 命令: 用戶在 Node-RED Dashboard 上點擊按鈕,Node-RED 將對應的控制命令(例如 "on")發布到 esp32/led/control 主題。

      • ESP32 訂閱 (Subscribe) 命令: ESP32 的 Arduino 程式碼會訂閱 esp32/led/control 這個 MQTT 主題。當它收到此主題的訊息時,會解析 payload (例如 "on"),然後根據內容執行相應的操作,例如點亮 LED、關閉 LED、讓 LED 閃爍或定時器操作。

    • 後續處理:

      • 按鈕被點擊後,除了發送 MQTT 命令,訊息還會傳遞給不同的 function 節點 (2b611e3b624f923c, dfa07159c3a3780a 等)。這些 function 節點會創建對應的日誌訊息,並將其記錄到 SQLite 資料庫中,同時顯示在 Dashboard 的事件日誌上。


4. 資料庫記錄與瀏覽 (輔助 ESP32 控制的長期監控)

雖然這部分不直接控制 ESP32,但它與 ESP32 的數據和命令流緊密相關,提供了監控和歷史查詢的能力。

  • function 節點 (Log LED ON/OFF/FLASH/TIMER, Log Temperature, Log Humidity):

    • 這些節點負責將 MQTT 訊息(無論是控制命令還是感測器數據)轉換為標準的事件日誌格式,並準備好插入 SQLite 資料庫的 SQL 語句和參數。

  • sqlite 節點 (c060205f27bb1c2c, 2baf2855da70384e):

    • 接收來自 function 節點的 SQL 語句和數據,將其寫入 esp32.db 檔案中的 events 表格。

  • ui_text 節點 (a19001309855523f - Event Log Display):

    • 接收 function 節點生成的日誌文本,並在 Node-RED Dashboard 上實時顯示最新的事件。

  • inject 節點 (1f8e6c7020a6e2e5 - Refresh DB) 和 function 節點 (e1de99b9f8718bcf - function query):

    • 當用戶點擊 "Refresh DB" 按鈕時,inject 節點會觸發一個流程。

    • function query 節點會設置一個 SQL SELECT 語句 (SELECT id, date, time, event FROM events ORDER BY id DESC),用於查詢 events 表格中的所有歷史數據。

  • sqlite 節點 (4b600969d2753a47 - Query DB):

    • 執行 SELECT 語句,從 esp32.db 中讀取歷史事件數據。

  • ui_table 節點 (63f4581ed6b567b4 - Database View):

    • 接收 Query DB 節點返回的數據,並以格式化的表格形式顯示在 Node-RED Dashboard 上,實現了數據庫瀏覽功能。

  • inject 節點 (060d4b1a8d05ddac - Create Table Once):

    • 在 Node-RED 啟動時執行一次,用於確保 events 表格在 SQLite 資料庫中已經存在。


總結控制關係

這整個 Node-RED 流程與 ESP32 之間建立了基於 MQTT 的完整雙向通訊

  • Node-RED 控制 ESP32 (命令下發):

    • Node-RED Dashboard 上的按鈕 (例如 "LED ON") -> 發布 MQTT 訊息到 esp32/led/control 主題 -> ESP32 訂閱此主題並執行相應的 LED 操作。

  • ESP32 向 Node-RED 報告 (數據上傳):

    • ESP32 感測器 (例如 DHT22) 讀取數據 -> 發布 MQTT 訊息到 esp32/dht/temperatureesp32/dht/humidity 主題 -> Node-RED 訂閱這些主題並在 Dashboard 上顯示數據,同時記錄到資料庫。

這種架構實現了一個健壯且可擴展的 IoT 系統,Node-RED 充當了用戶界面、數據處理和持久化層,而 ESP32 則作為邊緣設備負責物理世界的互動和數據採集。 

沒有留言:

張貼留言

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...