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 節點,請先進行安裝。
點擊 Node-RED 介面右上角的漢堡選單 (☰),選擇 Manage palette。
切換到 Install 標籤。
在搜尋框中輸入 node-red-dashboard。
找到它後,點擊右邊的 install 按鈕。
步驟 2: 建立 Node-RED 流程 (Flow)
我們將建立兩個獨立的流程,一個用於處理溫度,另一個用於處理濕度。這有助於保持流程清晰。
流程 1: 溫度資料 (Temperature Data)
從左側面板拖曳一個 mqtt in 節點到流程中。
雙擊該節點進行設定:
拖曳一個 function 節點到流程中,並將其連接到 mqtt in 節點。雙擊進行編輯,並將其 Name 設定為 格式化溫度。在 On Message 標籤中,輸入以下程式碼。這段程式碼會將接收到的字串轉換為數字。
拖曳一個 ui_gauge 節點(儀表)到流程中,並將其連接到 function 節點。雙擊進行設定:
拖曳一個 ui_chart 節點(圖表)到流程中,並將其連接到 function 節點。雙擊進行設定:
Group: 選擇之前建立的 DHT22 分組。
Type: Line chart
Label: 溫度歷史
x-axis label: 時間
y-axis label: 溫度 (°C)
Legend: 勾選 Show legend。
拖曳一個 ui_text 節點(文字)到流程中,並將其連接到 function 節點。雙擊進行設定:
流程 2: 濕度資料 (Humidity Data)
拖曳另一個 mqtt in 節點到流程中。
雙擊進行設定:
拖曳一個 function 節點,連接到 mqtt in 節點。Name 設定為 格式化濕度。在 On Message 標籤中,輸入以下程式碼:
拖曳一個 ui_gauge 節點到流程中,並將其連接到 function 節點。雙擊進行設定:
Group: 選擇 DHT22 分組。
Type: Gauge
Label: 濕度
Units: %
Min: 0
Max: 100
拖曳一個 ui_chart 節點到流程中,並將其連接到 function 節點。雙擊進行設定:
Group: 選擇 DHT22 分組。
Type: Line chart
Label: 濕度歷史
x-axis label: 時間
y-axis label: 濕度 (%)
Legend: 勾選 Show legend。
拖曳一個 ui_text 節點到流程中,並將其連接到 function 節點。雙擊進行設定:
步驟 3: 部署並查看 Dashboard
點擊 Node-RED 介面右上角的 Deploy 按鈕來部署你的流程。
部署成功後,點擊右側面板的 Dashboard 圖示 (小儀表板圖示)。
點擊 Dashboard 標籤旁邊的彈出式按鈕(一個帶箭頭的小方塊),這將會在一個新視窗或標籤頁中打開你的儀表板。
現在,當你的 ESP32 開始發布 MQTT 訊息時,你應該能在 Node-RED 的 Dashboard 上即時看到溫度和濕度的數值、儀表板和圖表的變化。
你可以直接複製這段 JSON 程式碼,然後在 Node-RED 介面中進行匯入。
如何匯入此 JSON 流程
複製下面完整的 JSON 程式碼。
打開 Node-RED 介面,點擊右上角的漢堡選單 (☰)。
選擇 Import,然後點擊 Clipboard。
將複製的 JSON 程式碼貼入文字框中。
點擊 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"
}
}
]
沒有留言:
張貼留言