IoT 智能控制面板
測試你的成果!
啟動 Wokwi 模擬: 點擊 Wokwi 的播放鍵,觀察序列埠(Serial Monitor),確認它成功連上
Wokwi-GUESTWiFi 並且顯示嘗試 MQTT 連線...已連線!。開啟網頁: 在電腦上雙擊打開修改好的
index.html。測試控制: 點擊網頁上的 LED 開關,你會看見 Wokwi 視窗裡的 ESP32 藍色燈(或外接 LED)同步亮起或熄滅!
測試接收: 在 Wokwi 中點擊 DHT22 元件,手動拉動滑桿改變溫度或濕度,約 5 秒內,你網頁上原本寫死的 Figma 數字就會即時更新。
這個架構就是現代智慧家居(如 Home Assistant)或工業物聯網(IIoT)的核心基礎。
網頁程式碼 (index.html)
請將以下內容完整複製,儲存為 index.html,直接雙擊檔案用瀏覽器開啟即可:
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IoT 智能控制面板 (Figma 實作版)</title>
<style>
/* 模擬 Figma 的基礎畫布與字型設定 */
:root {
--bg-color: #F5F5F7;
--card-bg: #FFFFFF;
--text-main: #1D1D1F;
--text-sub: #86868B;
--primary-green: #34C759;
--alert-red: #FF3B30;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-main);
margin: 0;
padding: 40px 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
box-sizing: border-box;
}
/* 面板主外殼 (Figma Frame) */
.dashboard {
width: 100%;
max-width: 480px;
background: var(--card-bg);
border-radius: 24px;
padding: 32px;
box-shadow: 0px 10px 30px rgba(0, 0, 0, 0.05);
}
/* 標題欄與連線狀態 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.header h1 {
font-size: 24px;
font-weight: 600;
margin: 0;
}
.status-badge {
display: flex;
align-items: center;
font-size: 14px;
color: var(--text-sub);
font-weight: 500;
}
.status-dot {
width: 8px;
height: 8px;
background-color: var(--alert-red);
border-radius: 50%;
margin-right: 8px;
transition: background-color 0.3s ease;
}
.status-dot.connected {
background-color: var(--primary-green);
}
/* 數據卡片佈局 (Grid) */
.grid-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 24px;
}
.card {
background: var(--bg-color);
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
}
.card-title {
font-size: 14px;
color: var(--text-sub);
margin-bottom: 8px;
font-weight: 500;
}
.card-value {
font-size: 28px;
font-weight: 700;
margin: 0;
}
/* 控制卡片 (全寬) */
.control-card {
background: var(--bg-color);
border-radius: 16px;
padding: 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.control-info h3 {
margin: 0 0 4px 0;
font-size: 18px;
font-weight: 600;
}
.control-info p {
margin: 0;
font-size: 13px;
color: var(--text-sub);
}
/* Figma 風格的 Toggle Switch 開關 */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: #D1D1D6;
transition: .3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 50%;
box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.15);
}
input:checked + .slider {
background-color: var(--primary-green);
}
input:checked + .slider:before {
transform: translateX(26px);
}
</style>
</head>
<body>
<div class="dashboard">
<!-- 頂部狀態 -->
<div class="header">
<h1>環境主控台</h1>
<div class="status-badge">
<span id="status-dot" class="status-dot"></span>
<span id="status-text">連線中...</span>
</div>
</div>
<!-- DHT22 溫濕度顯示區 -->
<div class="grid-container">
<div class="card">
<span class="card-title">🌡️ 當前溫度</span>
<p id="temp-text" class="card-value">--.- °C</p>
</div>
<div class="card">
<span class="card-title">💧 當前濕度</span>
<p id="hum-text" class="card-value">--.- %</p>
</div>
</div>
<!-- LED 控制區 -->
<div class="control-card">
<div class="control-info">
<h3>遠端 LED 節點</h3>
<p id="led-status-text">目前狀態:關閉</p>
</div>
<label class="switch">
<input type="checkbox" id="led-toggle" disabled>
<span class="slider"></span>
</label>
</div>
</div>
<!-- 核心:引入 MQTT.js 函式庫 -->
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
<script>
/*****************************************
* 1. 參數設定 (已更新為 alex9ufo 專屬代稱)
*****************************************/
const MQTT_BROKER = 'wss://broker.emqx.io:8084/mqtt';
const TOPIC_LED = 'alex9ufo/home/led';
const TOPIC_TEMP = 'alex9ufo/home/temperature';
const TOPIC_HUM = 'alex9ufo/home/humidity';
// 取得 DOM 元件
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
const tempText = document.getElementById('temp-text');
const humText = document.getElementById('hum-text');
const ledToggle = document.getElementById('led-toggle');
const ledStatusText = document.getElementById('led-status-text');
/*****************************************
* 2. 初始化 MQTT 連線
*****************************************/
console.log('正在嘗試連線至 MQTT Broker...');
const client = mqtt.connect(MQTT_BROKER);
// 連線成功事件
client.on('connect', () => {
console.log('成功連線至 EMQX Broker!');
statusDot.classList.add('connected');
statusText.innerText = '已連線';
ledToggle.disabled = false; // 開放按鈕操控
// 訂閱來自 Wokwi ESP32 的數據主題
client.subscribe([TOPIC_TEMP, TOPIC_HUM], (err) => {
if (!err) {
console.log('已成功訂閱溫濕度主題');
}
});
});
// 連線斷開事件
client.on('close', () => {
console.log('與 Broker 連線斷開');
statusDot.classList.remove('connected');
statusText.innerText = '連線斷開';
ledToggle.disabled = true;
});
/*****************************************
* 3. 接收數據 (接收從 Wokwi 傳來的 DHT22 資料)
*****************************************/
client.on('message', (topic, message) => {
const rawData = message.toString();
console.log(`收到主題 [${topic}]: ${rawData}`);
if (topic === TOPIC_TEMP) {
// 更新溫度
tempText.innerText = parseFloat(rawData).toFixed(1) + " °C";
} else if (topic === TOPIC_HUM) {
// 更新濕度
humText.innerText = parseFloat(rawData).toFixed(1) + " %";
}
});
/*****************************************
* 4. 發送控制 (點擊網頁開關時傳送給 Wokwi)
*****************************************/
ledToggle.addEventListener('change', (event) => {
const command = event.target.checked ? 'ON' : 'OFF';
client.publish(TOPIC_LED, command, { qos: 1 }, (err) => {
if (!err) {
console.log(`指令 [${command}] 已成功發送至主題: ${TOPIC_LED}`);
ledStatusText.innerText = `目前狀態:${command === 'ON' ? '開啟' : '關閉'}`;
} else {
console.error('指令發送失敗:', err);
}
});
});
</script>
</body>
</html>
程式碼是一個典型的 ESP32 物聯網(IoT)應用程式,運行於 Wokwi 模擬器環境中。它的核心功能是透過 Wi-Fi 連接到公共的 MQTT Broker (EMQX),將 DHT22 感測器量測到的溫度與濕度定時發送(Publish)出去,同時訂閱(Subscribe)控制主題,接收來自網頁端(Figma UI)的指令來切換 LED 的開關。
由於你傳送的文字中,程式碼重複貼了兩次,以下我將針對這份結構完整的程式碼進行逐行詳細解說:
📑 1. 引入函式庫與定義全域變數
#include <WiFi.h>
#include <PubSubClient.h>
#include <DHTesp.h> // 使用 DHTesp 函式庫
#include <WiFi.h>:引入 ESP32 的 Wi-Fi 核心函式庫,負責處理無線網路連線。#include <PubSubClient.h>:引入 MQTT 協定的函式庫,讓 ESP32 具備發布(Publish)與訂閱(Subscribe)訊息的功能。#include <DHTesp.h>:引入專為 ESP32 優化的 DHT 溫濕度感測器函式庫。
// --- 網路與 MQTT 伺服器設定 ---
const char* ssid = "Wokwi-GUEST"; // Wokwi 專用虛擬 WiFi
const char* password = "";
const char* mqtt_server = "broker.emqx.io";
const int mqtt_port = 1883;
ssid/password:設定 Wi-Fi 的帳號與密碼。Wokwi-GUEST是 Wokwi 模擬環境提供的專用虛擬基地台,不需密碼。mqtt_server:設定 MQTT 伺服器(Broker)的網址,這裡使用的是 EMQX 提供的免費公共測試伺服器。mqtt_port:設定 MQTT 的通訊埠,標準非加密的 MQTT 服務埠號為1883。
// 定義 MQTT 主題 (已更新為 alex9ufo 專屬代稱)
const char* topic_led = "alex9ufo/home/led";
const char* topic_temp = "alex9ufo/home/temperature";
const char* topic_hum = "alex9ufo/home/humidity";
這三行定義了通訊的「頻道名稱」(Topic)。網頁前端與硬體端必須對應相同的名稱才能互通:
topic_led:接收網頁端傳來控制 LED 開關的頻道。topic_temp:發送溫度數據的頻道。topic_hum:發送濕度數據的頻道。
#define LED_PIN 2
#define DHTPIN 15
使用巨集定義硬體腳位。將 ESP32 的 GPIO 2 定義為 LED 控制腳(通常也是內建藍色 LED 腳位),將 GPIO 15 定義為 DHT22 的數據訊號腳。
DHTesp dht; // 宣告 DHTesp 物件
WiFiClient espClient;
PubSubClient client(espClient);
unsigned long lastMsg = 0;
DHTesp dht;:實例化一個溫濕度感測器物件。WiFiClient espClient;:建立一個網路用戶端物件,負責底層的 TCP 連線。PubSubClient client(espClient);:建立 MQTT 用戶端物件,並將剛才的 Wi-Fi 連線傳入,讓 MQTT 透過 Wi-Fi 傳輸資料。lastMsg:宣告一個無號長整型變數,用來記錄上一次發送資料的時間(單位為毫秒),實現非阻塞式的定時器。
🌐 2. Wi-Fi 連線初始化函式
void setup_wifi() {
delay(10);
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected");
}
WiFi.begin(ssid, password);:啟動 ESP32 的 Wi-Fi 晶片並嘗試連線到指定的基地台。while (WiFi.status() != WL_CONNECTED):使用迴圈檢查連線狀態。如果尚未連線成功,程式會每隔 0.5 秒(delay(500))在序列埠監控視窗印出一個點(.),直到成功連上為止。
📩 3. MQTT 訊息接收回呼函式(Callback)
當網頁端(Figma UI)發送指令到 ESP32 訂閱的 alex9ufo/home/led 主題時,MQTT 伺服器會將訊息推播過來,自動觸發此函式:
void callback(char* topic, byte* payload, unsigned length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
String message;
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.println(message);
參數說明:
topic是接收到訊息的主題名稱;payload是傳過來的原始資料(位元組陣列);length是資料長度。for (int i = 0; i < length; i++):因為傳進來的資料是byte陣列,所以透過迴圈逐一將字元(char)累加到字串變數message中,轉換成看得懂的文字(例如 "ON" 或 "OFF")。
if (String(topic) == topic_led) {
if (message == "ON") {
digitalWrite(LED_PIN, HIGH);
} else if (message == "OFF") {
digitalWrite(LED_PIN, LOW);
}
}
}
if (String(topic) == topic_led):確認收到的訊息確實是來自控制 LED 的頻道。控制邏輯:如果文字內容為
"ON",執行digitalWrite(LED_PIN, HIGH)將 GPIO 2 輸出高電位,點亮 LED;若為"OFF",則輸出低電位,熄滅 LED。
🔄 4. MQTT 斷線重連函式
void reconnect() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// 隨機生成 Client ID 避免與他人衝突導致頻繁斷線
String clientId = "ESP32Client-" + String(random(0, 0xffff), HEX);
while (!client.connected()):如果與 MQTT 伺服器斷開連線,就會進入這個迴圈嘗試重連。clientId:公共 MQTT 伺服器規定每個客戶端的 Client ID 必須唯一。這裡利用random(0, 0xffff)產生隨機數並轉成十六進位字串,避免你的 ESP32 與其他人的裝置撞名而被剔除連線。
if (client.connect(clientId.c_str())) {
Serial.println("connected");
client.subscribe(topic_led); // 訂閱 LED 控制主題
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
delay(5000);
}
}
}
client.connect(...):嘗試向 MQTT 伺服器註冊並連線。連線成功:印出
connected,並立刻執行client.subscribe(topic_led),告訴伺服器「我要監聽 LED 控制頻道,有新訊息請發給我」。連線失敗:印出錯誤代碼(
rc),並等待 5 秒後重新進入迴圈嘗試。
⚙️ 5. 硬體與初始化設定(Setup)
void setup() {
pinMode(LED_PIN, OUTPUT);
Serial.begin(115200);
setup_wifi();
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
// 修正:DHTesp 的初始化寫法
dht.setup(DHTPIN, DHTesp::DHT22);
}
pinMode(LED_PIN, OUTPUT);:將 GPIO 2 設定為輸出模式。Serial.begin(115200);:初始化序列埠通訊,波特率設定為 115200,用於在電腦畫面上印出除錯訊息。setup_wifi();:呼叫前面寫好的函式連接無線網路。client.setServer(...):綁定 MQTT 伺服器位址與通訊埠。client.setCallback(callback);:向 MQTT 庫註冊回呼函式,指定當收到訊息時,由callback函式來處理。dht.setup(...):這是DHTesp函式庫特有的初始化方式,指定控制腳位為15,感測器型態為DHT22。
🔄 6. 主程式循環(Loop)
void loop() {
if (!client.connected()) {
reconnect();
}
client.loop();
if (!client.connected()):每次循環都檢查 MQTT 連線狀態,斷線就呼叫reconnect()。client.loop();:這是 MQTT 庫的核心函式,必須在loop()裡頻繁執行。它負責維持與伺服器的連線心跳(KeepAlive),並檢查是否有訂閱的訊息送達。
// 每 2 秒發送一次溫濕度
unsigned long now = millis();
if (now - lastMsg > 2000) {
lastMsg = now;
// 修正:DHTesp 讀取溫濕度的寫法
float h = dht.getHumidity();
float t = dht.getTemperature();
時間控制:
millis()會回傳系統開機至今的毫秒數。利用now - lastMsg > 2000來判斷是否過去了 2 秒。這種做法比使用delay(2000)更好,因為它不會讓晶片暫停運作,確保 MQTT 訊息接收不會卡頓。讀取數據:呼叫
dht.getHumidity()取得濕度,dht.getTemperature()取得溫度。
if (!isnan(h) && !isnan(t)) {
client.publish(topic_temp, String(t, 1).c_str()); // 格式化為小數點後1位
client.publish(topic_hum, String(h, 1).c_str());
Serial.print("Temp: "); Serial.print(t, 1);
Serial.print("°C | Hum: "); Serial.print(h, 1); Serial.println("%");
} else {
Serial.println("Failed to read from DHT sensor!");
}
}
}
!isnan(h) && !isnan(t):isnan代表 "Is Not a Number"。此處用來確認讀取出來的溫濕度數值是有效的,而非空值或錯誤訊號。client.publish(...):將數值轉成字串(String(t, 1)代表保留小數點後 1 位),然後轉成 C 語言格式字串(.c_str())發布到對應的 MQTT 頻道(topic_temp/topic_hum),網頁端便能即時接收並顯示。異常處理:若讀取失敗,則在序列埠印出錯誤訊息。



沒有留言:
張貼留言