https://share.gemini.google/OYiUwowsQNY9
提示詞
提供嵌入式系統物聯網程式開發,需求如下。開始寫程式前,根據需求,提供你的規劃給我確認。
#嵌入式系統硬體
##開發板
-NodeMCU-32S 開發板
-Keyes ESP-32 專用 GPIO 擴充板
##外接元件
-無
##接線需求
-無
#程式需求
##開發環境
-Arduino IDE 1.8.19
-ESP32 核心版本 3.1.1
##建立 AP mode wifi 連網
-SSID: ymiot1151
-PASSWORD: 12345678
##連網成功後 會檢查網路連線,當網路連線失敗,重新自動建立連線
##連網成功後,建立 web server 服務,產生一個單頁響應式網頁
-整合 HTML, CSS, jQuery(使用非同步 Ajax 請求)等語法
-網頁內容提供1個開關切換按鈕 用來開啟/關閉內建LED燈
-於開關按鈕動作結束後,讀取內建LED燈腳位的開關狀態,顯示在網頁上,讓使用者可以明確知道目前開關的狀態。回傳 JSON 格式數據(例如:{"GPIO25":ON, "GPIO25)":OFF})。
-網頁設計力求簡潔美觀、配色對比強,顯示文字以繁體中文或英文適當呈現
-網頁內容中提供完整詳細操作指導
##程式運作將必要的互動追蹤訊息 完整輸出至序列埠,速率 115200
##程式碼逐行以繁體中文註解
#程式安裝指導說明
##程式若使用第三方函式庫,須提供函式庫下載與安裝的詳細操作
##提供程式燒錄使用的詳細操作
Code
/*
* 專案名稱:ESP32 AP 模式 Web Server 控制內建 LED 燈
* 開發板:NodeMCU-32S (ESP32)
* 開發環境:Arduino IDE 1.8.19 / ESP32 Core 3.1.1
* 說明:建立 AP 模式熱點,提供單頁響應式網頁,透過 Ajax 與 REST API 切換與讀取內建 LED 狀態。
*/
// 引用 ESP32 WiFi 函式庫,用於建立 AP 模式與網路管理
#include <WiFi.h>
// 引用 ESP32 WebServer 函式庫,用於建立 HTTP Web 伺服器
#include <WebServer.h>
// 定義 Wi-Fi AP 模式的 SSID (熱點名稱)
const char* ap_ssid = "ymiot1151";
// 定義 Wi-Fi AP 模式的密碼 (至少需要 8 個字元)
const char* ap_password = "12345678";
// 定義 NodeMCU-32S 開發板內建 LED 腳位 (GPIO 2)
const int LED_PIN = 2;
// 建立運行於 80 通訊埠的 WebServer 物件
WebServer server(80);
// 記錄當前內建 LED 的開關狀態 (true 表示亮燈 ON,false 表示熄滅 OFF)
bool ledState = false;
// 宣告全域時間變數,用於非阻塞式的網路連線狀態檢查
unsigned long lastCheckTime = 0;
// 定義連線檢查的時間間隔為 5000 毫秒 (5 秒)
const unsigned long checkInterval = 5000;
// 函數宣告:傳回單頁 HTML/CSS/JavaScript 網頁內容
String getHTMLPage();
// 函數宣告:處理 HTTP GET / 請求 (回應主網頁)
void handleRoot();
// 函數宣告:處理 HTTP GET /toggle 請求 (切換 LED 狀態並回傳 JSON)
void handleToggle();
// 函數宣告:處理 HTTP GET /status 請求 (讀取當前狀態並回傳 JSON)
void handleStatus();
// 函數宣告:處理找不到路徑的 HTTP 404 請求
void handleNotFound();
// 函數宣告:檢查並維護 AP 模式網路連線
void checkWiFiAPStatus();
// -----------------------------------------------------------------------------
// Arduino 初始化函數 setup()
// -----------------------------------------------------------------------------
void setup() {
// 初始化序列埠傳輸,傳輸速率設定為 115200 bps
Serial.begin(115200);
// 等待序列埠準備完成 (確保啟動訊息能完整印出)
delay(1000);
// 印出系統啟動分隔線
Serial.println("\n==========================================");
// 印出系統初始化起始訊息
Serial.println("[系統訊息] ESP32 Web Server 控制系統啟動中...");
// 將 LED 腳位設定為輸出模式 (OUTPUT)
pinMode(LED_PIN, OUTPUT);
// 初始化 LED 腳位輸出為低電位 (關閉 LED)
digitalWrite(LED_PIN, LOW);
// 在序列埠顯示 LED 腳位初始化完成狀態
Serial.println("[硬體設定] GPIO 2 (內建 LED) 已設定為輸出模式,預設狀態:OFF");
// 設定 Wi-Fi 模式為存取點模式 (AP Mode)
WiFi.mode(WIFI_AP);
// 啟動 Wi-Fi AP 熱點,並傳入 SSID 與密碼
bool apStarted = WiFi.softAP(ap_ssid, ap_password);
// 判斷 AP 熱點是否建立成功
if (apStarted) {
// 序列埠印出 AP 建立成功訊息
Serial.println("[網路設定] Wi-Fi AP 模式啟動成功!");
// 印出 AP 的 SSID 名稱
Serial.print("[網路設定] 熱點名稱 (SSID): ");
Serial.println(ap_ssid);
// 印出 AP 的 IP 位址 (預設通常為 192.168.4.1)
Serial.print("[網路設定] ESP32 IP 位址: ");
Serial.println(WiFi.softAPIP());
} else {
// 若建立失敗,印出錯誤訊息
Serial.println("[網路錯誤] Wi-Fi AP 模式啟動失敗,請檢查設定!");
}
// 設定當使用者存取網站根目錄 "/" 時所執行的處理函數
server.on("/", handleRoot);
// 設定當使用者發送 "/toggle" 請求時所執行的切換控制處理函數
server.on("/toggle", handleToggle);
// 設定當使用者發送 "/status" 請求時所執行的狀態查詢處理函數
server.on("/status", handleStatus);
// 設定當存取未定義路由時的回應函數 (HTTP 404)
server.onNotFound(handleNotFound);
// 啟動 Web 伺服器
server.begin();
// 在序列埠顯示 HTTP 伺服器啟動完成訊息
Serial.println("[服務設定] HTTP Web 伺服器已成功啟動 (Port 80)");
Serial.println("==========================================\n");
}
// -----------------------------------------------------------------------------
// Arduino 主迴圈 loop()
// -----------------------------------------------------------------------------
void loop() {
// 持續處理來自用戶端的 HTTP 請求
server.handleClient();
// 取得目前晶片運作時間 (毫秒)
unsigned long currentMillis = millis();
// 檢查是否達到設定的 5 秒檢查時間間隔
if (currentMillis - lastCheckTime >= checkInterval) {
// 更新最後檢查時間
lastCheckTime = currentMillis;
// 執行 AP 網路狀態檢查與維護函數
checkWiFiAPStatus();
}
}
// -----------------------------------------------------------------------------
// 檢查並自動重新建立 AP 模式連線
// -----------------------------------------------------------------------------
void checkWiFiAPStatus() {
// 取得目前連線至 ESP32 AP 的用戶端數量
int stationCount = WiFi.softAPgetStationNum();
// 在序列埠輸出連線維護追蹤追蹤訊息
Serial.print("[網路狀態診斷] AP 運作中 | 當前已連線裝置數: ");
Serial.println(stationCount);
// 檢查 SoftAP 的 IP 位址是否有效,若為 IPAddress(0,0,0,0) 表示 AP 異常跌線
if (WiFi.softAPIP() == IPAddress(0, 0, 0, 0)) {
// 輸出網路斷線警告訊息
Serial.println("[網路警告] 偵測到 AP 服務異常跌線,嘗試重新啟動 AP 連線...");
// 斷開並重置 Wi-Fi 模式
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_AP);
// 重新啟動 AP 模式
if (WiFi.softAP(ap_ssid, ap_password)) {
// 輸出重連成功訊息與新 IP
Serial.println("[網路恢復] AP 連線重新建立成功!");
Serial.print("[網路恢復] 目前 IP 位址: ");
Serial.println(WiFi.softAPIP());
} else {
// 輸出重連失敗訊息
Serial.println("[網路錯誤] AP 重新建立失敗,將於下次檢測時重試。");
}
}
}
// -----------------------------------------------------------------------------
// 處理根目錄 HTTP GET / 請求 (輸出網頁 HTML)
// -----------------------------------------------------------------------------
void handleRoot() {
// 印出連線紀錄追蹤
Serial.println("[HTTP 請求] 用戶端存取主頁面 (/)");
// 發送 HTTP 200 回應,內容型態為 text/html,並附帶網頁完整原始碼
server.send(200, "text/html", getHTMLPage());
}
// -----------------------------------------------------------------------------
// 處理切換 LED 狀態請求 HTTP GET /toggle
// -----------------------------------------------------------------------------
void handleToggle() {
// 讀取當前 GPIO 2 的實際輸出狀態並進行反轉 (HIGH 變 LOW,LOW 變 HIGH)
ledState = !digitalRead(LED_PIN);
// 將反轉後的狀態寫入 GPIO 2 腳位
digitalWrite(LED_PIN, ledState ? HIGH : LOW);
// 讀取實體腳位狀態確保數據精確性
int actualPinState = digitalRead(LED_PIN);
// 將數字狀態轉為可讀性高的文字標籤 ("ON" 或 "OFF")
String statusStr = (actualPinState == HIGH) ? "ON" : "OFF";
// 組裝標準 JSON 格式數據字串,如:{"GPIO2": "ON"}
String jsonResponse = "{\"GPIO2\":\"" + statusStr + "\"}";
// 在序列埠印出動作追蹤紀錄
Serial.print("[動作追蹤] 控制按鈕觸發 -> 切換 GPIO 2 LED 狀態為: ");
Serial.print(statusStr);
Serial.print(" | 回傳 JSON: ");
Serial.println(jsonResponse);
// 回傳 HTTP 200 狀態碼與 JSON 數據格式
server.send(200, "application/json", jsonResponse);
}
// -----------------------------------------------------------------------------
// 處理查詢當前 LED 狀態請求 HTTP GET /status
// -----------------------------------------------------------------------------
void handleStatus() {
// 讀取當前 GPIO 2 的實體電位狀態
int actualPinState = digitalRead(LED_PIN);
// 判斷狀態字串
String statusStr = (actualPinState == HIGH) ? "ON" : "OFF";
// 組裝 JSON 狀態回應格式
String jsonResponse = "{\"GPIO2\":\"" + statusStr + "\"}";
// 在序列埠記錄查詢動作
Serial.print("[查詢追蹤] 讀取狀態請求 -> GPIO 2 目前狀態: ");
Serial.println(statusStr);
// 回傳 HTTP 200 與 JSON 數據
server.send(200, "application/json", jsonResponse);
}
// -----------------------------------------------------------------------------
// 處理 HTTP 404 找不到頁面
// -----------------------------------------------------------------------------
void handleNotFound() {
// 記錄 404 錯誤路徑
Serial.print("[HTTP 警告] 收到未知路徑請求: ");
Serial.println(server.uri());
// 回傳 HTTP 404 錯誤頁面
server.send(404, "text/plain", "404: Not Found");
}
// -----------------------------------------------------------------------------
// 前端網頁 HTML、CSS 與 jQuery JavaScript 網頁字串產生器
// -----------------------------------------------------------------------------
String getHTMLPage() {
// 使用 C++ R"rawliteral(...)" 原始字串巨集,避免繁瑣的引號跳脫字元
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 物聯網控制中心</title>
<!-- 引入 jQuery 3.6.0 CDN (若網頁無法連外網,腳本具備原生 fallback 非同步機制) -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
/* 全域樣式重置與高對比暗色主題設定 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #121212;
color: #E0E0E0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
/* 主卡片容器 */
.container {
background-color: #1E1E1E;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.8);
width: 100%;
max-width: 480px;
padding: 28px;
border: 1px solid #333333;
}
/* 標題區域 */
h1 {
font-size: 1.6rem;
text-align: center;
color: #FFFFFF;
margin-bottom: 8px;
}
.subtitle {
font-size: 0.9rem;
text-align: center;
color: #A0A0A0;
margin-bottom: 24px;
}
/* 開關卡片區塊 */
.control-section {
background-color: #2A2A2A;
border-radius: 12px;
padding: 20px;
text-align: center;
margin-bottom: 24px;
border: 1px solid #3E3E3E;
}
.control-title {
font-size: 1.1rem;
margin-bottom: 16px;
color: #FFFFFF;
}
/* 大型開關按鈕樣式 */
.btn-toggle {
width: 100%;
padding: 16px;
font-size: 1.2rem;
font-weight: bold;
color: #FFFFFF;
background-color: #2563EB;
border: none;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
}
.btn-toggle:active {
transform: scale(0.98);
}
/* 狀態顯示卡片 */
.status-box {
margin-top: 16px;
padding: 12px;
background-color: #121212;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.status-label {
font-size: 0.95rem;
color: #B0B0B0;
}
/* 狀態標籤高對比動態色彩 */
.status-badge {
font-size: 1rem;
font-weight: bold;
padding: 6px 14px;
border-radius: 20px;
transition: background-color 0.3s ease;
}
.status-off {
background-color: #EF4444;
color: #FFFFFF;
box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
}
.status-on {
background-color: #22C55E;
color: #FFFFFF;
box-shadow: 0 0 10px rgba(34, 197, 94, 0.5);
}
/* JSON 原始數據顯示區 */
.json-display {
margin-top: 12px;
font-family: 'Courier New', Courier, monospace;
font-size: 0.85rem;
color: #38BDF8;
background-color: #0F172A;
padding: 8px 12px;
border-radius: 6px;
text-align: left;
word-break: break-all;
}
/* 操作指導區塊 */
.guide-section {
background-color: #252525;
border-radius: 12px;
padding: 18px;
border-left: 4px solid #2563EB;
}
.guide-title {
font-size: 1rem;
font-weight: bold;
color: #FFFFFF;
margin-bottom: 10px;
display: flex;
align-items: center;
}
.guide-list {
padding-left: 18px;
font-size: 0.88rem;
line-height: 1.6;
color: #CCCCCC;
}
.guide-list li {
margin-bottom: 6px;
}
.highlight {
color: #FACC15;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>ESP32 物聯網控制台</h1>
<div class="subtitle">NodeMCU-32S 單頁響應式控制網頁</div>
<!-- 裝置控制核心卡片 -->
<div class="control-section">
<div class="control-title">內建 LED 燈控制 (GPIO 2)</div>
<!-- 開關控制按鈕 -->
<button id="toggleBtn" class="btn-toggle">切換開關狀態</button>
<!-- 即時狀態卡片 -->
<div class="status-box">
<span class="status-label">目前實體腳位狀態:</span>
<span id="statusBadge" class="status-badge status-off">讀取中...</span>
</div>
<!-- JSON 數據回傳即時卡片 -->
<div class="json-display">
內部 REST API JSON:<span id="jsonOutput">{"GPIO2":"UNKNOWN"}</span>
</div>
</div>
<!-- 操作指導與說明卡片 -->
<div class="guide-section">
<div class="guide-title">💡 詳細操作指導說明</div>
<ol class="guide-list">
<li><strong>連線熱點:</strong>請確保手機/電腦已連線至 Wi-Fi 熱點 <span class="highlight">ymiot1151</span> (密碼:<span class="highlight">12345678</span>)。</li>
<li><strong>開關控制:</strong>點擊上方藍色 <span class="highlight">「切換開關狀態」</span> 按鈕,透過非同步 Ajax 傳送指令至 ESP32。</li>
<li><strong>狀態確認:</strong>動作結束後,網頁將自動更新並顯示最新的 GPIO 2 實體位準與 <span class="highlight">JSON 數據</span>。</li>
<li><strong>內建 LED 指示:</strong>亮綠燈時開發板上的藍色板載 LED 會亮起,亮紅燈則熄滅。</li>
</ol>
</div>
</div>
<script>
// 網頁載入完成後執行的 JavaScript 主程式
$(document).ready(function() {
// 封裝更新前端 UI 介面的函數
function updateUI(status) {
var badge = $('#statusBadge');
var jsonOut = $('#jsonOutput');
// 判斷狀態文字
if (status === "ON") {
badge.text("開啟 (ON)").removeClass("status-off").addClass("status-on");
jsonOut.text('{"GPIO2":"ON"}');
} else if (status === "OFF") {
badge.text("關閉 (OFF)").removeClass("status-on").addClass("status-off");
jsonOut.text('{"GPIO2":"OFF"}');
} else {
badge.text("未知的狀態").removeClass("status-on status-off");
jsonOut.text('{"GPIO2":"ERROR"}');
}
}
// 封裝 Ajax 查詢狀態函數
function fetchStatus() {
$.ajax({
url: '/status',
type: 'GET',
dataType: 'json',
timeout: 3000,
success: function(data) {
if (data && data.GPIO2) {
updateUI(data.GPIO2);
}
},
error: function() {
console.log("無法連線至 ESP32 伺服器進行狀態查詢");
}
});
}
// 綁定切換按鈕點擊事件
$('#toggleBtn').on('click', function() {
// 禁用按鈕防止重複連點
var btn = $(this);
btn.prop('disabled', true).css('opacity', '0.6');
// 發送非同步 Ajax 控制請求
$.ajax({
url: '/toggle',
type: 'GET',
dataType: 'json',
timeout: 4000,
success: function(data) {
if (data && data.GPIO2) {
// 更新頁面狀態顯示
updateUI(data.GPIO2);
}
},
error: function(xhr, status, error) {
alert("網路傳送失敗,請確認是否仍連線至 ymiot1151 熱點!");
},
complete: function() {
// 恢復按鈕可點擊狀態
btn.prop('disabled', false).css('opacity', '1.0');
}
});
});
// 頁面初始化時,立即呼叫一次狀態讀取
fetchStatus();
});
</script>
</body>
</html>
)rawliteral";
// 回傳完整網頁 HTML 字串
return html;
}
複製 貼上 Arduino IDE,驗證,驗證無誤,上傳
若驗證錯誤,將錯誤訊息貼回 Gemini 迭代
上傳完畢,開啟序列埠視窗
更改速率 115200,抓控制板 SSID: ymiot1151(密碼 12345678), Web Server IP (192.168.4.1)
筆電、手機連線 ESP32 WiFi AP
筆電連上 ESP32 WiFi AP
手機連上 ESP32 WiFi AP
筆電開瀏覽器,網址列輸入 192.168.4.1,試試 ON, OFF,正常
手機開瀏覽器,網址列輸入 192.168.4.1,網頁異常, ON, OFF 按鈕無作用
提示詞
#測試結果
- 第一個裝置連入 192.168.4.1 可以正確開關 LED
- 第一個裝置連入 192.168.4.1 時,網頁切換開關無作用 腳位狀態顯示:讀取中 回應 JSON:{"GPIO2":"UNKNOWN"}
#網頁修正:
##開關LED的按鈕不夠明確,請改成下面樣式
-請更改成單一切換開關,往左往右分別是ON OFF
-切換開關的標籤改為作用腳位(如:GPIO2)
-切換開關的左右兩端,以文字明確標示 ON OFF
##回應訊息:
-以文字樣式回應訊息,格式如下
目前狀態:
GPIO2:ON
-刪除原回應的JSON訊息
開始修改前,先分析問題,提出你的規劃給我確認
Code
/*
* 專案名稱:ESP32 AP 模式 Web Server 控制內建 LED 燈 (無外網依賴優化版)
* 開發板:NodeMCU-32S (ESP32)
* 開發環境:Arduino IDE 1.8.19 / ESP32 Core 3.1.1
* 修正說明:
* 1. 移除外部 jQuery CDN 依賴,改用原生 Vanilla JS (fetch API),解決多裝置連入 AP 無外網時卡死的問題。
* 2. 前端改用滑動開關 (Toggle Switch),標籤顯示 GPIO 2,兩端標示 OFF/ON。
* 3. 訊息回應格式改為純文字樣式:「目前狀態:\n GPIO2:ON」。
*/
// 引用 ESP32 WiFi 函式庫,用於建立 AP 模式與網路管理
#include <WiFi.h>
// 引用 ESP32 WebServer 函式庫,用於建立 HTTP Web 伺服器
#include <WebServer.h>
// 定義 Wi-Fi AP 模式的 SSID (熱點名稱)
const char* ap_ssid = "ymiot1151";
// 定義 Wi-Fi AP 模式的密碼 (至少需要 8 個字元)
const char* ap_password = "12345678";
// 定義 NodeMCU-32S 開發板內建 LED 腳位 (GPIO 2)
const int LED_PIN = 2;
// 建立運行於 80 通訊埠的 WebServer 物件
WebServer server(80);
// 記錄當前內建 LED 的開關狀態 (true 表示亮燈 ON,false 表示熄滅 OFF)
bool ledState = false;
// 宣告全域時間變數,用於非阻塞式的網路連線狀態檢查
unsigned long lastCheckTime = 0;
// 定義連線檢查的時間間隔為 5000 毫秒 (5 秒)
const unsigned long checkInterval = 5000;
// 函數宣告:傳回單頁 HTML/CSS/JavaScript 網頁內容
String getHTMLPage();
// 函數宣告:處理 HTTP GET / 請求 (回應主網頁)
void handleRoot();
// 函數宣告:處理 HTTP GET /toggle 請求 (切換 LED 狀態並回傳 JSON)
void handleToggle();
// 函數宣告:處理 HTTP GET /status 請求 (讀取當前狀態並回傳 JSON)
void handleStatus();
// 函數宣告:處理找不到路徑的 HTTP 404 請求
void handleNotFound();
// 函數宣告:檢查並維護 AP 模式網路連線
void checkWiFiAPStatus();
// -----------------------------------------------------------------------------
// Arduino 初始化函數 setup()
// -----------------------------------------------------------------------------
void setup() {
// 初始化序列埠傳輸,傳輸速率設定為 115200 bps
Serial.begin(115200);
// 等待序列埠準備完成 (確保啟動訊息能完整印出)
delay(1000);
// 印出系統啟動分隔線
Serial.println("\n==========================================");
// 印出系統初始化起始訊息
Serial.println("[系統訊息] ESP32 Web Server 控制系統啟動中...");
// 將 LED 腳位設定為輸出模式 (OUTPUT)
pinMode(LED_PIN, OUTPUT);
// 初始化 LED 腳位輸出為低電位 (關閉 LED)
digitalWrite(LED_PIN, LOW);
// 在序列埠顯示 LED 腳位初始化完成狀態
Serial.println("[硬體設定] GPIO 2 (內建 LED) 已設定為輸出模式,預設狀態:OFF");
// 設定 Wi-Fi 模式為存取點模式 (AP Mode)
WiFi.mode(WIFI_AP);
// 啟動 Wi-Fi AP 熱點,並傳入 SSID 與密碼
bool apStarted = WiFi.softAP(ap_ssid, ap_password);
// 判斷 AP 熱點是否建立成功
if (apStarted) {
// 序列埠印出 AP 建立成功訊息
Serial.println("[網路設定] Wi-Fi AP 模式啟動成功!");
// 印出 AP 的 SSID 名稱
Serial.print("[網路設定] 熱點名稱 (SSID): ");
Serial.println(ap_ssid);
// 印出 AP 的 IP 位址 (預設通常為 192.168.4.1)
Serial.print("[網路設定] ESP32 IP 位址: ");
Serial.println(WiFi.softAPIP());
} else {
// 若建立失敗,印出錯誤訊息
Serial.println("[網路錯誤] Wi-Fi AP 模式啟動失敗,請檢查設定!");
}
// 設定當使用者存取網站根目錄 "/" 時所執行的處理函數
server.on("/", handleRoot);
// 設定當使用者發送 "/toggle" 請求時所執行的切換控制處理函數
server.on("/toggle", handleToggle);
// 設定當使用者發送 "/status" 請求時所執行的狀態查詢處理函數
server.on("/status", handleStatus);
// 設定當存取未定義路由時的回應函數 (HTTP 404)
server.onNotFound(handleNotFound);
// 啟動 Web 伺服器
server.begin();
// 在序列埠顯示 HTTP 伺服器啟動完成訊息
Serial.println("[服務設定] HTTP Web 伺服器已成功啟動 (Port 80)");
Serial.println("==========================================\n");
}
// -----------------------------------------------------------------------------
// Arduino 主迴圈 loop()
// -----------------------------------------------------------------------------
void loop() {
// 持續處理來自用戶端的 HTTP 請求
server.handleClient();
// 取得目前晶片運作時間 (毫秒)
unsigned long currentMillis = millis();
// 檢查是否達到設定的 5 秒檢查時間間隔
if (currentMillis - lastCheckTime >= checkInterval) {
// 更新最後檢查時間
lastCheckTime = currentMillis;
// 執行 AP 網路狀態檢查與維護函數
checkWiFiAPStatus();
}
}
// -----------------------------------------------------------------------------
// 檢查並自動重新建立 AP 模式連線
// -----------------------------------------------------------------------------
void checkWiFiAPStatus() {
// 取得目前連線至 ESP32 AP 的用戶端數量
int stationCount = WiFi.softAPgetStationNum();
// 在序列埠輸出連線維護追蹤訊息
Serial.print("[網路狀態診斷] AP 運作中 | 當前連線裝置數: ");
Serial.println(stationCount);
// 檢查 SoftAP 的 IP 位址是否有效,若為 IPAddress(0,0,0,0) 表示 AP 異常跌線
if (WiFi.softAPIP() == IPAddress(0, 0, 0, 0)) {
// 輸出網路斷線警告訊息
Serial.println("[網路警告] 偵測到 AP 服務異常跌線,嘗試重新啟動 AP 連線...");
// 斷開並重置 Wi-Fi 模式
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_AP);
// 重新啟動 AP 模式
if (WiFi.softAP(ap_ssid, ap_password)) {
// 輸出重連成功訊息與新 IP
Serial.println("[網路恢復] AP 連線重新建立成功!");
Serial.print("[網路恢復] 目前 IP 位址: ");
Serial.println(WiFi.softAPIP());
} else {
// 輸出重連失敗訊息
Serial.println("[網路錯誤] AP 重新建立失敗,將於下次檢測時重試。");
}
}
}
// -----------------------------------------------------------------------------
// 處理根目錄 HTTP GET / 請求 (輸出網頁 HTML)
// -----------------------------------------------------------------------------
void handleRoot() {
// 印出連線紀錄追蹤
Serial.println("[HTTP 請求] 用戶端存取主頁面 (/)");
// 發送 HTTP 200 回應,內容型態為 text/html,並附帶網頁完整原始碼
server.send(200, "text/html", getHTMLPage());
}
// -----------------------------------------------------------------------------
// 處理切換 LED 狀態請求 HTTP GET /toggle
// -----------------------------------------------------------------------------
void handleToggle() {
// 讀取當前 GPIO 2 的實際輸出狀態並進行反轉 (HIGH 變 LOW,LOW 變 HIGH)
ledState = !digitalRead(LED_PIN);
// 將反轉後的狀態寫入 GPIO 2 腳位
digitalWrite(LED_PIN, ledState ? HIGH : LOW);
// 讀取實體腳位狀態確保數據精確性
int actualPinState = digitalRead(LED_PIN);
// 將數字狀態轉為可讀性高的文字標籤 ("ON" 或 "OFF")
String statusStr = (actualPinState == HIGH) ? "ON" : "OFF";
// 組裝標準 JSON 格式數據字串回傳給前端 script 解析
String jsonResponse = "{\"GPIO2\":\"" + statusStr + "\"}";
// 在序列埠印出動作追蹤紀錄
Serial.print("[動作追蹤] 切換開關觸發 -> 切換 GPIO 2 LED 狀態為: ");
Serial.print(statusStr);
Serial.print(" | 回傳 JSON: ");
Serial.println(jsonResponse);
// 回傳 HTTP 200 狀態碼與 JSON 數據格式
server.send(200, "application/json", jsonResponse);
}
// -----------------------------------------------------------------------------
// 處理查詢當前 LED 狀態請求 HTTP GET /status
// -----------------------------------------------------------------------------
void handleStatus() {
// 讀取當前 GPIO 2 的實體電位狀態
int actualPinState = digitalRead(LED_PIN);
// 判斷狀態字串
String statusStr = (actualPinState == HIGH) ? "ON" : "OFF";
// 組裝 JSON 狀態回應格式
String jsonResponse = "{\"GPIO2\":\"" + statusStr + "\"}";
// 在序列埠記錄查詢動作
Serial.print("[查詢追蹤] 讀取狀態請求 -> GPIO 2 目前狀態: ");
Serial.println(statusStr);
// 回傳 HTTP 200 與 JSON 數據
server.send(200, "application/json", jsonResponse);
}
// -----------------------------------------------------------------------------
// 處理 HTTP 404 找不到頁面
// -----------------------------------------------------------------------------
void handleNotFound() {
// 記錄 404 錯誤路徑
Serial.print("[HTTP 警告] 收到未知路徑請求: ");
Serial.println(server.uri());
// 回傳 HTTP 404 錯誤頁面
server.send(404, "text/plain", "404: Not Found");
}
// -----------------------------------------------------------------------------
// 前端網頁 HTML、CSS 與 原生 JavaScript (無外部依賴) 網頁字串產生器
// -----------------------------------------------------------------------------
String getHTMLPage() {
// 使用 C++ R"rawliteral(...)" 原始字串巨集,避免引號跳脫字元
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 物聯網控制中心</title>
<style>
/* 全域樣式設定 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Arial, sans-serif;
}
body {
background-color: #121212;
color: #E0E0E0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
/* 主卡片容器 */
.container {
background-color: #1E1E1E;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.8);
width: 100%;
max-width: 440px;
padding: 28px;
border: 1px solid #333333;
}
h1 {
font-size: 1.5rem;
text-align: center;
color: #FFFFFF;
margin-bottom: 6px;
}
.subtitle {
font-size: 0.85rem;
text-align: center;
color: #888888;
margin-bottom: 24px;
}
/* 開關控制區域 */
.control-section {
background-color: #262626;
border-radius: 12px;
padding: 24px;
text-align: center;
margin-bottom: 24px;
border: 1px solid #3A3A3A;
}
/* 作用腳位標籤 */
.pin-label {
font-size: 1.25rem;
font-weight: bold;
color: #38BDF8;
margin-bottom: 20px;
letter-spacing: 1px;
}
/* 滑動開關容器 (包含左右 OFF/ON 標籤) */
.switch-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 24px;
}
/* 開關兩端標籤文字 */
.switch-side-label {
font-size: 1.1rem;
font-weight: bold;
width: 45px;
transition: color 0.3s ease;
}
.label-off {
text-align: right;
color: #EF4444; /* 紅色 */
}
.label-on {
text-align: left;
color: #22C55E; /* 綠色 */
}
/* 原生 Checkbox 轉滑動 Toggle 開關樣式 */
.switch {
position: relative;
display: inline-block;
width: 74px;
height: 38px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* 滑塊軌道 */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #EF4444; /* 預設 OFF 為紅色軌道 */
transition: .4s;
border-radius: 38px;
box-shadow: inset 0 2px 6px rgba(0,0,0,0.5);
}
/* 滑塊圓球 */
.slider:before {
position: absolute;
content: "";
height: 30px;
width: 30px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0,0,0,0.4);
}
/* 切換至 ON 狀態的軌道顏色變更與圓球移動 */
input:checked + .slider {
background-color: #22C55E; /* ON 變為綠色軌道 */
}
input:checked + .slider:before {
transform: translateX(36px);
}
/* 開關停用時透明度變化 */
input:disabled + .slider {
opacity: 0.5;
cursor: not-allowed;
}
/* 狀態回應顯示卡片區塊 */
.response-card {
background-color: #121212;
border-radius: 8px;
padding: 16px;
text-align: left;
border-left: 4px solid #38BDF8;
}
.response-title {
font-size: 0.95rem;
color: #AAAAAA;
margin-bottom: 6px;
}
.response-content {
font-size: 1.2rem;
font-weight: bold;
color: #FFFFFF;
font-family: 'Courier New', Courier, monospace;
white-space: pre-line; /* 保持斷行文字格式 */
}
.status-highlight-on {
color: #22C55E;
}
.status-highlight-off {
color: #EF4444;
}
/* 詳細操作指導卡片 */
.guide-section {
background-color: #262626;
border-radius: 12px;
padding: 18px;
border: 1px solid #3A3A3A;
}
.guide-title {
font-size: 0.95rem;
font-weight: bold;
color: #FFFFFF;
margin-bottom: 10px;
}
.guide-list {
padding-left: 18px;
font-size: 0.85rem;
line-height: 1.6;
color: #B0B0B0;
}
.guide-list li {
margin-bottom: 4px;
}
.hl {
color: #FACC15;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>ESP32 物聯網控制台</h1>
<div class="subtitle">NodeMCU-32S 開發板熱點控制頁面</div>
<!-- 裝置控制卡片區域 -->
<div class="control-section">
<!-- 切換開關標籤顯示作用腳位 -->
<div class="pin-label">作用腳位:GPIO2</div>
<!-- 切換開關:左右標示 OFF/ON -->
<div class="switch-wrapper">
<span class="switch-side-label label-off">OFF</span>
<label class="switch">
<input type="checkbox" id="ledToggle">
<span class="slider"></span>
</label>
<span class="switch-side-label label-on">ON</span>
</div>
<!-- 訊息回應區塊 (純文字格式) -->
<div class="response-card">
<div class="response-title">系統回應訊息:</div>
<div id="responseText" class="response-content">目前狀態:
GPIO2:讀取中...</div>
</div>
</div>
<!-- 詳細操作指導說明區塊 -->
<div class="guide-section">
<div class="guide-title">💡 操作指導說明</div>
<ol class="guide-list">
<li><strong>連線熱點:</strong>請確保已連接 Wi-Fi <span class="hl">ymiot1151</span> (密碼:<span class="hl">12345678</span>)。</li>
<li><strong>開關控制:</strong>撥動 <span class="hl">GPIO2</span> 單一切換開關即可切換內建 LED 燈。</li>
<li><strong>狀態確認:</strong>撥動後系統會回傳最新的 <span class="hl">GPIO2:ON/OFF</span> 狀態。</li>
<li><strong>多裝置連線:</strong>採用原生非同步語法,支援多台手機/電腦同時存取控制。</li>
</ol>
</div>
</div>
<script>
// 採用原生 JavaScript (不依賴外部 jQuery),確保 AP 離線環境 100% 正常運作
document.addEventListener('DOMContentLoaded', function() {
var toggle = document.getElementById('ledToggle');
var responseText = document.getElementById('responseText');
// 更新回應文字卡片的函數
function updateResponseView(status) {
if (status === "ON") {
toggle.checked = true;
responseText.innerHTML = '目前狀態:\nGPIO2:<span class="status-highlight-on">ON</span>';
} else if (status === "OFF") {
toggle.checked = false;
responseText.innerHTML = '目前狀態:\nGPIO2:<span class="status-highlight-off">OFF</span>';
} else {
responseText.innerHTML = '目前狀態:\nGPIO2:未知狀態';
}
}
// 向 ESP32 發送狀態查詢 (GET /status)
function fetchStatus() {
fetch('/status')
.then(function(response) {
return response.json();
})
.then(function(data) {
if (data && data.GPIO2) {
updateResponseView(data.GPIO2);
}
})
.catch(function(error) {
console.log('連線狀態讀取失敗:', error);
});
}
// 監聽開關切換動作 (觸發 GET /toggle)
toggle.addEventListener('change', function() {
// 切換期間暫時鎖定開關,防止連點
toggle.disabled = true;
fetch('/toggle')
.then(function(response) {
return response.json();
})
.then(function(data) {
if (data && data.GPIO2) {
updateResponseView(data.GPIO2);
}
})
.catch(function(error) {
alert('連線失敗,請檢查 Wi-Fi 是否仍連線至 ymiot1151 熱點!');
// 失敗時同步還原 toggle 狀態
toggle.checked = !toggle.checked;
})
.finally(function() {
// 動作完成後解除鎖定
toggle.disabled = false;
});
});
// 頁面初始化載入時讀取一次腳位狀態
fetchStatus();
});
</script>
</body>
</html>
)rawliteral";
// 回傳完整網頁 HTML 字串
return html;
}
筆電開瀏覽器,網址列輸入 192.168.4.1,試試 ON, OFF,正常
手機開瀏覽器,網址列輸入 192.168.4.1,試試 ON, OFF,正常
提示詞
程式運作正確,我要進一步優化,需求
1.回應框 刪除文字 "系統回應訊息:" ,其他不做更動
2.網頁增加一個回應框,顯示最近五次發出 ON OFF 的連線 IP訊息,顯示格式如
IP:ON
IP:ON
...
3.序列埠輸出訊息增加HTTP請求的連線IP
開始修改前,先分析問題,提出你的規劃給我確認
Code
/*
* 專案名稱:ESP32 AP 模式 Web Server 控制內建 LED 燈 (IP 紀錄與序列埠追蹤進階版)
* 開發板:NodeMCU-32S (ESP32)
* 開發環境:Arduino IDE 1.8.19 / ESP32 Core 3.1.1
* 需求優化說明:
* 1. 移除第一個回應框中的「系統回應訊息:」標題文字。
* 2. 網頁新增第二個回應框,動態顯示最近 5 次觸發 ON/OFF 的連線 IP 歷史紀錄 (格式:IP:ON/OFF)。
* 3. 序列埠 (Serial) 輸出訊息補強,所有 HTTP 請求皆記錄連線用戶端的真實 IP 位址。
*/
// 引用 ESP32 WiFi 函式庫,用於建立 AP 模式與網路管理
#include <WiFi.h>
// 引用 ESP32 WebServer 函式庫,用於建立 HTTP Web 伺服器
#include <WebServer.h>
// 定義 Wi-Fi AP 模式的 SSID (熱點名稱)
const char* ap_ssid = "ymiot1151";
// 定義 Wi-Fi AP 模式的密碼 (至少需要 8 個字元)
const char* ap_password = "12345678";
// 定義 NodeMCU-32S 開發板內建 LED 腳位 (GPIO 2)
const int LED_PIN = 2;
// 建立運行於 80 通訊埠的 WebServer 物件
WebServer server(80);
// 記錄當前內建 LED 的開關狀態 (true 表示亮燈 ON,false 表示熄滅 OFF)
bool ledState = false;
// 宣告全域時間變數,用於非阻塞式的網路連線狀態檢查
unsigned long lastCheckTime = 0;
// 定義連線檢查的時間間隔為 5000 毫秒 (5 秒)
const unsigned long checkInterval = 5000;
// 宣告全域陣列,用於儲存最近 5 次操作的歷史紀錄字串 (格式:"IP:ON" 或 "IP:OFF")
String ipHistory[5] = {"", "", "", "", ""};
// 記錄目前歷史紀錄的有效筆數 (最多為 5 筆)
int historyCount = 0;
// 函數宣告:傳回單頁 HTML/CSS/JavaScript 網頁內容
String getHTMLPage();
// 函數宣告:處理 HTTP GET / 請求 (回應主網頁)
void handleRoot();
// 函數宣告:處理 HTTP GET /toggle 請求 (切換 LED 狀態並回傳 JSON)
void handleToggle();
// 函數宣告:處理 HTTP GET /status 請求 (讀取當前狀態與歷史紀錄並回傳 JSON)
void handleStatus();
// 函數宣告:處理找不到路徑的 HTTP 404 請求
void handleNotFound();
// 函數宣告:檢查並維護 AP 模式網路連線
void checkWiFiAPStatus();
// 函數宣告:新增一筆連線 IP 與操作紀錄至長度為 5 的歷史陣列
void addHistoryEntry(String clientIP, String statusStr);
// 函數宣告:取得歷史紀錄陣列並轉換為 JSON 格式字串
String getHistoryJsonArray();
// -----------------------------------------------------------------------------
// Arduino 初始化函數 setup()
// -----------------------------------------------------------------------------
void setup() {
// 初始化序列埠傳輸,傳輸速率設定為 115200 bps
Serial.begin(115200);
// 等待序列埠準備完成 (確保啟動訊息能完整印出)
delay(1000);
// 印出系統啟動分隔線
Serial.println("\n==========================================");
// 印出系統初始化起始訊息
Serial.println("[系統訊息] ESP32 Web Server 控制系統啟動中...");
// 將 LED 腳位設定為輸出模式 (OUTPUT)
pinMode(LED_PIN, OUTPUT);
// 初始化 LED 腳位輸出為低電位 (關閉 LED)
digitalWrite(LED_PIN, LOW);
// 在序列埠顯示 LED 腳位初始化完成狀態
Serial.println("[硬體設定] GPIO 2 (內建 LED) 已設定為輸出模式,預設狀態:OFF");
// 設定 Wi-Fi 模式為存取點模式 (AP Mode)
WiFi.mode(WIFI_AP);
// 啟動 Wi-Fi AP 熱點,並傳入 SSID 與密碼
bool apStarted = WiFi.softAP(ap_ssid, ap_password);
// 判斷 AP 熱點是否建立成功
if (apStarted) {
// 序列埠印出 AP 建立成功訊息
Serial.println("[網路設定] Wi-Fi AP 模式啟動成功!");
// 印出 AP 的 SSID 名稱
Serial.print("[網路設定] 熱點名稱 (SSID): ");
Serial.println(ap_ssid);
// 印出 AP 的 IP 位址 (預設通常為 192.168.4.1)
Serial.print("[網路設定] ESP32 IP 位址: ");
Serial.println(WiFi.softAPIP());
} else {
// 若建立失敗,印出錯誤訊息
Serial.println("[網路錯誤] Wi-Fi AP 模式啟動失敗,請檢查設定!");
}
// 設定當使用者存取網站根目錄 "/" 時所執行的處理函數
server.on("/", handleRoot);
// 設定當使用者發送 "/toggle" 請求時所執行的切換控制處理函數
server.on("/toggle", handleToggle);
// 設定當使用者發送 "/status" 請求時所執行的狀態查詢處理函數
server.on("/status", handleStatus);
// 設定當存取未定義路由時的回應函數 (HTTP 404)
server.onNotFound(handleNotFound);
// 啟動 Web 伺服器
server.begin();
// 在序列埠顯示 HTTP 伺服器啟動完成訊息
Serial.println("[服務設定] HTTP Web 伺服器已成功啟動 (Port 80)");
Serial.println("==========================================\n");
}
// -----------------------------------------------------------------------------
// Arduino 主迴圈 loop()
// -----------------------------------------------------------------------------
void loop() {
// 持續處理來自用戶端的 HTTP 請求
server.handleClient();
// 取得目前晶片運作時間 (毫秒)
unsigned long currentMillis = millis();
// 檢查是否達到設定的 5 秒檢查時間間隔
if (currentMillis - lastCheckTime >= checkInterval) {
// 更新最後檢查時間
lastCheckTime = currentMillis;
// 執行 AP 網路狀態檢查與維護函數
checkWiFiAPStatus();
}
}
// -----------------------------------------------------------------------------
// 新增一筆操作紀錄至歷史紀錄陣列 (保持最新 5 筆)
// -----------------------------------------------------------------------------
void addHistoryEntry(String clientIP, String statusStr) {
// 將舊紀錄往前推移 (第 1 筆變成最舊被覆蓋,最新筆放在最後)
for (int i = 0; i < 4; i++) {
ipHistory[i] = ipHistory[i + 1];
}
// 將最新一筆 "IP:ON" 或 "IP:OFF" 填入第 5 個位置 (索引 4)
ipHistory[4] = clientIP + ":" + statusStr;
// 更新有效紀錄筆數,最高上限為 5
if (historyCount < 5) {
historyCount++;
}
}
// -----------------------------------------------------------------------------
// 將歷史陣列轉為 JSON Array 字串,供前端更新顯示
// -----------------------------------------------------------------------------
String getHistoryJsonArray() {
String jsonArr = "[";
int added = 0;
// 由最新 (索引 4) 往回尋找有效紀錄
for (int i = 4; i >= 0; i--) {
if (ipHistory[i] != "") {
if (added > 0) jsonArr += ",";
jsonArr += "\"" + ipHistory[i] + "\"";
added++;
}
}
jsonArr += "]";
return jsonArr;
}
// -----------------------------------------------------------------------------
// 檢查並自動重新建立 AP 模式連線
// -----------------------------------------------------------------------------
void checkWiFiAPStatus() {
// 取得目前連線至 ESP32 AP 的用戶端數量
int stationCount = WiFi.softAPgetStationNum();
// 在序列埠輸出連線維護追蹤訊息
Serial.print("[網路狀態診斷] AP 運作中 | 當前連線裝置數: ");
Serial.println(stationCount);
// 檢查 SoftAP 的 IP 位址是否有效,若為 IPAddress(0,0,0,0) 表示 AP 異常跌線
if (WiFi.softAPIP() == IPAddress(0, 0, 0, 0)) {
// 輸出網路斷線警告訊息
Serial.println("[網路警告] 偵測到 AP 服務異常跌線,嘗試重新啟動 AP連線...");
// 斷開並重置 Wi-Fi 模式
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_AP);
// 重新啟動 AP 模式
if (WiFi.softAP(ap_ssid, ap_password)) {
// 輸出重連成功訊息與新 IP
Serial.println("[網路恢復] AP 連線重新建立成功!");
Serial.print("[網路恢復] 目前 IP 位址: ");
Serial.println(WiFi.softAPIP());
} else {
// 輸出重連失敗訊息
Serial.println("[網路錯誤] AP 重新建立失敗,將於下次檢測時重試。");
}
}
}
// -----------------------------------------------------------------------------
// 處理根目錄 HTTP GET / 請求 (輸出網頁 HTML)
// -----------------------------------------------------------------------------
void handleRoot() {
// 取得請求來源端的真實 IP 位址
String clientIP = server.client().remoteIP().toString();
// 序列埠印出包含客戶端 IP 的連線日誌
Serial.print("[HTTP 請求] 來自 IP: ");
Serial.print(clientIP);
Serial.println(" 存取主頁面 (/)");
// 發送 HTTP 200 回應,內容型態為 text/html,並附帶網頁完整原始碼
server.send(200, "text/html", getHTMLPage());
}
// -----------------------------------------------------------------------------
// 處理切換 LED 狀態請求 HTTP GET /toggle
// -----------------------------------------------------------------------------
void handleToggle() {
// 取得請求來源端的真實 IP 位址
String clientIP = server.client().remoteIP().toString();
// 讀取當前 GPIO 2 的實際輸出狀態並進行反轉 (HIGH 變 LOW,LOW 變 HIGH)
ledState = !digitalRead(LED_PIN);
// 將反轉後的狀態寫入 GPIO 2 腳位
digitalWrite(LED_PIN, ledState ? HIGH : LOW);
// 讀取實體腳位狀態確保數據精確性
int actualPinState = digitalRead(LED_PIN);
// 將數字狀態轉為可讀性高的文字標籤 ("ON" 或 "OFF")
String statusStr = (actualPinState == HIGH) ? "ON" : "OFF";
// 將此次操作的連線 IP 與結果寫入歷史陣列中
addHistoryEntry(clientIP, statusStr);
// 組裝包含 GPIO2 狀態與最新歷史陣列的 JSON 格式回應
String jsonResponse = "{\"GPIO2\":\"" + statusStr + "\",\"history\":" + getHistoryJsonArray() + "}";
// 在序列埠印出帶有 Client IP 的詳細動作追蹤紀錄
Serial.print("[動作追蹤] 來自 IP: ");
Serial.print(clientIP);
Serial.print(" 觸發切換 -> GPIO 2 LED 狀態為: ");
Serial.print(statusStr);
Serial.print(" | 回傳 JSON: ");
Serial.println(jsonResponse);
// 回傳 HTTP 200 狀態碼與 JSON 數據格式
server.send(200, "application/json", jsonResponse);
}
// -----------------------------------------------------------------------------
// 處理查詢當前 LED 狀態請求 HTTP GET /status
// -----------------------------------------------------------------------------
void handleStatus() {
// 取得請求來源端的真實 IP 位址
String clientIP = server.client().remoteIP().toString();
// 讀取當前 GPIO 2 的實體電位狀態
int actualPinState = digitalRead(LED_PIN);
// 判斷狀態字串
String statusStr = (actualPinState == HIGH) ? "ON" : "OFF";
// 組裝 JSON 狀態與歷史陣列回應格式
String jsonResponse = "{\"GPIO2\":\"" + statusStr + "\",\"history\":" + getHistoryJsonArray() + "}";
// 在序列埠記錄帶有 Client IP 的查詢動作
Serial.print("[查詢追蹤] 來自 IP: ");
Serial.print(clientIP);
Serial.print(" 讀取狀態請求 -> GPIO 2 目前狀態: ");
Serial.println(statusStr);
// 回傳 HTTP 200 與 JSON 數據
server.send(200, "application/json", jsonResponse);
}
// -----------------------------------------------------------------------------
// 處理 HTTP 404 找不到頁面
// -----------------------------------------------------------------------------
void handleNotFound() {
// 取得請求來源端的真實 IP 位址
String clientIP = server.client().remoteIP().toString();
// 記錄 404 錯誤路徑與 IP
Serial.print("[HTTP 警告] 來自 IP: ");
Serial.print(clientIP);
Serial.print(" 收到未知路徑請求: ");
Serial.println(server.uri());
// 回傳 HTTP 404 錯誤頁面
server.send(404, "text/plain", "404: Not Found");
}
// -----------------------------------------------------------------------------
// 前端網頁 HTML、CSS 與 原生 JavaScript (無外部依賴) 網頁字串產生器
// -----------------------------------------------------------------------------
String getHTMLPage() {
// 使用 C++ R"rawliteral(...)" 原始字串巨集,避免引號跳脫字元
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 物聯網控制中心</title>
<style>
/* 全域樣式設定 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Arial, sans-serif;
}
body {
background-color: #121212;
color: #E0E0E0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
/* 主卡片容器 */
.container {
background-color: #1E1E1E;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.8);
width: 100%;
max-width: 440px;
padding: 28px;
border: 1px solid #333333;
}
h1 {
font-size: 1.5rem;
text-align: center;
color: #FFFFFF;
margin-bottom: 6px;
}
.subtitle {
font-size: 0.85rem;
text-align: center;
color: #888888;
margin-bottom: 24px;
}
/* 開關控制區域 */
.control-section {
background-color: #262626;
border-radius: 12px;
padding: 24px;
text-align: center;
margin-bottom: 24px;
border: 1px solid #3A3A3A;
}
/* 作用腳位標籤 */
.pin-label {
font-size: 1.25rem;
font-weight: bold;
color: #38BDF8;
margin-bottom: 20px;
letter-spacing: 1px;
}
/* 滑動開關容器 (包含左右 OFF/ON 標籤) */
.switch-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 24px;
}
/* 開關兩端標籤文字 */
.switch-side-label {
font-size: 1.1rem;
font-weight: bold;
width: 45px;
transition: color 0.3s ease;
}
.label-off {
text-align: right;
color: #EF4444; /* 紅色 */
}
.label-on {
text-align: left;
color: #22C55E; /* 綠色 */
}
/* 原生 Checkbox 轉滑動 Toggle 開關樣式 */
.switch {
position: relative;
display: inline-block;
width: 74px;
height: 38px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* 滑塊軌道 */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #EF4444; /* 預設 OFF 為紅色軌道 */
transition: .4s;
border-radius: 38px;
box-shadow: inset 0 2px 6px rgba(0,0,0,0.5);
}
/* 滑塊圓球 */
.slider:before {
position: absolute;
content: "";
height: 30px;
width: 30px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0,0,0,0.4);
}
/* 切換至 ON 狀態的軌道顏色變更與圓球移動 */
input:checked + .slider {
background-color: #22C55E; /* ON 變為綠色軌道 */
}
input:checked + .slider:before {
transform: translateX(36px);
}
/* 開關停用時透明度變化 */
input:disabled + .slider {
opacity: 0.5;
cursor: not-allowed;
}
/* 第一個回應卡片:目前腳位狀態 (無標題) */
.response-card {
background-color: #121212;
border-radius: 8px;
padding: 16px;
text-align: left;
border-left: 4px solid #38BDF8;
margin-bottom: 16px;
}
.response-content {
font-size: 1.2rem;
font-weight: bold;
color: #FFFFFF;
font-family: 'Courier New', Courier, monospace;
white-space: pre-line; /* 保持斷行文字格式 */
}
.status-highlight-on {
color: #22C55E;
}
.status-highlight-off {
color: #EF4444;
}
/* 第二個回應卡片:最近 5 次連線 IP 與操作紀錄 */
.history-card {
background-color: #121212;
border-radius: 8px;
padding: 16px;
text-align: left;
border-left: 4px solid #FACC15; /* 黃色邊框以示區隔 */
}
.history-title {
font-size: 0.95rem;
color: #AAAAAA;
margin-bottom: 8px;
font-weight: bold;
}
.history-content {
font-size: 1.05rem;
font-weight: bold;
color: #E2E8F0;
font-family: 'Courier New', Courier, monospace;
white-space: pre-line;
line-height: 1.5;
}
/* 詳細操作指導卡片 */
.guide-section {
background-color: #262626;
border-radius: 12px;
padding: 18px;
border: 1px solid #3A3A3A;
}
.guide-title {
font-size: 0.95rem;
font-weight: bold;
color: #FFFFFF;
margin-bottom: 10px;
}
.guide-list {
padding-left: 18px;
font-size: 0.85rem;
line-height: 1.6;
color: #B0B0B0;
}
.guide-list li {
margin-bottom: 4px;
}
.hl {
color: #FACC15;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>ESP32 物聯網控制台</h1>
<div class="subtitle">NodeMCU-32S 開發板熱點控制頁面</div>
<!-- 裝置控制卡片區域 -->
<div class="control-section">
<!-- 切換開關標籤顯示作用腳位 -->
<div class="pin-label">作用腳位:GPIO2</div>
<!-- 切換開關:左右標示 OFF/ON -->
<div class="switch-wrapper">
<span class="switch-side-label label-off">OFF</span>
<label class="switch">
<input type="checkbox" id="ledToggle">
<span class="slider"></span>
</label>
<span class="switch-side-label label-on">ON</span>
</div>
<!-- 回應區塊 1:顯示目前狀態 (依需求已刪除標題文字) -->
<div class="response-card">
<div id="responseText" class="response-content">目前狀態:
GPIO2:讀取中...</div>
</div>
<!-- 回應區塊 2:最近 5 次發出 ON OFF 的連線 IP 紀錄 -->
<div class="history-card">
<div class="history-title">最近 5 次操作紀錄:</div>
<div id="historyText" class="history-content">無紀錄</div>
</div>
</div>
<!-- 詳細操作指導說明區塊 -->
<div class="guide-section">
<div class="guide-title">💡 操作指導說明</div>
<ol class="guide-list">
<li><strong>連線熱點:</strong>請確保已連接 Wi-Fi <span class="hl">ymiot1151</span> (密碼:<span class="hl">12345678</span>)。</li>
<li><strong>開關控制:</strong>撥動 <span class="hl">GPIO2</span> 單一切換開關即可切換內建 LED 燈。</li>
<li><strong>連線 IP 追蹤:</strong>下方卡片會即時記錄最近 5 次控制發出者的 <span class="hl">IP:ON/OFF</span> 訊息。</li>
<li><strong>多裝置同步:</strong>支援多台裝置同時連入,並能在序列埠輸出完整請求 IP 日誌。</li>
</ol>
</div>
</div>
<script>
// 採用原生 JavaScript (不依賴外部 jQuery),確保 AP 離線環境 100% 正常運作
document.addEventListener('DOMContentLoaded', function() {
var toggle = document.getElementById('ledToggle');
var responseText = document.getElementById('responseText');
var historyText = document.getElementById('historyText');
// 更新 UI 顯示 (包含目前狀態與 5 次連線 IP 歷史紀錄)
function updateUI(data) {
if (!data) return;
// 1. 更新目前狀態回應框
if (data.GPIO2 === "ON") {
toggle.checked = true;
responseText.innerHTML = '目前狀態:\nGPIO2:<span class="status-highlight-on">ON</span>';
} else if (data.GPIO2 === "OFF") {
toggle.checked = false;
responseText.innerHTML = '目前狀態:\nGPIO2:<span class="status-highlight-off">OFF</span>';
}
// 2. 更新最近 5 次連線 IP 紀錄回應框
if (data.history && Array.isArray(data.history) && data.history.length > 0) {
historyText.innerText = data.history.join('\n');
} else {
historyText.innerText = '尚無操作紀錄';
}
}
// 向 ESP32 發送狀態查詢 (GET /status)
function fetchStatus() {
fetch('/status')
.then(function(response) {
return response.json();
})
.then(function(data) {
updateUI(data);
})
.catch(function(error) {
console.log('連線狀態讀取失敗:', error);
});
}
// 監聽開關切換動作 (觸發 GET /toggle)
toggle.addEventListener('change', function() {
// 切換期間暫時鎖定開關,防止連點
toggle.disabled = true;
fetch('/toggle')
.then(function(response) {
return response.json();
})
.then(function(data) {
updateUI(data);
})
.catch(function(error) {
alert('連線失敗,請檢查 Wi-Fi 是否仍連線至 ymiot1151 熱點!');
// 失敗時同步還原 toggle 狀態
toggle.checked = !toggle.checked;
})
.finally(function() {
// 動作完成後解除鎖定
toggle.disabled = false;
});
});
// 頁面初始化載入時讀取一次腳位狀態與歷史紀錄
fetchStatus();
});
</script>
</body>
</html>
)rawliteral";
// 回傳完整網頁 HTML 字串
return html;
}
提示詞
程式運作正常,我要進一步優化,需求
#多使用者裝置同步
多使用者連上網頁後,彼此動作結果未即時同步,分析原因,提供解決方案
#網頁呈現不更動
開始優化修改前,先提供你的規畫給我
Code
/*
* 專案名稱:ESP32 AP 模式 Web Server 控制內建 LED 燈 (多使用者輪詢同步版)
* 開發板:NodeMCU-32S (ESP32)
* 開發環境:Arduino IDE 1.8.19 / ESP32 Core 3.1.1
* 需求優化說明:
* 1. 前端 JavaScript 引入 1 秒自動輪詢機制 (Periodic Polling),實時同步多使用者裝置間的 LED 腳位狀態與 IP 歷史紀錄。
* 2. 網頁 UI 樣式與排版完全維持原樣不更動。
* 3. 序列埠 Log 優化:過濾頻繁的背景 /status 輪詢訊息,僅在狀態變更或發生 /toggle 切換時輸出,保持 Log 清晰。
*/
// 引用 ESP32 WiFi 函式庫,用於建立 AP 模式與網路管理
#include <WiFi.h>
// 引用 ESP32 WebServer 函式庫,用於建立 HTTP Web 伺服器
#include <WebServer.h>
// 定義 Wi-Fi AP 模式的 SSID (熱點名稱)
const char* ap_ssid = "ymiot1151";
// 定義 Wi-Fi AP 模式的密碼 (至少需要 8 個字元)
const char* ap_password = "12345678";
// 定義 NodeMCU-32S 開發板內建 LED 腳位 (GPIO 2)
const int LED_PIN = 2;
// 建立運行於 80 通訊埠的 WebServer 物件
WebServer server(80);
// 記錄當前內建 LED 的開關狀態 (true 表示亮燈 ON,false 表示熄滅 OFF)
bool ledState = false;
// 記錄上一次查詢時的腳位狀態,用於判定狀態是否有變更
bool lastLedState = false;
// 宣告全域時間變數,用於非阻塞式的網路連線狀態檢查
unsigned long lastCheckTime = 0;
// 定義連線檢查的時間間隔為 5000 毫秒 (5 秒)
const unsigned long checkInterval = 5000;
// 宣告全域陣列,用於儲存最近 5 次操作的歷史紀錄字串 (格式:"IP:ON" 或 "IP:OFF")
String ipHistory[5] = {"", "", "", "", ""};
// 記錄目前歷史紀錄的有效筆數 (最多為 5 筆)
int historyCount = 0;
// 函數宣告:傳回單頁 HTML/CSS/JavaScript 網頁內容
String getHTMLPage();
// 函數宣告:處理 HTTP GET / 請求 (回應主網頁)
void handleRoot();
// 函數宣告:處理 HTTP GET /toggle 請求 (切換 LED 狀態並回傳 JSON)
void handleToggle();
// 函數宣告:處理 HTTP GET /status 請求 (讀取當前狀態與歷史紀錄並回傳 JSON)
void handleStatus();
// 函數宣告:處理找不到路徑的 HTTP 404 請求
void handleNotFound();
// 函數宣告:檢查並維護 AP 模式網路連線
void checkWiFiAPStatus();
// 函數宣告:新增一筆連線 IP 與操作紀錄至長度為 5 的歷史陣列
void addHistoryEntry(String clientIP, String statusStr);
// 函數宣告:取得歷史紀錄陣列並轉換為 JSON 格式字串
String getHistoryJsonArray();
// -----------------------------------------------------------------------------
// Arduino 初始化函數 setup()
// -----------------------------------------------------------------------------
void setup() {
// 初始化序列埠傳輸,傳輸速率設定為 115200 bps
Serial.begin(115200);
// 等待序列埠準備完成 (確保啟動訊息能完整印出)
delay(1000);
// 印出系統啟動分隔線
Serial.println("\n==========================================");
// 印出系統初始化起始訊息
Serial.println("[系統訊息] ESP32 Web Server 多裝置同步控制系統啟動中...");
// 將 LED 腳位設定為輸出模式 (OUTPUT)
pinMode(LED_PIN, OUTPUT);
// 初始化 LED 腳位輸出為低電位 (關閉 LED)
digitalWrite(LED_PIN, LOW);
// 在序列埠顯示 LED 腳位初始化完成狀態
Serial.println("[硬體設定] GPIO 2 (內建 LED) 已設定為輸出模式,預設狀態:OFF");
// 設定 Wi-Fi 模式為存取點模式 (AP Mode)
WiFi.mode(WIFI_AP);
// 啟動 Wi-Fi AP 熱點,並傳入 SSID 與密碼
bool apStarted = WiFi.softAP(ap_ssid, ap_password);
// 判斷 AP 熱點是否建立成功
if (apStarted) {
// 序列埠印出 AP 建立成功訊息
Serial.println("[網路設定] Wi-Fi AP 模式啟動成功!");
// 印出 AP 的 SSID 名稱
Serial.print("[網路設定] 熱點名稱 (SSID): ");
Serial.println(ap_ssid);
// 印出 AP 的 IP 位址 (預設通常為 192.168.4.1)
Serial.print("[網路設定] ESP32 IP 位址: ");
Serial.println(WiFi.softAPIP());
} else {
// 若建立失敗,印出錯誤訊息
Serial.println("[網路錯誤] Wi-Fi AP 模式啟動失敗,請檢查設定!");
}
// 設定當使用者存取網站根目錄 "/" 時所執行的處理函數
server.on("/", handleRoot);
// 設定當使用者發送 "/toggle" 請求時所執行的切換控制處理函數
server.on("/toggle", handleToggle);
// 設定當使用者發送 "/status" 請求時所執行的狀態查詢處理函數
server.on("/status", handleStatus);
// 設定當存取未定義路由時的回應函數 (HTTP 404)
server.onNotFound(handleNotFound);
// 啟動 Web 伺服器
server.begin();
// 在序列埠顯示 HTTP 伺服器啟動完成訊息
Serial.println("[服務設定] HTTP Web 伺服器已成功啟動 (Port 80)");
Serial.println("==========================================\n");
}
// -----------------------------------------------------------------------------
// Arduino 主迴圈 loop()
// -----------------------------------------------------------------------------
void loop() {
// 持續處理來自用戶端的 HTTP 請求
server.handleClient();
// 取得目前晶片運作時間 (毫秒)
unsigned long currentMillis = millis();
// 檢查是否達到設定的 5 秒檢查時間間隔
if (currentMillis - lastCheckTime >= checkInterval) {
// 更新最後檢查時間
lastCheckTime = currentMillis;
// 執行 AP 網路狀態檢查與維護函數
checkWiFiAPStatus();
}
}
// -----------------------------------------------------------------------------
// 新增一筆操作紀錄至歷史紀錄陣列 (保持最新 5 筆)
// -----------------------------------------------------------------------------
void addHistoryEntry(String clientIP, String statusStr) {
// 將舊紀錄往前推移 (第 1 筆變成最舊被覆蓋,最新筆放在最後)
for (int i = 0; i < 4; i++) {
ipHistory[i] = ipHistory[i + 1];
}
// 將最新一筆 "IP:ON" 或 "IP:OFF" 填入第 5 個位置 (索引 4)
ipHistory[4] = clientIP + ":" + statusStr;
// 更新有效紀錄筆數,最高上限為 5
if (historyCount < 5) {
historyCount++;
}
}
// -----------------------------------------------------------------------------
// 將歷史陣列轉為 JSON Array 字串,供前端更新顯示
// -----------------------------------------------------------------------------
String getHistoryJsonArray() {
String jsonArr = "[";
int added = 0;
// 由最新 (索引 4) 往回尋找有效紀錄
for (int i = 4; i >= 0; i--) {
if (ipHistory[i] != "") {
if (added > 0) jsonArr += ",";
jsonArr += "\"" + ipHistory[i] + "\"";
added++;
}
}
jsonArr += "]";
return jsonArr;
}
// -----------------------------------------------------------------------------
// 檢查並自動重新建立 AP 模式連線
// -----------------------------------------------------------------------------
void checkWiFiAPStatus() {
// 取得目前連線至 ESP32 AP 的用戶端數量
int stationCount = WiFi.softAPgetStationNum();
// 在序列埠輸出連線維護追蹤訊息
Serial.print("[網路狀態診斷] AP 運作中 | 當前連線裝置數: ");
Serial.println(stationCount);
// 檢查 SoftAP 的 IP 位址是否有效,若為 IPAddress(0,0,0,0) 表示 AP 異常跌線
if (WiFi.softAPIP() == IPAddress(0, 0, 0, 0)) {
// 輸出網路斷線警告訊息
Serial.println("[網路警告] 偵測到 AP 服務異常跌線,嘗試重新啟動 AP 連線...");
// 斷開並重置 Wi-Fi 模式
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_AP);
// 重新啟動 AP 模式
if (WiFi.softAP(ap_ssid, ap_password)) {
// 輸出重連成功訊息與新 IP
Serial.println("[網路恢復] AP 連線重新建立成功!");
Serial.print("[網路恢復] 目前 IP 位址: ");
Serial.println(WiFi.softAPIP());
} else {
// 輸出重連失敗訊息
Serial.println("[網路錯誤] AP 重新建立失敗,將於下次檢測時重試。");
}
}
}
// -----------------------------------------------------------------------------
// 處理根目錄 HTTP GET / 請求 (輸出網頁 HTML)
// -----------------------------------------------------------------------------
void handleRoot() {
// 取得請求來源端的真實 IP 位址
String clientIP = server.client().remoteIP().toString();
// 序列埠印出包含客戶端 IP 的連線日誌
Serial.print("[HTTP 請求] 來自 IP: ");
Serial.print(clientIP);
Serial.println(" 載入主頁面 (/)");
// 發送 HTTP 200 回應,內容型態為 text/html,並附帶網頁完整原始碼
server.send(200, "text/html", getHTMLPage());
}
// -----------------------------------------------------------------------------
// 處理切換 LED 狀態請求 HTTP GET /toggle
// -----------------------------------------------------------------------------
void handleToggle() {
// 取得請求來源端的真實 IP 位址
String clientIP = server.client().remoteIP().toString();
// 讀取當前 GPIO 2 的實際輸出狀態並進行反轉 (HIGH 變 LOW,LOW 變 HIGH)
ledState = !digitalRead(LED_PIN);
// 將反轉後的狀態寫入 GPIO 2 腳位
digitalWrite(LED_PIN, ledState ? HIGH : LOW);
// 讀取實體腳位狀態確保數據精確性
int actualPinState = digitalRead(LED_PIN);
// 將數字狀態轉為可讀性高的文字標籤 ("ON" 或 "OFF")
String statusStr = (actualPinState == HIGH) ? "ON" : "OFF";
// 將此次操作的連線 IP 與結果寫入歷史陣列中
addHistoryEntry(clientIP, statusStr);
// 組裝包含 GPIO2 狀態與最新歷史陣列的 JSON 格式回應
String jsonResponse = "{\"GPIO2\":\"" + statusStr + "\",\"history\":" + getHistoryJsonArray() + "}";
// 在序列埠印出帶有 Client IP 的詳細動作追蹤紀錄
Serial.print("[動作追蹤] 來自 IP: ");
Serial.print(clientIP);
Serial.print(" 觸發切換 -> GPIO 2 LED 狀態變更為: ");
Serial.print(statusStr);
Serial.print(" | 回傳 JSON: ");
Serial.println(jsonResponse);
// 回傳 HTTP 200 狀態碼與 JSON 數據格式
server.send(200, "application/json", jsonResponse);
}
// -----------------------------------------------------------------------------
// 處理查詢當前 LED 狀態請求 HTTP GET /status (自動輪詢 API)
// -----------------------------------------------------------------------------
void handleStatus() {
// 讀取當前 GPIO 2 的實體電位狀態
int actualPinState = digitalRead(LED_PIN);
bool currentLedBool = (actualPinState == HIGH);
// 判斷狀態字串
String statusStr = currentLedBool ? "ON" : "OFF";
// 組裝 JSON 狀態與歷史陣列回應格式
String jsonResponse = "{\"GPIO2\":\"" + statusStr + "\",\"history\":" + getHistoryJsonArray() + "}";
// 僅在腳位狀態與上次記錄不同時輸出序列埠 Log,避免背景輪詢造成 Log 刷屏
if (currentLedBool != lastLedState) {
String clientIP = server.client().remoteIP().toString();
Serial.print("[狀態同步] 偵測到腳位狀態異動 (來自 IP: ");
Serial.print(clientIP);
Serial.print(") -> 最新狀態: ");
Serial.println(statusStr);
lastLedState = currentLedBool;
}
// 回傳 HTTP 200 與 JSON 數據
server.send(200, "application/json", jsonResponse);
}
// -----------------------------------------------------------------------------
// 處理 HTTP 404 找不到頁面
// -----------------------------------------------------------------------------
void handleNotFound() {
// 取得請求來源端的真實 IP 位址
String clientIP = server.client().remoteIP().toString();
// 記錄 404 錯誤路徑與 IP
Serial.print("[HTTP 警告] 來自 IP: ");
Serial.print(clientIP);
Serial.print(" 收到未知路徑請求: ");
Serial.println(server.uri());
// 回傳 HTTP 404 錯誤頁面
server.send(404, "text/plain", "404: Not Found");
}
// -----------------------------------------------------------------------------
// 前端網頁 HTML、CSS 與 原生 JavaScript (含自動輪詢同步) 網頁字串產生器
// -----------------------------------------------------------------------------
String getHTMLPage() {
// 使用 C++ R"rawliteral(...)" 原始字串巨集,避免引號跳脫字元
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 物聯網控制中心</title>
<style>
/* 全域樣式設定 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Arial, sans-serif;
}
body {
background-color: #121212;
color: #E0E0E0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
/* 主卡片容器 */
.container {
background-color: #1E1E1E;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.8);
width: 100%;
max-width: 440px;
padding: 28px;
border: 1px solid #333333;
}
h1 {
font-size: 1.5rem;
text-align: center;
color: #FFFFFF;
margin-bottom: 6px;
}
.subtitle {
font-size: 0.85rem;
text-align: center;
color: #888888;
margin-bottom: 24px;
}
/* 開關控制區域 */
.control-section {
background-color: #262626;
border-radius: 12px;
padding: 24px;
text-align: center;
margin-bottom: 24px;
border: 1px solid #3A3A3A;
}
/* 作用腳位標籤 */
.pin-label {
font-size: 1.25rem;
font-weight: bold;
color: #38BDF8;
margin-bottom: 20px;
letter-spacing: 1px;
}
/* 滑動開關容器 (包含左右 OFF/ON 標籤) */
.switch-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 24px;
}
/* 開關兩端標籤文字 */
.switch-side-label {
font-size: 1.1rem;
font-weight: bold;
width: 45px;
transition: color 0.3s ease;
}
.label-off {
text-align: right;
color: #EF4444; /* 紅色 */
}
.label-on {
text-align: left;
color: #22C55E; /* 綠色 */
}
/* 原生 Checkbox 轉滑動 Toggle 開關樣式 */
.switch {
position: relative;
display: inline-block;
width: 74px;
height: 38px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* 滑塊軌道 */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #EF4444; /* 預設 OFF 為紅色軌道 */
transition: .4s;
border-radius: 38px;
box-shadow: inset 0 2px 6px rgba(0,0,0,0.5);
}
/* 滑塊圓球 */
.slider:before {
position: absolute;
content: "";
height: 30px;
width: 30px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0,0,0,0.4);
}
/* 切換至 ON 狀態的軌道顏色變更與圓球移動 */
input:checked + .slider {
background-color: #22C55E; /* ON 變為綠色軌道 */
}
input:checked + .slider:before {
transform: translateX(36px);
}
/* 開關停用時透明度變化 */
input:disabled + .slider {
opacity: 0.5;
cursor: not-allowed;
}
/* 第一個回應卡片:目前腳位狀態 (無標題) */
.response-card {
background-color: #121212;
border-radius: 8px;
padding: 16px;
text-align: left;
border-left: 4px solid #38BDF8;
margin-bottom: 16px;
}
.response-content {
font-size: 1.2rem;
font-weight: bold;
color: #FFFFFF;
font-family: 'Courier New', Courier, monospace;
white-space: pre-line; /* 保持斷行文字格式 */
}
.status-highlight-on {
color: #22C55E;
}
.status-highlight-off {
color: #EF4444;
}
/* 第二個回應卡片:最近 5 次連線 IP 與操作紀錄 */
.history-card {
background-color: #121212;
border-radius: 8px;
padding: 16px;
text-align: left;
border-left: 4px solid #FACC15; /* 黃色邊框以示區隔 */
}
.history-title {
font-size: 0.95rem;
color: #AAAAAA;
margin-bottom: 8px;
font-weight: bold;
}
.history-content {
font-size: 1.05rem;
font-weight: bold;
color: #E2E8F0;
font-family: 'Courier New', Courier, monospace;
white-space: pre-line;
line-height: 1.5;
}
/* 詳細操作指導卡片 */
.guide-section {
background-color: #262626;
border-radius: 12px;
padding: 18px;
border: 1px solid #3A3A3A;
}
.guide-title {
font-size: 0.95rem;
font-weight: bold;
color: #FFFFFF;
margin-bottom: 10px;
}
.guide-list {
padding-left: 18px;
font-size: 0.85rem;
line-height: 1.6;
color: #B0B0B0;
}
.guide-list li {
margin-bottom: 4px;
}
.hl {
color: #FACC15;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>ESP32 物聯網控制台</h1>
<div class="subtitle">NodeMCU-32S 開發板熱點控制頁面</div>
<!-- 裝置控制卡片區域 -->
<div class="control-section">
<!-- 切換開關標籤顯示作用腳位 -->
<div class="pin-label">作用腳位:GPIO2</div>
<!-- 切換開關:左右標示 OFF/ON -->
<div class="switch-wrapper">
<span class="switch-side-label label-off">OFF</span>
<label class="switch">
<input type="checkbox" id="ledToggle">
<span class="slider"></span>
</label>
<span class="switch-side-label label-on">ON</span>
</div>
<!-- 回應區塊 1:顯示目前狀態 -->
<div class="response-card">
<div id="responseText" class="response-content">目前狀態:
GPIO2:讀取中...</div>
</div>
<!-- 回應區塊 2:最近 5 次發出 ON OFF 的連線 IP 紀錄 -->
<div class="history-card">
<div class="history-title">最近 5 次操作紀錄:</div>
<div id="historyText" class="history-content">無紀錄</div>
</div>
</div>
<!-- 詳細操作指導說明區塊 -->
<div class="guide-section">
<div class="guide-title">💡 操作指導說明</div>
<ol class="guide-list">
<li><strong>連線熱點:</strong>請確保已連接 Wi-Fi <span class="hl">ymiot1151</span> (密碼:<span class="hl">12345678</span>)。</li>
<li><strong>開關控制:</strong>撥動 <span class="hl">GPIO2</span> 單一切換開關即可切換內建 LED 燈。</li>
<li><strong>多使用者同步:</strong>網頁已啟用 <span class="hl">1 秒實時自動輪詢</span>,多台手機/電腦操作結果會自動即時同步。</li>
<li><strong>連線 IP 追蹤:</strong>下方卡片會即時記錄最近 5 次控制發出者的 <span class="hl">IP:ON/OFF</span> 訊息。</li>
</ol>
</div>
</div>
<script>
// 採用原生 JavaScript,引入 1 秒自動輪詢實現多裝置無感狀態同步
document.addEventListener('DOMContentLoaded', function() {
var toggle = document.getElementById('ledToggle');
var responseText = document.getElementById('responseText');
var historyText = document.getElementById('historyText');
// 標記使用者目前是否正在與開關互動,避免輪詢與點擊動作發生衝突
var isUserInteracting = false;
// 更新 UI 顯示 (包含目前狀態與 5 次連線 IP 歷史紀錄)
function updateUI(data) {
if (!data) return;
// 若使用者正在撥動開關,暫停背景輪詢對 toggle UI 的覆蓋,防干擾
if (!isUserInteracting) {
if (data.GPIO2 === "ON") {
toggle.checked = true;
responseText.innerHTML = '目前狀態:\nGPIO2:<span class="status-highlight-on">ON</span>';
} else if (data.GPIO2 === "OFF") {
toggle.checked = false;
responseText.innerHTML = '目前狀態:\nGPIO2:<span class="status-highlight-off">OFF</span>';
}
}
// 更新最近 5 次連線 IP 紀錄回應框
if (data.history && Array.isArray(data.history) && data.history.length > 0) {
historyText.innerText = data.history.join('\n');
} else {
historyText.innerText = '尚無操作紀錄';
}
}
// 向 ESP32 發送狀態查詢 (GET /status)
function fetchStatus() {
fetch('/status')
.then(function(response) {
return response.json();
})
.then(function(data) {
updateUI(data);
})
.catch(function(error) {
console.log('背景連線狀態讀取失敗:', error);
});
}
// 監聽開關切換動作 (觸發 GET /toggle)
toggle.addEventListener('change', function() {
// 標記使用者開始互動,並鎖定開關
isUserInteracting = true;
toggle.disabled = true;
fetch('/toggle')
.then(function(response) {
return response.json();
})
.then(function(data) {
// 解除互動鎖定並強制更新最新 UI
isUserInteracting = false;
updateUI(data);
})
.catch(function(error) {
alert('連線失敗,請檢查 Wi-Fi 是否仍連線至 ymiot1151 熱點!');
// 失敗時同步還原 toggle 狀態
toggle.checked = !toggle.checked;
isUserInteracting = false;
})
.finally(function() {
// 動作完成後解除開關鎖定
toggle.disabled = false;
});
});
// 頁面初始化載入時立即執行一次狀態讀取
fetchStatus();
// 啟動 1 秒 (1000ms) 背景定時輪詢,實時同步其他裝置的切換動作
setInterval(fetchStatus, 1000);
});
</script>
</body>
</html>
)rawliteral";
// 回傳完整網頁 HTML 字串
return html;
}
筆電連入 192.168.4.1
手機連入 192.168.4.1
提示詞
程式運作正常,我要進一步優化,需求
#多使用者裝置同步
評估更改使用 websocket 連線方式
#網頁呈現不更動
開始優化修改前,先提供你的規畫給我
Code
/*
* 專案名稱:ESP32 AP 模式 Web Server 控制內建 LED 燈 (AsyncWebSocket 即時多裝置同步版)
* 開發板:NodeMCU-32S (ESP32)
* 開發環境:Arduino IDE 1.8.19 / ESP32 Core 3.1.1
* 依賴函式庫:
* 1. ESPAsyncWebServer (https://github.com/me-no-dev/ESPAsyncWebServer)
* 2. AsyncTCP (https://github.com/me-no-dev/AsyncTCP)
* 3. ArduinoJson (建議 v6.x)
* 需求優化說明:
* 1. 改用 WebSocket (/ws) 長連線架構,當任一裝置切換狀態時,ESP32 立即廣播 (Broadcast) 給所有線上裝置,達到毫秒級即時同步。
* 2. 前端 JavaScript 加入自動重連機制 (Auto Reconnect),斷線時每 2 秒自動嘗試重新建立 WebSocket 連線。
* 3. 網頁 UI 樣式與排版完全維持原樣不更動。
*/
// 引用 ESP32 WiFi 函式庫,用於建立 AP 模式與網路管理
#include <WiFi.h>
// 引用 ESP32 異步 WebServer 與 WebSocket 函式庫
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>
// 引用 ArduinoJson 函式庫,用於輕鬆解析與打包 JSON 格式
#include <ArduinoJson.h>
// 定義 Wi-Fi AP 模式的 SSID (熱點名稱)
const char* ap_ssid = "ymiot1151";
// 定義 Wi-Fi AP 模式的密碼 (至少需要 8 個字元)
const char* ap_password = "12345678";
// 定義 NodeMCU-32S 開發板內建 LED 腳位 (GPIO 2)
const int LED_PIN = 2;
// 建立運行於 80 通訊埠的 AsyncWebServer 物件
AsyncWebServer server(80);
// 建立運行於 "/ws" 路徑的 AsyncWebSocket 物件
AsyncWebSocket ws("/ws");
// 記錄當前內建 LED 的開關狀態 (true 表示亮燈 ON,false 表示熄滅 OFF)
bool ledState = false;
// 宣告全域時間變數,用於非阻塞式的網路連線狀態檢查
unsigned long lastCheckTime = 0;
// 定義連線檢查的時間間隔為 5000 毫秒 (5 秒)
const unsigned long checkInterval = 5000;
// 宣告全域陣列,用於儲存最近 5 次操作的歷史紀錄字串 (格式:"IP:ON" 或 "IP:OFF")
String ipHistory[5] = {"", "", "", "", ""};
// 記錄目前歷史紀錄的有效筆數 (最多為 5 筆)
int historyCount = 0;
// 函數宣告:傳回單頁 HTML/CSS/JavaScript 網頁內容
String getHTMLPage();
// 函數宣告:檢查並維護 AP 模式網路連線
void checkWiFiAPStatus();
// 函數宣告:新增一筆連線 IP 與操作紀錄至長度為 5 的歷史陣列
void addHistoryEntry(String clientIP, String statusStr);
// 函數宣告:組裝當前狀態與歷史紀錄的 JSON 字串
String buildStatusJson();
// 函數宣告:向所有已連線的 WebSocket 用戶端廣播最新狀態 JSON
void notifyClients();
// 函數宣告:處理 WebSocket 事件 (連線、斷線、接收數據、錯誤)
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len);
// 函數宣告:處理來自 WebSocket 用戶端發送的數據訊息
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len, AsyncWebSocketClient *client);
// -----------------------------------------------------------------------------
// Arduino 初始化函數 setup()
// -----------------------------------------------------------------------------
void setup() {
// 初始化序列埠傳輸,傳輸速率設定為 115200 bps
Serial.begin(115200);
// 等待序列埠準備完成 (確保啟動訊息能完整印出)
delay(1000);
// 印出系統啟動分隔線
Serial.println("\n==========================================");
// 印出系統初始化起始訊息
Serial.println("[系統訊息] ESP32 WebSocket 即時多裝置同步系統啟動中...");
// 將 LED 腳位設定為輸出模式 (OUTPUT)
pinMode(LED_PIN, OUTPUT);
// 初始化 LED 腳位輸出為低電位 (關閉 LED)
digitalWrite(LED_PIN, LOW);
// 在序列埠顯示 LED 腳位初始化完成狀態
Serial.println("[硬體設定] GPIO 2 (內建 LED) 已設定為輸出模式,預設狀態:OFF");
// 設定 Wi-Fi 模式為存取點模式 (AP Mode)
WiFi.mode(WIFI_AP);
// 啟動 Wi-Fi AP 熱點,並傳入 SSID 與密碼
bool apStarted = WiFi.softAP(ap_ssid, ap_password);
// 判斷 AP 熱點是否建立成功
if (apStarted) {
// 序列埠印出 AP 建立成功訊息
Serial.println("[網路設定] Wi-Fi AP 模式啟動成功!");
// 印出 AP 的 SSID 名稱
Serial.print("[網路設定] 熱點名稱 (SSID): ");
Serial.println(ap_ssid);
// 印出 AP 的 IP 位址 (預設通常為 192.168.4.1)
Serial.print("[網路設定] ESP32 IP 位址: ");
Serial.println(WiFi.softAPIP());
} else {
// 若建立失敗,印出錯誤訊息
Serial.println("[網路錯誤] Wi-Fi AP 模式啟動失敗,請檢查設定!");
}
// 註冊 WebSocket 事件監聽函數
ws.onEvent(onEvent);
// 將 WebSocket 服務綁定掛載至 WebServer 伺服器
server.addHandler(&ws);
// 設定當使用者存取網站根目錄 "/" 時,回傳主 HTML 網頁
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
String clientIP = request->client()->remoteIP().toString();
Serial.print("[HTTP 請求] 來自 IP: ");
Serial.print(clientIP);
Serial.println(" 載入主頁面 (/)");
request->send(200, "text/html", getHTMLPage());
});
// 設定 404 找不到頁面的回應處理
server.onNotFound([](AsyncWebServerRequest *request){
request->send(404, "text/plain", "404: Not Found");
});
// 啟動 異步 Web 伺服器
server.begin();
// 在序列埠顯示 HTTP 與 WebSocket 伺服器啟動完成訊息
Serial.println("[服務設定] AsyncWebServer & WebSocket (/ws) 已成功啟動 (Port 80)");
Serial.println("==========================================\n");
}
// -----------------------------------------------------------------------------
// Arduino 主迴圈 loop()
// -----------------------------------------------------------------------------
void loop() {
// 清理無效或已斷開的 WebSocket 用戶端資源 (釋放記憶體)
ws.cleanupClients();
// 取得目前晶片運作時間 (毫秒)
unsigned long currentMillis = millis();
// 檢查是否達到設定的 5 秒檢查時間間隔
if (currentMillis - lastCheckTime >= checkInterval) {
// 更新最後檢查時間
lastCheckTime = currentMillis;
// 執行 AP 網路狀態檢查與維護函數
checkWiFiAPStatus();
}
}
// -----------------------------------------------------------------------------
// 新增一筆操作紀錄至歷史紀錄陣列 (保持最新 5 筆)
// -----------------------------------------------------------------------------
void addHistoryEntry(String clientIP, String statusStr) {
// 將舊紀錄往前推移 (第 1 筆變成最舊被覆蓋,最新筆放在最後)
for (int i = 0; i < 4; i++) {
ipHistory[i] = ipHistory[i + 1];
}
// 將最新一筆 "IP:ON" 或 "IP:OFF" 填入第 5 個位置 (索引 4)
ipHistory[4] = clientIP + ":" + statusStr;
// 更新有效紀錄筆數,最高上限為 5
if (historyCount < 5) {
historyCount++;
}
}
// -----------------------------------------------------------------------------
// 組裝 JSON 狀態資料 (包含 GPIO2 狀態與歷史紀錄陣列)
// -----------------------------------------------------------------------------
String buildStatusJson() {
// 建立動態 JSON 文件物件 (容量大小 512 bytes)
DynamicJsonDocument doc(512);
// 讀取當前 GPIO 2 的實體電位狀態
int actualPinState = digitalRead(LED_PIN);
String statusStr = (actualPinState == HIGH) ? "ON" : "OFF";
// 設定 GPIO2 開關文字欄位
doc["GPIO2"] = statusStr;
// 建立 JSON Array 欄位以填入歷史紀錄
JsonArray historyArr = doc.createNestedArray("history");
// 由最新 (索引 4) 往回尋找有效紀錄傳入 JSON
for (int i = 4; i >= 0; i--) {
if (ipHistory[i] != "") {
historyArr.add(ipHistory[i]);
}
}
// 將 JSON 文件物件序列化轉為字串
String output;
serializeJson(doc, output);
return output;
}
// -----------------------------------------------------------------------------
// 向所有連線中的 WebSocket Client 廣播最新 JSON 狀態數據
// -----------------------------------------------------------------------------
void notifyClients() {
String jsonString = buildStatusJson();
// 呼叫 WebSocket 的 textAll 方法,向線上所有裝置主動推送數據
ws.textAll(jsonString);
Serial.print("[WebSocket 廣播] 即時推送狀態至 ");
Serial.print(ws.count());
Serial.print(" 個線上裝置: ");
Serial.println(jsonString);
}
// -----------------------------------------------------------------------------
// 處理來自 WebSocket 用戶端的指令訊息
// -----------------------------------------------------------------------------
void handleWebSocketMessage(void *arg, uint8_t *data, size_t len, AsyncWebSocketClient *client) {
AwsFrameInfo *info = (AwsFrameInfo*)arg;
// 確保收到完整的文字幀 (Text Frame) 數據
if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
data[len] = 0; // 補上字串結束符號
String message = String((char*)data);
// 取得發送指令者的 Client IP 位址
String clientIP = client->remoteIP().toString();
// 判斷接收到的命令是否為切換指令 "toggle"
if (message == "toggle") {
// 讀取當前 GPIO 2 實際輸出狀態並進行反轉 (HIGH 變 LOW,LOW 變 HIGH)
ledState = !digitalRead(LED_PIN);
digitalWrite(LED_PIN, ledState ? HIGH : LOW);
// 讀取實體腳位最新狀態
int actualPinState = digitalRead(LED_PIN);
String statusStr = (actualPinState == HIGH) ? "ON" : "OFF";
// 記錄本次操作連線的 Client IP 與觸發狀態
addHistoryEntry(clientIP, statusStr);
Serial.print("[WebSocket 動作] 來自 IP: ");
Serial.print(clientIP);
Serial.print(" 發送切換指令 -> GPIO 2 狀態變更為: ");
Serial.println(statusStr);
// 狀態變更後,立即向所有連線的裝置廣播最新狀態!
notifyClients();
}
// 判斷接收到的命令是否為查詢指令 "getStatus"
else if (message == "getStatus") {
// 僅向該位連線用戶端單獨回應當前狀態 JSON
client->text(buildStatusJson());
}
}
}
// -----------------------------------------------------------------------------
// WebSocket 事件總監聽函數 (處理連線、斷線、訊息接收)
// -----------------------------------------------------------------------------
void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
switch (type) {
case WS_EVT_CONNECT:
// 有新裝置建立 WebSocket 連線時觸發
Serial.print("[WebSocket 連線] 新用戶端上線! Client ID: ");
Serial.print(client->id());
Serial.print(" | IP: ");
Serial.println(client->remoteIP().toString());
// 裝置連線成功後,立即發送當前最新狀態數據給該新裝置
client->text(buildStatusJson());
break;
case WS_EVT_DISCONNECT:
// 當用戶端離線時觸發
Serial.print("[WebSocket 斷線] 用戶端離線 Client ID: ");
Serial.println(client->id());
break;
case WS_EVT_DATA:
// 當接收到來自用戶端的數據時觸發
handleWebSocketMessage(arg, data, len, client);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
// -----------------------------------------------------------------------------
// 檢查並自動重新建立 AP 模式連線
// -----------------------------------------------------------------------------
void checkWiFiAPStatus() {
// 取得目前連線至 ESP32 AP 的用戶端數量
int stationCount = WiFi.softAPgetStationNum();
// 在序列埠輸出連線維護追蹤訊息
Serial.print("[網路狀態診斷] AP 運作中 | 當前 Wi-Fi 連線裝置數: ");
Serial.print(stationCount);
Serial.print(" | WebSocket 在線數: ");
Serial.println(ws.count());
// 檢查 SoftAP 的 IP 位址是否有效
if (WiFi.softAPIP() == IPAddress(0, 0, 0, 0)) {
Serial.println("[網路警告] 偵測到 AP 服務異常跌線,嘗試重新啟動 AP 連線...");
WiFi.softAPdisconnect(true);
WiFi.mode(WIFI_AP);
if (WiFi.softAP(ap_ssid, ap_password)) {
Serial.println("[網路恢復] AP 連線重新建立成功!");
Serial.print("[網路恢復] 目前 IP 位址: ");
Serial.println(WiFi.softAPIP());
} else {
Serial.println("[網路錯誤] AP 重新建立失敗,將於下次檢測時重試。");
}
}
}
// -----------------------------------------------------------------------------
// 前端網頁 HTML、CSS 與 原生 JavaScript (WebSocket 即時雙向通訊) 網頁字串
// -----------------------------------------------------------------------------
String getHTMLPage() {
String html = R"rawliteral(
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 物聯網控制中心</title>
<style>
/* 全域樣式設定 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Arial, sans-serif;
}
body {
background-color: #121212;
color: #E0E0E0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
/* 主卡片容器 */
.container {
background-color: #1E1E1E;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.8);
width: 100%;
max-width: 440px;
padding: 28px;
border: 1px solid #333333;
}
h1 {
font-size: 1.5rem;
text-align: center;
color: #FFFFFF;
margin-bottom: 6px;
}
.subtitle {
font-size: 0.85rem;
text-align: center;
color: #888888;
margin-bottom: 24px;
}
/* 開關控制區域 */
.control-section {
background-color: #262626;
border-radius: 12px;
padding: 24px;
text-align: center;
margin-bottom: 24px;
border: 1px solid #3A3A3A;
}
/* 作用腳位標籤 */
.pin-label {
font-size: 1.25rem;
font-weight: bold;
color: #38BDF8;
margin-bottom: 20px;
letter-spacing: 1px;
}
/* 滑動開關容器 (包含左右 OFF/ON 標籤) */
.switch-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin-bottom: 24px;
}
/* 開關兩端標籤文字 */
.switch-side-label {
font-size: 1.1rem;
font-weight: bold;
width: 45px;
transition: color 0.3s ease;
}
.label-off {
text-align: right;
color: #EF4444; /* 紅色 */
}
.label-on {
text-align: left;
color: #22C55E; /* 綠色 */
}
/* 原生 Checkbox 轉滑動 Toggle 開關樣式 */
.switch {
position: relative;
display: inline-block;
width: 74px;
height: 38px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* 滑塊軌道 */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #EF4444; /* 預設 OFF 為紅色軌道 */
transition: .4s;
border-radius: 38px;
box-shadow: inset 0 2px 6px rgba(0,0,0,0.5);
}
/* 滑塊圓球 */
.slider:before {
position: absolute;
content: "";
height: 30px;
width: 30px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0,0,0,0.4);
}
/* 切換至 ON 狀態的軌道顏色變更與圓球移動 */
input:checked + .slider {
background-color: #22C55E; /* ON 變為綠色軌道 */
}
input:checked + .slider:before {
transform: translateX(36px);
}
/* 開關停用時透明度變化 */
input:disabled + .slider {
opacity: 0.5;
cursor: not-allowed;
}
/* 第一個回應卡片:目前腳位狀態 (無標題) */
.response-card {
background-color: #121212;
border-radius: 8px;
padding: 16px;
text-align: left;
border-left: 4px solid #38BDF8;
margin-bottom: 16px;
}
.response-content {
font-size: 1.2rem;
font-weight: bold;
color: #FFFFFF;
font-family: 'Courier New', Courier, monospace;
white-space: pre-line; /* 保持斷行文字格式 */
}
.status-highlight-on {
color: #22C55E;
}
.status-highlight-off {
color: #EF4444;
}
/* 第二個回應卡片:最近 5 次連線 IP 與操作紀錄 */
.history-card {
background-color: #121212;
border-radius: 8px;
padding: 16px;
text-align: left;
border-left: 4px solid #FACC15; /* 黃色邊框以示區隔 */
}
.history-title {
font-size: 0.95rem;
color: #AAAAAA;
margin-bottom: 8px;
font-weight: bold;
}
.history-content {
font-size: 1.05rem;
font-weight: bold;
color: #E2E8F0;
font-family: 'Courier New', Courier, monospace;
white-space: pre-line;
line-height: 1.5;
}
/* 詳細操作指導卡片 */
.guide-section {
background-color: #262626;
border-radius: 12px;
padding: 18px;
border: 1px solid #3A3A3A;
}
.guide-title {
font-size: 0.95rem;
font-weight: bold;
color: #FFFFFF;
margin-bottom: 10px;
}
.guide-list {
padding-left: 18px;
font-size: 0.85rem;
line-height: 1.6;
color: #B0B0B0;
}
.guide-list li {
margin-bottom: 4px;
}
.hl {
color: #FACC15;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>ESP32 物聯網控制台</h1>
<div class="subtitle">NodeMCU-32S 開發板熱點控制頁面</div>
<!-- 裝置控制卡片區域 -->
<div class="control-section">
<!-- 切換開關標籤顯示作用腳位 -->
<div class="pin-label">作用腳位:GPIO2</div>
<!-- 切換開關:左右標示 OFF/ON -->
<div class="switch-wrapper">
<span class="switch-side-label label-off">OFF</span>
<label class="switch">
<input type="checkbox" id="ledToggle">
<span class="slider"></span>
</label>
<span class="switch-side-label label-on">ON</span>
</div>
<!-- 回應區塊 1:顯示目前狀態 -->
<div class="response-card">
<div id="responseText" class="response-content">目前狀態:
GPIO2:連線中...</div>
</div>
<!-- 回應區塊 2:最近 5 次發出 ON OFF 的連線 IP 紀錄 -->
<div class="history-card">
<div class="history-title">最近 5 次操作紀錄:</div>
<div id="historyText" class="history-content">無紀錄</div>
</div>
</div>
<!-- 詳細操作指導說明區塊 -->
<div class="guide-section">
<div class="guide-title">💡 操作指導說明</div>
<ol class="guide-list">
<li><strong>連線熱點:</strong>請確保已連接 Wi-Fi <span class="hl">ymiot1151</span> (密碼:<span class="hl">12345678</span>)。</li>
<li><strong>開關控制:</strong>撥動 <span class="hl">GPIO2</span> 單一切換開關即可切換內建 LED 燈。</li>
<li><strong>WebSocket 毫秒同步:</strong>已啟用 <span class="hl">WebSocket 即時推送</span>,多裝置同步零延遲。</li>
<li><strong>連線 IP 追蹤:</strong>下方卡片會即時記錄最近 5 次控制發出者的 <span class="hl">IP:ON/OFF</span> 訊息。</li>
</ol>
</div>
</div>
<script>
// 原生 JavaScript WebSocket 通訊機制 (含自動斷線重連機制)
document.addEventListener('DOMContentLoaded', function() {
var toggle = document.getElementById('ledToggle');
var responseText = document.getElementById('responseText');
var historyText = document.getElementById('historyText');
var websocket;
// 動態獲取當前主機的 IP 位址建立 WebSocket 連線 URL (例如: ws://192.168.4.1/ws)
var gateway = 'ws://' + window.location.host + '/ws';
// 初始化 WebSocket 連線
function initWebSocket() {
console.log('嘗試建立 WebSocket 連線至 ' + gateway);
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
websocket.onerror = onError;
}
// WebSocket 連線成功建立處理
function onOpen(event) {
console.log('WebSocket 連線成功建立!');
// 解除開關停用狀態
toggle.disabled = false;
}
// WebSocket 斷線處理 (觸發自動重連機制)
function onClose(event) {
console.log('WebSocket 連線中斷');
responseText.innerHTML = '目前狀態:\n<span class="status-highlight-off">WebSocket 連線中斷,嘗試重連...</span>';
toggle.disabled = true;
// 每隔 2 秒自動嘗試重新建立 WebSocket 連線
setTimeout(initWebSocket, 2000);
}
// 接收來自 ESP32 主動廣播的 WebSocket 數據
function onMessage(event) {
try {
// 解析 ESP32 推送的 JSON 數據
var data = JSON.parse(event.data);
updateUI(data);
} catch (e) {
console.error('JSON 解析失敗:', e);
}
}
// 發生錯誤時處理
function onError(event) {
console.error('WebSocket 發生錯誤:', event);
}
// 更新 UI 畫面顯示
function updateUI(data) {
if (!data) return;
// 即時同步開關選取狀態與文字標籤
if (data.GPIO2 === "ON") {
toggle.checked = true;
responseText.innerHTML = '目前狀態:\nGPIO2:<span class="status-highlight-on">ON</span>';
} else if (data.GPIO2 === "OFF") {
toggle.checked = false;
responseText.innerHTML = '目前狀態:\nGPIO2:<span class="status-highlight-off">OFF</span>';
}
// 即時更新最近 5 次 IP 操作歷史紀錄
if (data.history && Array.isArray(data.history) && data.history.length > 0) {
historyText.innerText = data.history.join('\n');
} else {
historyText.innerText = '尚無操作紀錄';
}
}
// 當使用者撥動開關時,透過 WebSocket 向 ESP32 發送 "toggle" 指令
toggle.addEventListener('change', function() {
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.send('toggle');
} else {
alert('WebSocket 連線已斷開,請確認 Wi-Fi 熱點連線!');
toggle.checked = !toggle.checked;
}
});
// 頁面載入完成後啟動 WebSocket 連線
initWebSocket();
});
</script>
</body>
</html>
)rawliteral";
return html;
}
草稿碼 > 匯入程式庫 > 管理程式庫
搜尋關鍵字:ArduinoJson
作者:Benoît Blanchon
建議版本:v6.x(例如 v6.21.x,若安裝 v7.x 版本語法可能需要小幅調整,建議優先選用 v6.x)。
作用:處理 ESP32 端的狀態與 5 筆 IP 歷史紀錄打包/解析。
作者:me-no-dev
作用:提供 ESP32 底層異步 TCP 網路傳輸通訊協定支援(WebSocket 的必要依賴庫)。
連線 https://github.com/me-no-dev/AsyncTCP,下載 AsyncTCP-master.zip
草稿碼 > 匯入程式庫 > 加入.ZIP程式庫
作者:me-no-dev (或 me-no-dev/ESPAsyncWebServer)
作用:提供異步 Web Server 與 WebSocket (/ws) 路由廣播支援。
連線 https://github.com/me-no-dev/ESPAsyncWebServer,下載 ESPAsyncWebServer-master.zip
草稿碼 > 匯入程式庫 > 加入.ZIP程式庫
筆電連接 WiFi
筆電連接 WiFi
筆電連入 192.168.4.1
手機連入 192.168.4.1
筆電 ON,手機即時同步 ON
手機 OFF,筆電即時同步 OFF
筆電 ON,手機即時同步 ON
手機 OFF,筆電即時同步 OFF