提示詞
提供嵌入式系統物聯網程式開發,需求如下。開始寫程式前,根據需求,提供你的規劃給我確認。
#嵌入式系統硬體
##開發板
-NodeMCU-32S 開發板
-Keyes ESP-32 專用 GPIO 擴充板
##外接元件
-無
##接線需求
-無
#程式需求
##開發環境
-Arduino IDE 1.8.19
-ESP32 核心版本 3.1.1
## WiFi 連線資訊
-SSID: chuangHome
-PASSWORD: 0988087024
##建立 station mode wifi 連網
##連網成功後 會檢查網路連線,當網路連線失敗,重新自動建立連線
##連網成功後,建立 web server 服務,產生一個單頁響應式網頁
-整合 HTML, CSS, jQuery(使用 CDN 載入或非同步 Ajax 請求)等語法
-網頁內容提供1組開與關按鈕 用來開啟/關閉內建LED燈
-於開關按鈕動作結束後,讀取內建LED燈腳位的開關狀態,顯示在網頁上,讓使用者可以明確知道目前開關的狀態。回傳 JSON 格式數據(例如:{"GPIO25":ON, "GPIO25)":OFF})。
-網頁設計力求簡潔美觀、配色對比強,顯示文字以繁體中文或英文適當呈現
-網頁內容中提供完整詳細操作指導
##程式運作訊息 完整輸出至序列埠,速率 115200
##程式碼逐行以繁體中文註解
#程式安裝指導說明
##程式若使用第三方函式庫,須提供函式庫下載與安裝的詳細操作
##提供程式燒錄使用的詳細操作
複製 貼上 驗證
驗證錯誤
將錯誤訊息貼回 Gemini 迭代
複製貼上程式碼,編譯後出現錯誤
#錯誤訊息置於分隔線內
-----------------------------------------------------------
sendStatusJSON' was not declared in this scope
-----------------------------------------------------------
#修改程式
-先提供找出的錯誤原因與修正方向供我確認
-提供修改後的完整程式
複製 貼上 驗證正確 上傳
上傳完畢
更改速率,抓控制板 ip
開瀏覽器
網頁無法操作 LED 未正確反應
#include <WiFi.h> // 載入 ESP32 的 WiFi 基礎函式庫
#include <WebServer.h> // 載入 ESP32 的 Web Server 函式庫
// 定義 WiFi 連線資訊
const char* ssid = "chuangHome"; // WiFi 無線網路名稱 (SSID)
const char* password = "0988087024"; // WiFi 無線網路密碼
// 定義 NodeMCU-32S 內建 LED 控制腳位 (GPIO 2,高電位亮)
#define LED_PIN 2
// 建立 WebServer 物件,監聽埠號為 80 (HTTP 預設埠號)
WebServer server(80);
// 記錄上一次檢查網路連線的時間點 (用於非阻塞式自動重連機制)
unsigned long previousMillis = 0;
// 設定檢查網路連線的時間間隔 (單位:毫秒,例如 10000 毫秒 = 10 秒)
const long interval = 10000;
// =========================================================================
// 函式前置宣告 (Function Prototypes)
// =========================================================================
void handleRoot();
void handleToggle();
void handleStatus();
void sendStatusJSON();
void handleNotFound();
void checkWiFiConnection();
// 定義儲存於程式記憶體 (PROGMEM) 中的 HTML/CSS/原生 JS 網頁內容 (帶文字標示之單一膠囊開關)
const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<!-- 設定網頁編碼為 UTF-8 -->
<meta charset="UTF-8">
<!-- 設定響應式網頁縮放比例,確保手機與電腦皆能正常顯示 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 遠端 LED 控制面板</title>
<style>
/* 全域基礎樣式設定 */
body {
font-family: Arial, "Helvetica Neue", "Microsoft JhengHei", sans-serif;
background-color: #121212; /* 深色背景提高視覺對比 */
color: #FFFFFF; /* 文字統一使用白色 */
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 90vh;
}
/* 主容器卡片樣式 */
.container {
background-color: #1E1E1E;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0,0,0,0.5);
padding: 30px;
max-width: 450px;
width: 100%;
text-align: center;
box-sizing: border-box;
}
/* 標題樣式 */
h2 {
margin-top: 0;
color: #00E676; /* 高對比亮綠色標題 */
}
/* 狀態顯示文字樣式 */
.status-box {
font-size: 1.2rem;
margin: 20px 0;
padding: 15px;
background-color: #2C2C2C;
border-radius: 8px;
border-left: 5px solid #FFAB00;
}
.status-value {
font-weight: bold;
color: #FFD600;
}
/* ===== 帶有左右文字標示之膠囊狀切換開關 (Toggle Switch) ===== */
.switch-container {
display: flex;
justify-content: center;
align-items: center;
gap: 15px; /* 兩端文字與開關間距 */
margin: 25px 0;
}
/* 左右兩端文字標示樣式 */
.switch-label {
font-size: 1.1rem;
font-weight: bold;
user-select: none;
}
.label-off {
color: #FF5252; /* 高對比紅色 */
}
.label-on {
color: #69F0AE; /* 高對比亮綠色 */
}
.switch {
position: relative;
display: inline-block;
width: 80px;
height: 40px;
}
/* 隱藏原生 Checkbox 輸入框 */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* 滑軌外觀 (關閉時為紅色背景) */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #D50000; /* 預設關閉顏色 (紅色) */
transition: .4s;
border-radius: 40px;
}
/* 圓形滑塊 */
.slider:before {
position: absolute;
content: "";
height: 32px;
width: 32px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
/* 當 Checkbox 被勾選 (ON) 時的樣式 (開啟時為綠色背景) */
input:checked + .slider {
background-color: #00C853; /* 開啟顏色 (亮綠色) */
}
input:checked + .slider:before {
transform: translateX(40px); /* 滑塊右移 */
}
/* 操作指導說明區塊 */
.instructions {
text-align: left;
background-color: #252525;
padding: 15px;
border-radius: 8px;
font-size: 0.9rem;
line-height: 1.6;
color: #B0BEC5;
}
.instructions h4 {
margin-top: 0;
color: #80D8FF;
}
.instructions ol {
margin: 0;
padding-left: 20px;
}
</style>
</head>
<body>
<div class="container">
<h2>ESP32 內建 LED 控制器</h2>
<!-- 即時狀態顯示區塊 -->
<div class="status-box">
當前腳位狀態:<span id="ledStatus" class="status-value">讀取中...</span>
</div>
<!-- 帶有左右文字標示之單一膠囊切換開關 -->
<div class="switch-container">
<span class="switch-label label-off">關 (OFF)</span>
<label class="switch">
<input type="checkbox" id="ledToggle" onchange="toggleLED(this)">
<span class="slider"></span>
</label>
<span class="switch-label label-on">開 (ON)</span>
</div>
<!-- 操作指導說明 -->
<div class="instructions">
<h4>詳細操作指導:</h4>
<ol>
<li>點擊上方膠囊開關右側(開/ON)或將滑塊向右撥動可點亮 LED 燈,膠囊開關背景顯示為綠色。</li>
<li>點擊上方膠囊開關左側(關/OFF)或將滑塊向左撥動可熄滅 LED 燈,膠囊開關背景顯示為紅色。</li>
<li>滑塊切換後,系統將透過原生 Fetch API 非同步發送請求至 ESP32,並即時更新「當前腳位狀態」。</li>
<li>頁面載入時會自動拉取當前裝置最新腳位狀態並自動調整滑塊位置。</li>
</ol>
</div>
</div>
<script>
// 頁面DOMContentLoaded完成後自動載入當前狀態
document.addEventListener('DOMContentLoaded', function() {
updateStatus();
});
// 當單一切換開關狀態改變時觸發
function toggleLED(element) {
var action = element.checked ? 'on' : 'off';
fetch('/toggle?state=' + action)
.then(response => {
if (!response.ok) {
throw new Error('網路回應錯誤');
}
return response.json();
})
.then(data => {
parseResponse(data);
})
.catch(error => {
console.error('Error:', error);
alert('控制請求失敗,請檢查網路連線!');
// 失敗時把開關狀態彈回
element.checked = !element.checked;
});
}
// 使用原生 fetch API 向 ESP32 查詢當前狀態
function updateStatus() {
fetch('/status')
.then(response => {
if (!response.ok) {
throw new Error('網路回應錯誤');
}
return response.json();
})
.then(data => {
parseResponse(data);
})
.catch(error => {
console.error('Error:', error);
document.getElementById('ledStatus').innerText = '無法連線';
});
}
// 解析 JSON 回傳內容並動態渲染至 DOM 元素與開關滑塊
function parseResponse(data) {
// 取得 JSON 物件的第一個 key (例如 "GPIO2")
var gpioKey = Object.keys(data)[0];
// 取得對應的狀態 (例如 "ON" 或 "OFF")
var statusValue = data[gpioKey];
// 更新狀態顯示文字
document.getElementById('ledStatus').innerHTML = gpioKey + ' 狀態:<strong>' + statusValue + '</strong>';
// 同步單一切換開關的位置 (ON 時開關為 checked,OFF 時為 unchecked)
document.getElementById('ledToggle').checked = (statusValue === 'ON');
}
</script>
</body>
</html>
)rawliteral";
// 讀取 GPIO 狀態並回傳 JSON 格式數據的通用函式
void sendStatusJSON() {
// 讀取實體腳位電位高低
int currentPinState = digitalRead(LED_PIN);
// 判斷電位狀態並轉換為文字 "ON" 或 "OFF" (GPIO 2 高電位 HIGH 代表 ON)
String statusText = (currentPinState == HIGH) ? "ON" : "OFF";
// 組裝 JSON 格式字串 (格式例如:{"GPIO2":"ON"})
String jsonOutput = "{\"GPIO" + String(LED_PIN) + "\":\"" + statusText + "\"}";
// 發送 JSON 標頭與數據內容給客戶端
server.send(200, "application/json", jsonOutput);
// 於序列埠記錄發送內容
Serial.print("[JSON 回傳] ");
Serial.println(jsonOutput);
}
// 處理請求根目錄 HTTP GET / 的回應處理函式
void handleRoot() {
Serial.println("[Web Server] 收到頁面請求 HTTP GET /");
// 將記憶體中的 HTML 字串發送回客戶端
server.send(200, "text/html", INDEX_HTML);
}
// 處理控制 LED 開關之 HTTP GET /toggle 請求
void handleToggle() {
// 檢查 HTTP 請求參數中是否包含 "state" 鍵名
if (server.hasArg("state")) {
String stateStr = server.arg("state");
// 如果參數值為 "on",寫入高電位 (HIGH) 點亮 LED
if (stateStr == "on") {
digitalWrite(LED_PIN, HIGH);
Serial.println("[Web Server] 收到指令:開啟 LED (HIGH)");
}
// 如果參數值為 "off",寫入低電位 (LOW) 熄滅 LED
else if (stateStr == "off") {
digitalWrite(LED_PIN, LOW);
Serial.println("[Web Server] 收到指令:關閉 LED (LOW)");
}
}
// 讀取當前 GPIO 實際狀態並封裝為 JSON 格式傳回
sendStatusJSON();
}
// 處理查詢狀態 HTTP GET /status 請求
void handleStatus() {
Serial.println("[Web Server] 收到狀態查詢 HTTP GET /status");
// 傳回當前 GPIO 最新狀態 JSON
sendStatusJSON();
}
// 處理 404 找不到頁面之請求處理函式
void handleNotFound() {
server.send(404, "text/plain", "404: 找不到此網頁路徑");
}
// 自動檢查網路狀態,若斷線則進行連線建立與重連
void checkWiFiConnection() {
unsigned long currentMillis = millis();
// 使用非阻塞時間判斷,避免 loop 使用 delay 卡死主程式
if (WiFi.status() != WL_CONNECTED && (currentMillis - previousMillis >= interval)) {
Serial.println("[WiFi] 偵測到網路斷線,嘗試重新建立連線...");
WiFi.disconnect();
WiFi.reconnect();
previousMillis = currentMillis;
}
}
// 系統初始化入口函式
void setup() {
// 初始化序列埠通訊,速率設定為 115200
Serial.begin(115200);
delay(1000); // 稍作停頓讓序列埠完成通訊準備
Serial.println("\n========== ESP32 IoT 系統啟動 ==========");
// 將 LED 腳位 (GPIO 2) 設定為輸出模式
pinMode(LED_PIN, OUTPUT);
// 預設關閉 LED 燈 (LOW)
digitalWrite(LED_PIN, LOW);
// 設定 WiFi 模式為 Station (STA) 模式
WiFi.mode(WIFI_STA);
// 開始連接至無線區域網路
WiFi.begin(ssid, password);
Serial.print("[WiFi] 正在連接至 SSID: ");
Serial.println(ssid);
// 等待 WiFi 連線完成
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// 連線成功輸出紀錄
Serial.println("\n[WiFi] 連線成功!");
Serial.print("[WiFi] 取得 IP 位址: ");
Serial.println(WiFi.localIP());
// 設定 Web Server 路由對應函式
server.on("/", handleRoot); // 根目錄返回 HTML 頁面
server.on("/toggle", handleToggle); // 切換開關介面
server.on("/status", handleStatus); // 狀態查詢介面
server.onNotFound(handleNotFound); // 未定義路由之 404 處置
// 啟動 Web Server 服務
server.begin();
Serial.println("[Web Server] HTTP 伺服器啟動成功,連接埠: 80");
Serial.println("=========================================");
}
// 系統持續執行之主迴圈
void loop() {
// 定期檢查 WiFi 狀態,斷線自動重連
checkWiFiConnection();
// 持續處理來自客戶端的 HTTP 請求
server.handleClient();
}
編譯驗證,確認無誤
開序列埠視窗,上傳程式,抓取 ESP32 IP(192.168.50.105)
開 Chrome,網址列輸入 ESP32 IP
多裝置同時操作時,會有狀態不同步現象。請分析原因,規劃可以解決這現象的作法
【方案 A:背景定期自動輪詢 (Short Polling)】
#include <WiFi.h> // 載入 ESP32 的 WiFi 基礎函式庫
#include <WebServer.h> // 載入 ESP32 的 Web Server 函式庫
// 定義 WiFi 連線資訊
const char* ssid = "chuangHome"; // WiFi 無線網路名稱 (SSID)
const char* password = "0988087024"; // WiFi 無線網路密碼
// 定義 NodeMCU-32S 內建 LED 控制腳位 (GPIO 2,高電位亮)
#define LED_PIN 2
// 建立 WebServer 物件,監聽埠號為 80 (HTTP 預設埠號)
WebServer server(80);
// 記錄上一次檢查網路連線的時間點 (用於非阻塞式自動重連機制)
unsigned long previousMillis = 0;
// 設定檢查網路連線的時間間隔 (單位:毫秒,例如 10000 毫秒 = 10 秒)
const long interval = 10000;
// =========================================================================
// 函式前置宣告 (Function Prototypes)
// =========================================================================
void handleRoot();
void handleToggle();
void handleStatus();
void sendStatusJSON();
void handleNotFound();
void checkWiFiConnection();
// 定義儲存於程式記憶體 (PROGMEM) 中的 HTML/CSS/原生 JS 網頁內容 (具備自動輪詢功能)
const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<!-- 設定網頁編碼為 UTF-8 -->
<meta charset="UTF-8">
<!-- 設定響應式網頁縮放比例,確保手機與電腦皆能正常顯示 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 遠端 LED 控制面板</title>
<style>
/* 全域基礎樣式設定 */
body {
font-family: Arial, "Helvetica Neue", "Microsoft JhengHei", sans-serif;
background-color: #121212; /* 深色背景提高視覺對比 */
color: #FFFFFF; /* 文字統一使用白色 */
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 90vh;
}
/* 主容器卡片樣式 */
.container {
background-color: #1E1E1E;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0,0,0,0.5);
padding: 30px;
max-width: 450px;
width: 100%;
text-align: center;
box-sizing: border-box;
}
/* 標題樣式 */
h2 {
margin-top: 0;
color: #00E676; /* 高對比亮綠色標題 */
}
/* 狀態顯示文字樣式 */
.status-box {
font-size: 1.2rem;
margin: 20px 0;
padding: 15px;
background-color: #2C2C2C;
border-radius: 8px;
border-left: 5px solid #FFAB00;
}
.status-value {
font-weight: bold;
color: #FFD600;
}
/* ===== 帶有左右文字標示之膠囊狀切換開關 (Toggle Switch) ===== */
.switch-container {
display: flex;
justify-content: center;
align-items: center;
gap: 15px; /* 兩端文字與開關間距 */
margin: 25px 0;
}
/* 左右兩端文字標示樣式 */
.switch-label {
font-size: 1.1rem;
font-weight: bold;
user-select: none;
}
.label-off {
color: #FF5252; /* 高對比紅色 */
}
.label-on {
color: #69F0AE; /* 高對比亮綠色 */
}
.switch {
position: relative;
display: inline-block;
width: 80px;
height: 40px;
}
/* 隱藏原生 Checkbox 輸入框 */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* 滑軌外觀 (關閉時為紅色背景) */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #D50000; /* 預設關閉顏色 (紅色) */
transition: .4s;
border-radius: 40px;
}
/* 圓形滑塊 */
.slider:before {
position: absolute;
content: "";
height: 32px;
width: 32px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
/* 當 Checkbox 被勾選 (ON) 時的樣式 (開啟時為綠色背景) */
input:checked + .slider {
background-color: #00C853; /* 開啟顏色 (亮綠色) */
}
input:checked + .slider:before {
transform: translateX(40px); /* 滑塊右移 */
}
/* 操作指導說明區塊 */
.instructions {
text-align: left;
background-color: #252525;
padding: 15px;
border-radius: 8px;
font-size: 0.9rem;
line-height: 1.6;
color: #B0BEC5;
}
.instructions h4 {
margin-top: 0;
color: #80D8FF;
}
.instructions ol {
margin: 0;
padding-left: 20px;
}
</style>
</head>
<body>
<div class="container">
<h2>ESP32 內建 LED 控制器</h2>
<!-- 即時狀態顯示區塊 -->
<div class="status-box">
當前腳位狀態:<span id="ledStatus" class="status-value">讀取中...</span>
</div>
<!-- 帶有左右文字標示之單一膠囊切換開關 -->
<div class="switch-container">
<span class="switch-label label-off">關 (OFF)</span>
<label class="switch">
<input type="checkbox" id="ledToggle" onchange="toggleLED(this)">
<span class="slider"></span>
</label>
<span class="switch-label label-on">開 (ON)</span>
</div>
<!-- 操作指導說明 -->
<div class="instructions">
<h4>詳細操作指導:</h4>
<ol>
<li>點擊上方膠囊開關右側(開/ON)或將滑塊向右撥動可點亮 LED 燈,膠囊開關背景顯示為綠色。</li>
<li>點擊上方膠囊開關左側(關/OFF)或將滑塊向左撥動可熄滅 LED 燈,膠囊開關背景顯示為紅色。</li>
<li>系統建置了<strong>【背景自動輪詢機制】</strong>,每 2 秒自動與 ESP32 同步狀態,多裝置同時開啟時畫面會自動同步。</li>
<li>頁面載入時會自動拉取當前裝置最新腳位狀態並自動調整滑塊位置。</li>
</ol>
</div>
</div>
<script>
// 標記目前是否正在發送手動點擊請求,避免與輪詢產生衝突
var isOperating = false;
// 頁面DOMContentLoaded完成後自動初始化
document.addEventListener('DOMContentLoaded', function() {
// 立即更新一次狀態
updateStatus();
// 設定每 2000 毫秒 (2 秒) 自動於背景向 ESP32 查詢最新狀態
setInterval(function() {
if (!isOperating) {
updateStatus();
}
}, 2000);
});
// 當單一切換開關狀態被手動改變時觸發
function toggleLED(element) {
isOperating = true; // 設定操作鎖定
var action = element.checked ? 'on' : 'off';
fetch('/toggle?state=' + action)
.then(response => {
if (!response.ok) {
throw new Error('網路回應錯誤');
}
return response.json();
})
.then(data => {
parseResponse(data);
})
.catch(error => {
console.error('Error:', error);
alert('控制請求失敗,請檢查網路連線!');
// 失敗時把開關狀態彈回
element.checked = !element.checked;
})
.finally(() => {
isOperating = false; // 解除操作鎖定
});
}
// 使用原生 fetch API 向 ESP32 查詢當前狀態
function updateStatus() {
fetch('/status')
.then(response => {
if (!response.ok) {
throw new Error('網路回應錯誤');
}
return response.json();
})
.then(data => {
parseResponse(data);
})
.catch(error => {
console.error('Error:', error);
document.getElementById('ledStatus').innerText = '無法連線';
});
}
// 解析 JSON 回傳內容並動態渲染至 DOM 元素與開關滑塊
function parseResponse(data) {
// 取得 JSON 物件的第一個 key (例如 "GPIO2")
var gpioKey = Object.keys(data)[0];
// 取得對應的狀態 (例如 "ON" 或 "OFF")
var statusValue = data[gpioKey];
// 更新狀態顯示文字
document.getElementById('ledStatus').innerHTML = gpioKey + ' 狀態:<strong>' + statusValue + '</strong>';
// 自動同步單一切換開關的位置 (避免重複觸發 onchange)
var toggleCheckbox = document.getElementById('ledToggle');
var targetChecked = (statusValue === 'ON');
if (toggleCheckbox.checked !== targetChecked) {
toggleCheckbox.checked = targetChecked;
}
}
</script>
</body>
</html>
)rawliteral";
// 讀取 GPIO 狀態並回傳 JSON 格式數據的通用函式
void sendStatusJSON() {
// 讀取實體腳位電位高低
int currentPinState = digitalRead(LED_PIN);
// 判斷電位狀態並轉換為文字 "ON" 或 "OFF" (GPIO 2 高電位 HIGH 代表 ON)
String statusText = (currentPinState == HIGH) ? "ON" : "OFF";
// 組裝 JSON 格式字串 (格式例如:{"GPIO2":"ON"})
String jsonOutput = "{\"GPIO" + String(LED_PIN) + "\":\"" + statusText + "\"}";
// 發送 JSON 標頭與數據內容給客戶端
server.send(200, "application/json", jsonOutput);
}
// 處理請求根目錄 HTTP GET / 的回應處理函式
void handleRoot() {
Serial.println("[Web Server] 收到頁面請求 HTTP GET /");
// 將記憶體中的 HTML 字串發送回客戶端
server.send(200, "text/html", INDEX_HTML);
}
// 處理控制 LED 開關之 HTTP GET /toggle 請求
void handleToggle() {
// 檢查 HTTP 請求參數中是否包含 "state" 鍵名
if (server.hasArg("state")) {
String stateStr = server.arg("state");
// 如果參數值為 "on",寫入高電位 (HIGH) 點亮 LED
if (stateStr == "on") {
digitalWrite(LED_PIN, HIGH);
Serial.println("[Web Server] 收到指令:開啟 LED (HIGH)");
}
// 如果參數值為 "off",寫入低電位 (LOW) 熄滅 LED
else if (stateStr == "off") {
digitalWrite(LED_PIN, LOW);
Serial.println("[Web Server] 收到指令:關閉 LED (LOW)");
}
}
// 讀取當前 GPIO 實際狀態並封裝為 JSON 格式傳回
sendStatusJSON();
}
// 處理查詢狀態 HTTP GET /status 請求
void handleStatus() {
// 傳回當前 GPIO 最新狀態 JSON
sendStatusJSON();
}
// 處理 404 找不到頁面之請求處理函式
void handleNotFound() {
server.send(404, "text/plain", "404: 找不到此網頁路徑");
}
// 自動檢查網路狀態,若斷線則進行連線建立與重連
void checkWiFiConnection() {
unsigned long currentMillis = millis();
// 使用非阻塞時間判斷,避免 loop 使用 delay 卡死主程式
if (WiFi.status() != WL_CONNECTED && (currentMillis - previousMillis >= interval)) {
Serial.println("[WiFi] 偵測到網路斷線,嘗試重新建立連線...");
WiFi.disconnect();
WiFi.reconnect();
previousMillis = currentMillis;
}
}
// 系統初始化入口函式
void setup() {
// 初始化序列埠通訊,速率設定為 115200
Serial.begin(115200);
delay(1000); // 稍作停頓讓序列埠完成通訊準備
Serial.println("\n========== ESP32 IoT 系統啟動 ==========");
// 將 LED 腳位 (GPIO 2) 設定為輸出模式
pinMode(LED_PIN, OUTPUT);
// 預設關閉 LED 燈 (LOW)
digitalWrite(LED_PIN, LOW);
// 設定 WiFi 模式為 Station (STA) 模式
WiFi.mode(WIFI_STA);
// 開始連接至無線區域網路
WiFi.begin(ssid, password);
Serial.print("[WiFi] 正在連接至 SSID: ");
Serial.println(ssid);
// 等待 WiFi 連線完成
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// 連線成功輸出紀錄
Serial.println("\n[WiFi] 連線成功!");
Serial.print("[WiFi] 取得 IP 位址: ");
Serial.println(WiFi.localIP());
// 設定 Web Server 路由對應函式
server.on("/", handleRoot); // 根目錄返回 HTML 頁面
server.on("/toggle", handleToggle); // 切換開關介面
server.on("/status", handleStatus); // 狀態查詢介面
server.onNotFound(handleNotFound); // 未定義路由之 404 處置
// 啟動 Web Server 服務
server.begin();
Serial.println("[Web Server] HTTP 伺服器啟動成功,連接埠: 80");
Serial.println("=========================================");
}
// 系統持續執行之主迴圈
void loop() {
// 定期檢查 WiFi 狀態,斷線自動重連
checkWiFiConnection();
// 持續處理來自客戶端的 HTTP 請求
server.handleClient();
}
輪詢機制運作正常,做下列修正
1.輪詢時間間隔調整為 1 秒
2.把發出輪詢及HTTP回應的訊息輸出至序列埠
3.把讀取 GPIO 狀態並回傳 JSON 的結果輸出至序列埠
先把你的規畫給我
#include <WiFi.h> // 載入 ESP32 的 WiFi 基礎函式庫
#include <WebServer.h> // 載入 ESP32 的 Web Server 函式庫
// 定義 WiFi 連線資訊
const char* ssid = "chuangHome"; // WiFi 無線網路名稱 (SSID)
const char* password = "0988087024"; // WiFi 無線網路密碼
// 定義 NodeMCU-32S 內建 LED 控制腳位 (GPIO 2,高電位亮)
#define LED_PIN 2
// 建立 WebServer 物件,監聽埠號為 80 (HTTP 預設埠號)
WebServer server(80);
// 記錄上一次檢查網路連線的時間點 (用於非阻塞式自動重連機制)
unsigned long previousMillis = 0;
// 設定檢查網路連線的時間間隔 (單位:毫秒,例如 10000 毫秒 = 10 秒)
const long interval = 10000;
// =========================================================================
// 函式前置宣告 (Function Prototypes)
// =========================================================================
void handleRoot();
void handleToggle();
void handleStatus();
void sendStatusJSON();
void handleNotFound();
void checkWiFiConnection();
// 定義儲存於程式記憶體 (PROGMEM) 中的 HTML/CSS/原生 JS 網頁內容 (1 秒輪詢)
const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<!-- 設定網頁編碼為 UTF-8 -->
<meta charset="UTF-8">
<!-- 設定響應式網頁縮放比例,確保手機與電腦皆能正常顯示 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 遠端 LED 控制面板</title>
<style>
/* 全域基礎樣式設定 */
body {
font-family: Arial, "Helvetica Neue", "Microsoft JhengHei", sans-serif;
background-color: #121212; /* 深色背景提高視覺對比 */
color: #FFFFFF; /* 文字統一使用白色 */
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 90vh;
}
/* 主容器卡片樣式 */
.container {
background-color: #1E1E1E;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0,0,0,0.5);
padding: 30px;
max-width: 450px;
width: 100%;
text-align: center;
box-sizing: border-box;
}
/* 標題樣式 */
h2 {
margin-top: 0;
color: #00E676; /* 高對比亮綠色標題 */
}
/* 狀態顯示文字樣式 */
.status-box {
font-size: 1.2rem;
margin: 20px 0;
padding: 15px;
background-color: #2C2C2C;
border-radius: 8px;
border-left: 5px solid #FFAB00;
}
.status-value {
font-weight: bold;
color: #FFD600;
}
/* ===== 帶有左右文字標示之膠囊狀切換開關 (Toggle Switch) ===== */
.switch-container {
display: flex;
justify-content: center;
align-items: center;
gap: 15px; /* 兩端文字與開關間距 */
margin: 25px 0;
}
/* 左右兩端文字標示樣式 */
.switch-label {
font-size: 1.1rem;
font-weight: bold;
user-select: none;
}
.label-off {
color: #FF5252; /* 高對比紅色 */
}
.label-on {
color: #69F0AE; /* 高對比亮綠色 */
}
.switch {
position: relative;
display: inline-block;
width: 80px;
height: 40px;
}
/* 隱藏原生 Checkbox 輸入框 */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* 滑軌外觀 (關閉時為紅色背景) */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #D50000; /* 預設關閉顏色 (紅色) */
transition: .4s;
border-radius: 40px;
}
/* 圓形滑塊 */
.slider:before {
position: absolute;
content: "";
height: 32px;
width: 32px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
/* 當 Checkbox 被勾選 (ON) 時的樣式 (開啟時為綠色背景) */
input:checked + .slider {
background-color: #00C853; /* 開啟顏色 (亮綠色) */
}
input:checked + .slider:before {
transform: translateX(40px); /* 滑塊右移 */
}
/* 操作指導說明區塊 */
.instructions {
text-align: left;
background-color: #252525;
padding: 15px;
border-radius: 8px;
font-size: 0.9rem;
line-height: 1.6;
color: #B0BEC5;
}
.instructions h4 {
margin-top: 0;
color: #80D8FF;
}
.instructions ol {
margin: 0;
padding-left: 20px;
}
</style>
</head>
<body>
<div class="container">
<h2>ESP32 內建 LED 控制器</h2>
<!-- 即時狀態顯示區塊 -->
<div class="status-box">
當前腳位狀態:<span id="ledStatus" class="status-value">讀取中...</span>
</div>
<!-- 帶有左右文字標示之單一膠囊切換開關 -->
<div class="switch-container">
<span class="switch-label label-off">關 (OFF)</span>
<label class="switch">
<input type="checkbox" id="ledToggle" onchange="toggleLED(this)">
<span class="slider"></span>
</label>
<span class="switch-label label-on">開 (ON)</span>
</div>
<!-- 操作指導說明 -->
<div class="instructions">
<h4>詳細操作指導:</h4>
<ol>
<li>點擊上方膠囊開關右側(開/ON)或將滑塊向右撥動可點亮 LED 燈,膠囊開關背景顯示為綠色。</li>
<li>點擊上方膠囊開關左側(關/OFF)或將滑塊向左撥動可熄滅 LED 燈,膠囊開關背景顯示為紅色。</li>
<li>系統建置了<strong>【背景自動輪詢機制】</strong>,每 1 秒自動與 ESP32 同步狀態,多裝置同時開啟時畫面會自動同步。</li>
<li>頁面載入時會自動拉取當前裝置最新腳位狀態並自動調整滑塊位置。</li>
</ol>
</div>
</div>
<script>
// 標記目前是否正在發送手動點擊請求,避免與輪詢產生衝突
var isOperating = false;
// 頁面DOMContentLoaded完成後自動初始化
document.addEventListener('DOMContentLoaded', function() {
// 立即更新一次狀態
updateStatus();
// 設定每 1000 毫秒 (1 秒) 自動於背景向 ESP32 查詢最新狀態
setInterval(function() {
if (!isOperating) {
updateStatus();
}
}, 1000);
});
// 當單一切換開關狀態被手動改變時觸發
function toggleLED(element) {
isOperating = true; // 設定操作鎖定
var action = element.checked ? 'on' : 'off';
fetch('/toggle?state=' + action)
.then(response => {
if (!response.ok) {
throw new Error('網路回應錯誤');
}
return response.json();
})
.then(data => {
parseResponse(data);
})
.catch(error => {
console.error('Error:', error);
alert('控制請求失敗,請檢查網路連線!');
// 失敗時把開關狀態彈回
element.checked = !element.checked;
})
.finally(() => {
isOperating = false; // 解除操作鎖定
});
}
// 使用原生 fetch API 向 ESP32 查詢當前狀態
function updateStatus() {
fetch('/status')
.then(response => {
if (!response.ok) {
throw new Error('網路回應錯誤');
}
return response.json();
})
.then(data => {
parseResponse(data);
})
.catch(error => {
console.error('Error:', error);
document.getElementById('ledStatus').innerText = '無法連線';
});
}
// 解析 JSON 回傳內容並動態渲染至 DOM 元素與開關滑塊
function parseResponse(data) {
// 取得 JSON 物件的第一個 key (例如 "GPIO2")
var gpioKey = Object.keys(data)[0];
// 取得對應的狀態 (例如 "ON" 或 "OFF")
var statusValue = data[gpioKey];
// 更新狀態顯示文字
document.getElementById('ledStatus').innerHTML = gpioKey + ' 狀態:<strong>' + statusValue + '</strong>';
// 自動同步單一切換開關的位置 (避免重複觸發 onchange)
var toggleCheckbox = document.getElementById('ledToggle');
var targetChecked = (statusValue === 'ON');
if (toggleCheckbox.checked !== targetChecked) {
toggleCheckbox.checked = targetChecked;
}
}
</script>
</body>
</html>
)rawliteral";
// 讀取 GPIO 狀態並回傳 JSON 格式數據的通用函式
void sendStatusJSON() {
// 讀取實體腳位電位高低
int currentPinState = digitalRead(LED_PIN);
// 判斷電位狀態並轉換為文字 "ON" 或 "OFF" (GPIO 2 高電位 HIGH 代表 ON)
String statusText = (currentPinState == HIGH) ? "ON" : "OFF";
// 組裝 JSON 格式字串 (格式例如:{"GPIO2":"ON"})
String jsonOutput = "{\"GPIO" + String(LED_PIN) + "\":\"" + statusText + "\"}";
// 發送 JSON 標頭與數據內容給客戶端
server.send(200, "application/json", jsonOutput);
// 於序列埠即時輸出 JSON 回傳內容
Serial.print("[JSON 回傳] ");
Serial.println(jsonOutput);
}
// 處理請求根目錄 HTTP GET / 的回應處理函式
void handleRoot() {
Serial.println("[Web Server] 收到頁面請求 HTTP GET /");
// 將記憶體中的 HTML 字串發送回客戶端
server.send(200, "text/html", INDEX_HTML);
}
// 處理控制 LED 開關之 HTTP GET /toggle 請求
void handleToggle() {
// 檢查 HTTP 請求參數中是否包含 "state" 鍵名
if (server.hasArg("state")) {
String stateStr = server.arg("state");
// 如果參數值為 "on",寫入高電位 (HIGH) 點亮 LED
if (stateStr == "on") {
digitalWrite(LED_PIN, HIGH);
Serial.println("[Web Server] 收到指令:開啟 LED (HIGH)");
}
// 如果參數值為 "off",寫入低電位 (LOW) 熄滅 LED
else if (stateStr == "off") {
digitalWrite(LED_PIN, LOW);
Serial.println("[Web Server] 收到指令:關閉 LED (LOW)");
}
}
// 讀取當前 GPIO 實際狀態並封裝為 JSON 格式傳回
sendStatusJSON();
}
// 處理查詢狀態 HTTP GET /status 請求 (含背景輪詢)
void handleStatus() {
Serial.println("[Web Server] 收到狀態查詢 HTTP GET /status");
// 傳回當前 GPIO 最新狀態 JSON
sendStatusJSON();
}
// 處理 404 找不到頁面之請求處理函式
void handleNotFound() {
server.send(404, "text/plain", "404: 找不到此網頁路徑");
}
// 自動檢查網路狀態,若斷線則進行連線建立與重連
void checkWiFiConnection() {
unsigned long currentMillis = millis();
// 使用非阻塞時間判斷,避免 loop 使用 delay 卡死主程式
if (WiFi.status() != WL_CONNECTED && (currentMillis - previousMillis >= interval)) {
Serial.println("[WiFi] 偵測到網路斷線,嘗試重新建立連線...");
WiFi.disconnect();
WiFi.reconnect();
previousMillis = currentMillis;
}
}
// 系統初始化入口函式
void setup() {
// 初始化序列埠通訊,速率設定為 115200
Serial.begin(115200);
delay(1000); // 稍作停頓讓序列埠完成通訊準備
Serial.println("\n========== ESP32 IoT 系統啟動 ==========");
// 將 LED 腳位 (GPIO 2) 設定為輸出模式
pinMode(LED_PIN, OUTPUT);
// 預設關閉 LED 燈 (LOW)
digitalWrite(LED_PIN, LOW);
// 設定 WiFi 模式為 Station (STA) 模式
WiFi.mode(WIFI_STA);
// 開始連接至無線區域網路
WiFi.begin(ssid, password);
Serial.print("[WiFi] 正在連接至 SSID: ");
Serial.println(ssid);
// 等待 WiFi 連線完成
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// 連線成功輸出紀錄
Serial.println("\n[WiFi] 連線成功!");
Serial.print("[WiFi] 取得 IP 位址: ");
Serial.println(WiFi.localIP());
// 設定 Web Server 路由對應函式
server.on("/", handleRoot); // 根目錄返回 HTML 頁面
server.on("/toggle", handleToggle); // 切換開關介面
server.on("/status", handleStatus); // 狀態查詢介面
server.onNotFound(handleNotFound); // 未定義路由之 404 處置
// 啟動 Web Server 服務
server.begin();
Serial.println("[Web Server] HTTP 伺服器啟動成功,連接埠: 80");
Serial.println("=========================================");
}
// 系統持續執行之主迴圈
void loop() {
// 定期檢查 WiFi 狀態,斷線自動重連
checkWiFiConnection();
// 持續處理來自客戶端的 HTTP 請求
server.handleClient();
}
【方案 B:WebSocket 即時通訊】
第三方函式庫需求
因 ESP32 核心預設未內建 WebSocket Server 模組,需安裝以下函式庫:
函式庫名稱:WebSockets by Markus Sattler
安裝方式:可直接於 Arduino IDE 的「函式庫管理員」搜尋並安裝。
#include <WiFi.h> // 載入 ESP32 的 WiFi 基礎函式庫
#include <WebServer.h> // 載入 ESP32 的 Web Server 函式庫 (Port 80)
#include <WebSocketsServer.h>// 載入 WebSockets 函式庫 (Port 81)
// 定義 WiFi 連線資訊
const char* ssid = "chuangHome"; // WiFi 無線網路名稱 (SSID)
const char* password = "0988087024"; // WiFi 無線網路密碼
// 定義 NodeMCU-32S 內建 LED 控制腳位 (GPIO 2,高電位亮)
#define LED_PIN 2
// 建立 WebServer 物件 (HTTP 服務,監聽 Port 80)
WebServer server(80);
// 建立 WebSocketsServer 物件 (WebSocket 即時通訊服務,監聽 Port 81)
WebSocketsServer webSocket = WebSocketsServer(81);
// 記錄上一次檢查網路連線的時間點 (用於非阻塞式自動重連機制)
unsigned long previousMillis = 0;
// 設定檢查網路連線的時間間隔 (單位:毫秒,例如 10000 毫秒 = 10 秒)
const long interval = 10000;
// =========================================================================
// 函式前置宣告 (Function Prototypes)
// =========================================================================
void handleRoot();
void handleNotFound();
void checkWiFiConnection();
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length);
void broadcastStatus();
// 定義儲存於程式記憶體 (PROGMEM) 中的 HTML/CSS/WebSocket 原生 JS 網頁內容
const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<!-- 設定網頁編碼為 UTF-8 -->
<meta charset="UTF-8">
<!-- 設定響應式網頁縮放比例,確保手機與電腦皆能正常顯示 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 遠端 LED 控制面板 (WebSocket 即時同步)</title>
<style>
/* 全域基礎樣式設定 */
body {
font-family: Arial, "Helvetica Neue", "Microsoft JhengHei", sans-serif;
background-color: #121212; /* 深色背景提高視覺對比 */
color: #FFFFFF; /* 文字統一使用白色 */
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 90vh;
}
/* 主容器卡片樣式 */
.container {
background-color: #1E1E1E;
border-radius: 12px;
box-shadow: 0 8px 16px rgba(0,0,0,0.5);
padding: 30px;
max-width: 450px;
width: 100%;
text-align: center;
box-sizing: border-box;
}
/* 標題樣式 */
h2 {
margin-top: 0;
color: #00E676; /* 高對比亮綠色標題 */
}
/* 狀態顯示文字樣式 */
.status-box {
font-size: 1.2rem;
margin: 20px 0;
padding: 15px;
background-color: #2C2C2C;
border-radius: 8px;
border-left: 5px solid #FFAB00;
}
.status-value {
font-weight: bold;
color: #FFD600;
}
/* ===== 帶有左右文字標示之膠囊狀切換開關 (Toggle Switch) ===== */
.switch-container {
display: flex;
justify-content: center;
align-items: center;
gap: 15px; /* 兩端文字與開關間距 */
margin: 25px 0;
}
/* 左右兩端文字標示樣式 */
.switch-label {
font-size: 1.1rem;
font-weight: bold;
user-select: none;
}
.label-off {
color: #FF5252; /* 高對比紅色 */
}
.label-on {
color: #69F0AE; /* 高對比亮綠色 */
}
.switch {
position: relative;
display: inline-block;
width: 80px;
height: 40px;
}
/* 隱藏原生 Checkbox 輸入框 */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* 滑軌外觀 (關閉時為紅色背景) */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #D50000; /* 預設關閉顏色 (紅色) */
transition: .4s;
border-radius: 40px;
}
/* 圓形滑塊 */
.slider:before {
position: absolute;
content: "";
height: 32px;
width: 32px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
/* 當 Checkbox 被勾選 (ON) 時的樣式 (開啟時為綠色背景) */
input:checked + .slider {
background-color: #00C853; /* 開啟顏色 (亮綠色) */
}
input:checked + .slider:before {
transform: translateX(40px); /* 滑塊右移 */
}
/* 操作指導說明區塊 */
.instructions {
text-align: left;
background-color: #252525;
padding: 15px;
border-radius: 8px;
font-size: 0.9rem;
line-height: 1.6;
color: #B0BEC5;
}
.instructions h4 {
margin-top: 0;
color: #80D8FF;
}
.instructions ol {
margin: 0;
padding-left: 20px;
}
</style>
</head>
<body>
<div class="container">
<h2>ESP32 內建 LED 控制器</h2>
<!-- 即時狀態顯示區塊 -->
<div class="status-box">
當前腳位狀態:<span id="ledStatus" class="status-value">連接 WebSocket 中...</span>
</div>
<!-- 帶有左右文字標示之單一膠囊切換開關 -->
<div class="switch-container">
<span class="switch-label label-off">關 (OFF)</span>
<label class="switch">
<input type="checkbox" id="ledToggle" onchange="toggleLED(this)">
<span class="slider"></span>
</label>
<span class="switch-label label-on">開 (ON)</span>
</div>
<!-- 操作指導說明 -->
<div class="instructions">
<h4>詳細操作指導:</h4>
<ol>
<li>點擊上方膠囊開關右側(開/ON)或將滑塊向右撥動可點亮 LED 燈,膠囊開關背景顯示為綠色。</li>
<li>點擊上方膠囊開關左側(關/OFF)或將滑塊向左撥動可熄滅 LED 燈,膠囊開關背景顯示為紅色。</li>
<li>本頁面採用 <strong>WebSocket 全雙工即時廣播技術</strong>,多裝置同時開啟時,任何一端的修改均會秒級同步至所有裝置。</li>
</ol>
</div>
</div>
<script>
var socket;
// 初始化 WebSocket 連線
function initWebSocket() {
// 建立與 ESP32 WebSocket 服務 (Port 81) 的通道
var wsUri = 'ws://' + window.location.hostname + ':81/';
socket = new WebSocket(wsUri);
// 監聽連線建立事件
socket.onopen = function(evt) {
console.log("WebSocket 連線成功");
};
// 監聽斷線事件 (自動重新連線)
socket.onclose = function(evt) {
console.log("WebSocket 連線中斷,2 秒後嘗試重連...");
document.getElementById('ledStatus').innerText = '連線中斷,重連中...';
setTimeout(initWebSocket, 2000);
};
// 監聽收到伺服器訊息 (JSON) 事件
socket.onmessage = function(evt) {
console.log("收到 WebSocket 廣播: " + evt.data);
try {
var data = JSON.parse(evt.data);
parseResponse(data);
} catch(e) {
console.error("JSON 解析失敗", e);
}
};
// 監聽錯誤事件
socket.onerror = function(evt) {
console.error("WebSocket 發生錯誤", evt);
};
}
// 當單一切換開關狀態改變時觸發,透過 WebSocket 發送指令
function toggleLED(element) {
if (socket && socket.readyState === WebSocket.OPEN) {
var action = element.checked ? 'on' : 'off';
socket.send(action); // 直接傳送 "on" 或 "off" 字串
} else {
alert('WebSocket 未連線,無法傳送指令!');
element.checked = !element.checked; // 彈回原始狀態
}
}
// 解析 JSON 回傳內容並動態渲染至 DOM 元素與開關滑塊
function parseResponse(data) {
// 取得 JSON 物件的第一個 key (例如 "GPIO2")
var gpioKey = Object.keys(data)[0];
// 取得對應的狀態 (例如 "ON" 或 "OFF")
var statusValue = data[gpioKey];
// 更新狀態顯示文字
document.getElementById('ledStatus').innerHTML = gpioKey + ' 狀態:<strong>' + statusValue + '</strong>';
// 同步單一切換開關的位置 (ON 時開關為 checked,OFF 時為 unchecked)
document.getElementById('ledToggle').checked = (statusValue === 'ON');
}
// 頁面載入完成後啟動 WebSocket
document.addEventListener('DOMContentLoaded', function() {
initWebSocket();
});
</script>
</body>
</html>
)rawliteral";
// WebSocket 事件處理核心函式
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
switch(type) {
// 客戶端斷開連線
case WStype_DISCONNECTED:
Serial.printf("[WebSocket] 客戶端 #%u 已斷開連線\n", num);
break;
// 客戶端建立連線
case WStype_CONNECTED: {
IPAddress ip = webSocket.remoteIP(num);
Serial.printf("[WebSocket] 客戶端 #%u 已連線,來源 IP: %d.%d.%d.%d\n", num, ip[0], ip[1], ip[2], ip[3]);
// 當有新裝置連線時,立即推送當前的最新腳位狀態給該客戶端
broadcastStatus();
break;
}
// 收到來自客戶端的文字訊息 (開關指令)
case WStype_TEXT: {
String text = String((char*)payload);
Serial.printf("[WebSocket] 收到來自 #%u 的指令: %s\n", num, text.c_str());
if (text == "on") {
digitalWrite(LED_PIN, HIGH);
Serial.println("[硬體動作] 開啟 LED (HIGH)");
} else if (text == "off") {
digitalWrite(LED_PIN, LOW);
Serial.println("[硬體動作] 關閉 LED (LOW)");
}
// 腳位狀態變更後,立即廣播 (Broadcast) 最新狀態給所有連線中的裝置
broadcastStatus();
break;
}
default:
break;
}
}
// 讀取實體 GPIO 狀態,並透過 WebSocket 全網頁廣播 JSON 數據
void broadcastStatus() {
int currentPinState = digitalRead(LED_PIN);
String statusText = (currentPinState == HIGH) ? "ON" : "OFF";
// 組裝 JSON 格式字串 (格式:{"GPIO2":"ON"})
String jsonOutput = "{\"GPIO" + String(LED_PIN) + "\":\"" + statusText + "\"}";
// 將最新狀態廣播推播給「所有」已連線的 WebSocket 客戶端
webSocket.broadcastTXT(jsonOutput);
Serial.print("[WebSocket 廣播] ");
Serial.println(jsonOutput);
}
// 處理請求根目錄 HTTP GET / 的回應處理函式
void handleRoot() {
Serial.println("[HTTP Server] 收到頁面請求 GET /");
server.send(200, "text/html", INDEX_HTML);
}
// 處理 404 找不到頁面處理函式
void handleNotFound() {
server.send(404, "text/plain", "404: 找不到此網頁路徑");
}
// 自動檢查網路狀態,若斷線則進行連線建立與重連
void checkWiFiConnection() {
unsigned long currentMillis = millis();
if (WiFi.status() != WL_CONNECTED && (currentMillis - previousMillis >= interval)) {
Serial.println("[WiFi] 偵測到網路斷線,嘗試重新建立連線...");
WiFi.disconnect();
WiFi.reconnect();
previousMillis = currentMillis;
}
}
// 系統初始化入口函式
void setup() {
// 初始化序列埠通訊,速率設定為 115200
Serial.begin(115200);
delay(1000);
Serial.println("\n========== ESP32 WebSocket IoT 系統啟動 ==========");
// 將 LED 腳位 (GPIO 2) 設定為輸出模式
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW); // 預設關閉
// 設定 WiFi 模式為 Station (STA) 模式
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("[WiFi] 正在連接至 SSID: ");
Serial.println(ssid);
// 等待 WiFi 連線完成
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// 連線成功輸出紀錄
Serial.println("\n[WiFi] 連線成功!");
Serial.print("[WiFi] 取得 IP 位址: ");
Serial.println(WiFi.localIP());
// 1. 設定並啟動 HTTP Web Server (Port 80)
server.on("/", handleRoot);
server.onNotFound(handleNotFound);
server.begin();
Serial.println("[HTTP Server] 伺服器啟動成功,Port: 80");
// 2. 設定並啟動 WebSocket Server (Port 81)
webSocket.begin();
webSocket.onEvent(webSocketEvent); // 設定 WebSocket 事件處理常式
Serial.println("[WebSocket] 伺服器啟動成功,Port: 81");
Serial.println("=================================================");
}
// 系統持續執行之主迴圈
void loop() {
// 自動檢查 WiFi 連線
checkWiFiConnection();
// 處理 HTTP 網頁請求
server.handleClient();
// 處理 WebSocket 即時封包與事件 (全雙工核心)
webSocket.loop();
}