2025年8月6日 星期三

Wokwi ESP32 Simulator : ESP32 + DHT22+MQTT+Python TKinet + Node-RED Dashboard

Wokwi ESP32 Simulator :   ESP32 + DHT22+MQTT+Python TKinet + Node-RED Dashboard










import tkinter as tk

from tkinter import ttk

import paho.mqtt.client as mqtt

import threading

import time


# --- MQTT Configuration ---

MQTT_BROKER = "broker.mqttgo.io"

MQTT_PORT = 1883

MQTT_TOPIC_TEMP = "esp32/dht/temperature"

MQTT_TOPIC_HUM = "esp32/dht/humidity"


# --- Tkinter GUI Class ---

class MqttDhtApp(tk.Tk):

    def __init__(self):

        super().__init__()

        self.title("MQTT DHT22 Monitor")

        self.geometry("400x300")


        self.last_temp = None

        self.last_hum = None

        self.is_flashing = False

        self.flash_start_time = 0

        self.flash_duration = 5000  # 5 seconds in milliseconds

        

        # UI Elements

        self.label_title = ttk.Label(self, text="DHT22 Sensor Data", font=("Helvetica", 16, "bold"))

        self.label_title.pack(pady=10)


        self.label_temp = ttk.Label(self, text="Temperature: -- °C", font=("Helvetica", 14))

        self.label_temp.pack(pady=5)


        self.label_hum = ttk.Label(self, text="Humidity: -- %", font=("Helvetica", 14))

        self.label_hum.pack(pady=5)


        # LED Indicator

        self.canvas_led = tk.Canvas(self, width=50, height=50)

        self.canvas_led.pack(pady=20)

        self.led_circle = self.canvas_led.create_oval(10, 10, 40, 40, outline="black", fill="gray")


        # MQTT Client Setup

        self.client = mqtt.Client()

        self.client.on_connect = self.on_connect

        self.client.on_message = self.on_message

        self.client.connect_async(MQTT_BROKER, MQTT_PORT, 60)

        

        # Start the MQTT network loop in a separate thread

        self.mqtt_thread = threading.Thread(target=self.client.loop_forever)

        self.mqtt_thread.daemon = True

        self.mqtt_thread.start()


    def on_connect(self, client, userdata, flags, rc):

        if rc == 0:

            print("Connected to MQTT Broker!")

            self.client.subscribe(MQTT_TOPIC_TEMP)

            self.client.subscribe(MQTT_TOPIC_HUM)

            print(f"Subscribed to topics: {MQTT_TOPIC_TEMP} and {MQTT_TOPIC_HUM}")

        else:

            print(f"Failed to connect, return code {rc}")


    def on_message(self, client, userdata, msg):

        topic = msg.topic

        payload = msg.payload.decode()

        

        print(f"Received message: {topic} -> {payload}")

        

        # Use a flag to check for data changes

        changed = False


        if topic == MQTT_TOPIC_TEMP:

            try:

                current_temp = float(payload)

                if self.last_temp is None or current_temp != self.last_temp:

                    self.label_temp.config(text=f"Temperature: {current_temp:.2f} °C")

                    self.last_temp = current_temp

                    changed = True

            except ValueError:

                print("Invalid temperature payload")

        

        elif topic == MQTT_TOPIC_HUM:

            try:

                current_hum = float(payload)

                if self.last_hum is None or current_hum != self.last_hum:

                    self.label_hum.config(text=f"Humidity: {current_hum:.2f} %")

                    self.last_hum = current_hum

                    changed = True

            except ValueError:

                print("Invalid humidity payload")


        # If data changed, start the flashing animation

        if changed and not self.is_flashing:

            self.flash_start_time = time.time() * 1000  # Get current time in milliseconds

            self.is_flashing = True

            # Use self.after to start the flashing loop

            self.after(0, self.flash_led)


    def flash_led(self):

        # Check if 5 seconds have passed

        if (time.time() * 1000) - self.flash_start_time < self.flash_duration:

            current_color = self.canvas_led.itemcget(self.led_circle, "fill")

            new_color = "red" if current_color == "gray" else "gray"

            self.canvas_led.itemconfig(self.led_circle, fill=new_color)

            # Schedule the next flash after 200ms

            self.after(200, self.flash_led)

        else:

            # Flashing is over, reset the state

            self.canvas_led.itemconfig(self.led_circle, fill="gray")

            self.is_flashing = False


if __name__ == "__main__":

    app = MqttDhtApp()

    app.mainloop()





#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) ---
// Removed MQTT_TOPIC_LED_CONTROL as LED functionality is removed
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);

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

// --- FreeRTOS Task Handles ---
// Removed TaskHandle_t TaskLEDControl as LED functionality is removed
TaskHandle_t TaskDHTSensor = NULL;

// --- Function Declarations ---
void setup_wifi();
void reconnect_mqtt();
void mqtt_callback(char* topic, byte* payload, unsigned int length);
// Removed ledControlTask function declaration as LED functionality is removed
void dhtSensorTask(void *pvParameters);

void setup() {
  Serial.begin(115200);
  // Removed LED pin setup: pinMode(ledPin, OUTPUT); digitalWrite(ledPin, LOW);

 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

 // Removed LED Control Task creation
 // xTaskCreatePinnedToCore(ledControlTask, "LED Control", 2048, NULL, 1, &TaskLEDControl, 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!");
      // Removed LED control topic subscription
      // 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("This ESP32 is now publishing DHT22 sensor data.");
      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);

  // Removed LED control logic
  // if (String(topic) == MQTT_TOPIC_LED_CONTROL) { ... }
}

// --- Removed LED Control Task (was on Core 0) ---
// void ledControlTask(void *pvParameters) { ... }

// --- 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(10000)); // Publish data every 10 seconds (adjustable)
  }
}




在 Node-RED 中接收 ESP32 發送的溫度和濕度資料,並將其顯示在 Dashboard 上是一個常見且直觀的任務。你需要使用 MQTT 節點來訂閱 ESP32 發布的主題,然後將資料傳遞給儀表板(Dashboard)節點。

以下是詳細的步驟和節點配置。


步驟 1: 安裝 Node-RED Dashboard 節點

如果你尚未安裝 Node-RED 的 Dashboard 節點,請先進行安裝。

  1. 點擊 Node-RED 介面右上角的漢堡選單 (☰),選擇 Manage palette

  2. 切換到 Install 標籤。

  3. 在搜尋框中輸入 node-red-dashboard

  4. 找到它後,點擊右邊的 install 按鈕。


步驟 2: 建立 Node-RED 流程 (Flow)

我們將建立兩個獨立的流程,一個用於處理溫度,另一個用於處理濕度。這有助於保持流程清晰。

流程 1: 溫度資料 (Temperature Data)

  1. 從左側面板拖曳一個 mqtt in 節點到流程中。

  2. 雙擊該節點進行設定:

    • Server: 點擊右邊的鉛筆圖示新增一個 MQTT 伺服器。

      • Server: broker.mqttgo.io

      • Port: 1883

      • 點擊 Add 完成設定。

    • Topic: esp32/dht/temperature (這必須與你的 ESP32 程式碼中的主題完全匹配)

    • QoS: 2

    • Name: 接收溫度

  3. 拖曳一個 function 節點到流程中,並將其連接到 mqtt in 節點。雙擊進行編輯,並將其 Name 設定為 格式化溫度。在 On Message 標籤中,輸入以下程式碼。這段程式碼會將接收到的字串轉換為數字。

    JavaScript
    msg.payload = parseFloat(msg.payload);
    return msg;
    
  4. 拖曳一個 ui_gauge 節點(儀表)到流程中,並將其連接到 function 節點。雙擊進行設定:

    • Group: 點擊右邊的鉛筆圖示新增一個 Dashboard 分組。

      • Name: DHT22

      • Tab: Sensors (這是儀表板上顯示的標籤名稱)

      • 點擊 Add

    • Type: Gauge

    • Label: 溫度

    • Units: °C

    • Min: 0

    • Max: 50

  5. 拖曳一個 ui_chart 節點(圖表)到流程中,並將其連接到 function 節點。雙擊進行設定:

    • Group: 選擇之前建立的 DHT22 分組。

    • Type: Line chart

    • Label: 溫度歷史

    • x-axis label: 時間

    • y-axis label: 溫度 (°C)

    • Legend: 勾選 Show legend

  6. 拖曳一個 ui_text 節點(文字)到流程中,並將其連接到 function 節點。雙擊進行設定:

    • Group: 選擇 DHT22 分組。

    • Label: 當前溫度

    • Value format: {{msg.payload}} °C

流程 2: 濕度資料 (Humidity Data)

  1. 拖曳另一個 mqtt in 節點到流程中。

  2. 雙擊進行設定:

    • Server: 選擇之前設定好的 broker.mqttgo.io

    • Topic: esp32/dht/humidity

    • Name: 接收濕度

  3. 拖曳一個 function 節點,連接到 mqtt in 節點。Name 設定為 格式化濕度。在 On Message 標籤中,輸入以下程式碼:

    JavaScript
    msg.payload = parseFloat(msg.payload);
    return msg;
    
  4. 拖曳一個 ui_gauge 節點到流程中,並將其連接到 function 節點。雙擊進行設定:

    • Group: 選擇 DHT22 分組。

    • Type: Gauge

    • Label: 濕度

    • Units: %

    • Min: 0

    • Max: 100

  5. 拖曳一個 ui_chart 節點到流程中,並將其連接到 function 節點。雙擊進行設定:

    • Group: 選擇 DHT22 分組。

    • Type: Line chart

    • Label: 濕度歷史

    • x-axis label: 時間

    • y-axis label: 濕度 (%)

    • Legend: 勾選 Show legend

  6. 拖曳一個 ui_text 節點到流程中,並將其連接到 function 節點。雙擊進行設定:

    • Group: 選擇 DHT22 分組。

    • Label: 當前濕度

    • Value format: {{msg.payload}} %


步驟 3: 部署並查看 Dashboard

  1. 點擊 Node-RED 介面右上角的 Deploy 按鈕來部署你的流程。

  2. 部署成功後,點擊右側面板的 Dashboard 圖示 (小儀表板圖示)。

  3. 點擊 Dashboard 標籤旁邊的彈出式按鈕(一個帶箭頭的小方塊),這將會在一個新視窗或標籤頁中打開你的儀表板。

現在,當你的 ESP32 開始發布 MQTT 訊息時,你應該能在 Node-RED 的 Dashboard 上即時看到溫度和濕度的數值、儀表板和圖表的變化。

你可以直接複製這段 JSON 程式碼,然後在 Node-RED 介面中進行匯入。

如何匯入此 JSON 流程

  1. 複製下面完整的 JSON 程式碼。

  2. 打開 Node-RED 介面,點擊右上角的漢堡選單 (☰)。

  3. 選擇 Import,然後點擊 Clipboard

  4. 將複製的 JSON 程式碼貼入文字框中。

  5. 點擊 Import 按鈕。


Node-RED 流程 JSON

[
    {
        "id": "c00539ac0ef8e744",
        "type": "mqtt in",
        "z": "8b8ef09eefeff005",
        "name": "接收溫度",
        "topic": "esp32/dht/temperature",
        "qos": "2",
        "datatype": "utf8",
        "broker": "b3e346f90ab0501a",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 100,
        "y": 80,
        "wires": [
            [
                "c080b06b00569889"
            ]
        ]
    },
    {
        "id": "c080b06b00569889",
        "type": "function",
        "z": "8b8ef09eefeff005",
        "name": "格式化溫度",
        "func": "msg.payload = parseFloat(msg.payload);\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 280,
        "y": 80,
        "wires": [
            [
                "2f8c5f6e80b435ff",
                "1c352a129f79b329",
                "6d7d5402a76f6259"
            ]
        ]
    },
    {
        "id": "2f8c5f6e80b435ff",
        "type": "ui_gauge",
        "z": "8b8ef09eefeff005",
        "name": "溫度",
        "group": "185e33d45e41e3d5",
        "order": 3,
        "width": 5,
        "height": 4,
        "gtype": "gage",
        "title": "溫度",
        "label": "℃",
        "format": "{{value}}",
        "min": "0",
        "max": "50",
        "colors": [
            "#00b500",
            "#e6e600",
            "#ca3838"
        ],
        "seg1": "",
        "seg2": "",
        "className": "",
        "x": 470,
        "y": 40,
        "wires": []
    },
    {
        "id": "1c352a129f79b329",
        "type": "ui_chart",
        "z": "8b8ef09eefeff005",
        "name": "",
        "group": "185e33d45e41e3d5",
        "order": 5,
        "width": 10,
        "height": 4,
        "label": "溫度歷史",
        "chartType": "line",
        "legend": "true",
        "xformat": "HH:mm:ss",
        "interpolate": "linear",
        "nodata": "",
        "dot": false,
        "ymin": "-40",
        "ymax": "80",
        "removeOlder": "1",
        "removeOlderPoints": "200",
        "removeOlderUnit": "3600",
        "cutout": 0,
        "useOneColor": false,
        "useUTC": false,
        "colors": [
            "#1f77b4",
            "#aec7e8",
            "#ff7f0e",
            "#2ca02c",
            "#98df8a",
            "#d62728",
            "#ff9896",
            "#9467bd",
            "#c5b0d5"
        ],
        "outputs": 1,
        "useDifferentColor": false,
        "className": "",
        "x": 480,
        "y": 80,
        "wires": [
            []
        ]
    },
    {
        "id": "6d7d5402a76f6259",
        "type": "ui_text",
        "z": "8b8ef09eefeff005",
        "group": "185e33d45e41e3d5",
        "order": 1,
        "width": 5,
        "height": 1,
        "name": "",
        "label": "當前溫度",
        "format": "{{msg.payload}} °C",
        "layout": "row-spread",
        "className": "",
        "x": 480,
        "y": 120,
        "wires": []
    },
    {
        "id": "060d403612574e44",
        "type": "mqtt in",
        "z": "8b8ef09eefeff005",
        "name": "接收濕度",
        "topic": "esp32/dht/humidity",
        "qos": "2",
        "datatype": "utf8",
        "broker": "b3e346f90ab0501a",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 100,
        "y": 240,
        "wires": [
            [
                "61b8f52288301712"
            ]
        ]
    },
    {
        "id": "61b8f52288301712",
        "type": "function",
        "z": "8b8ef09eefeff005",
        "name": "格式化濕度",
        "func": "msg.payload = parseFloat(msg.payload);\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 280,
        "y": 240,
        "wires": [
            [
                "4666f2178229f3d9",
                "8940562e1e988226",
                "8d47b068779b00de"
            ]
        ]
    },
    {
        "id": "4666f2178229f3d9",
        "type": "ui_gauge",
        "z": "8b8ef09eefeff005",
        "name": "",
        "group": "185e33d45e41e3d5",
        "order": 4,
        "width": 5,
        "height": 4,
        "gtype": "gage",
        "title": "濕度",
        "label": "%",
        "format": "{{value}}",
        "min": "0",
        "max": "100",
        "colors": [
            "#00b500",
            "#e6e600",
            "#ca3838"
        ],
        "seg1": "",
        "seg2": "",
        "className": "",
        "x": 470,
        "y": 200,
        "wires": []
    },
    {
        "id": "8940562e1e988226",
        "type": "ui_chart",
        "z": "8b8ef09eefeff005",
        "name": "",
        "group": "185e33d45e41e3d5",
        "order": 6,
        "width": 10,
        "height": 4,
        "label": "濕度歷史",
        "chartType": "line",
        "legend": "true",
        "xformat": "HH:mm:ss",
        "interpolate": "linear",
        "nodata": "",
        "dot": false,
        "ymin": "0",
        "ymax": "100",
        "removeOlder": "1",
        "removeOlderPoints": "200",
        "removeOlderUnit": "3600",
        "cutout": 0,
        "useOneColor": false,
        "useUTC": false,
        "colors": [
            "#1f77b4",
            "#aec7e8",
            "#ff7f0e",
            "#2ca02c",
            "#98df8a",
            "#d62728",
            "#ff9896",
            "#9467bd",
            "#c5b0d5"
        ],
        "outputs": 1,
        "useDifferentColor": false,
        "className": "",
        "x": 480,
        "y": 240,
        "wires": [
            []
        ]
    },
    {
        "id": "8d47b068779b00de",
        "type": "ui_text",
        "z": "8b8ef09eefeff005",
        "group": "185e33d45e41e3d5",
        "order": 2,
        "width": 5,
        "height": 1,
        "name": "",
        "label": "當前濕度",
        "format": "{{msg.payload}} %",
        "layout": "row-spread",
        "className": "",
        "x": 480,
        "y": 280,
        "wires": []
    },
    {
        "id": "b3e346f90ab0501a",
        "type": "mqtt-broker",
        "name": "broker.mqttgo.io",
        "broker": "broker.mqttgo.io",
        "port": "1883",
        "tls": "",
        "clientid": "",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": 4,
        "keepalive": 15,
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    },
    {
        "id": "185e33d45e41e3d5",
        "type": "ui_group",
        "name": "DHT22",
        "tab": "5950d24e10137ff7",
        "order": 1,
        "disp": true,
        "width": 10,
        "collapse": false,
        "className": ""
    },
    {
        "id": "5950d24e10137ff7",
        "type": "ui_tab",
        "name": "Sensors",
        "icon": "dashboard",
        "order": 1,
        "disabled": false,
        "hidden": false
    },
    {
        "id": "4cc3f691862b64e7",
        "type": "global-config",
        "env": [],
        "modules": {
            "node-red-dashboard": "3.6.5"
        }
    }
]

沒有留言:

張貼留言

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