提示詞
依下列需求,寫一個 Arduino 程式。開始寫程式前,根據需求,提供你的規劃給我確認。
#知識專長
-嵌入式系統與物聯網(IoT)硬體開發專家
#嵌入式系統硬體
##開發板
-NodeMCU-32S 開發板
-Keyes ESP-32 專用 GPIO 擴充板
##外接元件
-1 個 5050 RGB LED 模組,共陰極
##接線
-RGB LED R 接 擴充板 GPIO25
-RGB LED G 接 擴充板 GPIO26
-RGB LED B 接 擴充板 GPIO27
-RGB LED GND 接 擴充板 GND
#程式需求
##開發環境
-Arduino IDE 1.8.19
-ESP32 核心版本 3.1.1
##建立 Station mode wifi 連網
-SSID: chuangHome
-PASSWORD: 0988087024
##連網成功後 會檢查網路連線,當網路連線失敗,重新自動建立連線
##連網成功後,建立 web server 服務,產生一個單頁響應式網頁
-整合 HTML, CSS, jQuery(使用非同步 Ajax 請求)等語法
-網頁內容提供3個單一切換按鈕,分別控制3個LED燈
-單一切換按鈕樣式:
--往左往右分別是ON OFF
--切換開關的標籤改為作用腳位(如:GPIO2)
--切換開關的左右兩端,以文字明確標示 ON OFF
-於按鈕動作結束後,讀取LED燈腳位的開關狀態,顯示在網頁上,讓使用者可以明確知道每個GPIO的狀態。
--回傳 JSON 格式數據(例如:{"GPIO25":ON, "GPIO26":OFF, "GPIO27":OFF})。
--網頁上以文字樣式顯示每個LED的回應訊息,訊息格式如下:GPIO2:ON
--每個LED的回應訊息位置:單一切換按鈕上方置中處
-網頁增加一個回應框,顯示最近五次發出 ON OFF 的連線 IP訊息,位置在所有切換開關下方,顯示格式如
IP:ON
IP:ON
...
-網頁設計力求簡潔美觀、配色對比強,顯示文字以繁體中文或英文適當呈現
-網頁內容中提供完整詳細操作指導
##多使用者裝置同步
-使用 websocket 連線方式
##程式運作將必要的互動追蹤訊息 完整輸出至序列埠
-速率 115200
-序列埠輸出訊息,需簡潔明確,方便開發者追蹤除錯
-HTTP請求部分,提供連線IP,ON OFF 請求,JSON 回應
##程式碼逐行以繁體中文註解
#程式安裝指導說明
##程式若使用第三方函式庫,須提供函式庫下載與安裝的詳細操作
##提供程式燒錄使用的詳細操作
Code
#include <WiFi.h> // 引用 ESP32 原生 WiFi 標頭檔
#include <WebServer.h> // 引用 ESP32 原生 WebServer 標頭檔
#include <WebSocketsServer.h> // 引用第三方 WebSocketsServer 標頭檔
#include <ArduinoJson.h> // 引用第三方 ArduinoJson 標頭檔
// ==================== 網路設定 ====================
const char* ssid = "chuangHome"; // WiFi SSID 帳號
const char* password = "0988087024"; // WiFi 密碼
// ==================== 硬體腳位設定 ====================
const int pinR = 25; // RGB LED 紅燈連接至 GPIO25
const int pinG = 26; // RGB LED 綠燈連接至 GPIO26
const int pinB = 27; // RGB LED 藍燈連接至 GPIO27
// ==================== 物件建立 ====================
WebServer server(80); // 建立 HTTP 伺服器,監聽 80 埠
WebSocketsServer webSocket = WebSocketsServer(81); // 建立 WebSocket 伺服器,監聽 81 埠
// ==================== 全域變數 ====================
bool stateR = false; // GPIO25 (R) 狀態紀錄 (false: OFF, true: ON)
bool stateG = false; // GPIO26 (G) 狀態紀錄 (false: OFF, true: ON)
bool stateB = false; // GPIO27 (B) 狀態紀錄 (false: OFF, true: ON)
String ipHistory[5]; // 儲存最近 5 次發出 ON/OFF 控制的紀錄訊息
int historyCount = 0; // 紀錄目前已儲存的歷史訊息數量
// ==================== 輔助函式:新增控制歷史紀錄 ====================
void addHistory(String ip, String action) {
String entry = ip + " : " + action; // 格式化歷史紀錄字串
// 陣列內容往後推移,騰出第 0 個位置
for (int i = 4; i > 0; i--) {
ipHistory[i] = ipHistory[i - 1];
}
ipHistory[0] = entry; // 最新紀錄放入第 0 位
if (historyCount < 5) historyCount++; // 更新總數(最多 5 筆)
}
// ==================== 輔助函式:產生 GPIO 狀態 JSON 字串 ====================
String getStatusJSON() {
StaticJsonDocument<200> doc; // 建立容量為 200 位元的 JSON 文件物件
doc["GPIO25"] = stateR ? "ON" : "OFF"; // 設定 GPIO25 鍵值
doc["GPIO26"] = stateG ? "ON" : "OFF"; // 設定 GPIO26 鍵值
doc["GPIO27"] = stateB ? "ON" : "OFF"; // 設定 GPIO27 鍵值
String jsonString; // 宣告字串變數
serializeJson(doc, jsonString); // 將 JSON 物件轉為字串
return jsonString; // 回傳 JSON 字串
}
// ==================== 輔助函式:廣播最新狀態至所有 WebSocket 用戶端 ====================
void broadcastStatus() {
StaticJsonDocument<500> doc; // 建立 JSON 文件物件
doc["GPIO25"] = stateR ? "ON" : "OFF"; // 記錄 GPIO25 狀態
doc["GPIO26"] = stateG ? "ON" : "OFF"; // 記錄 GPIO26 狀態
doc["GPIO27"] = stateB ? "ON" : "OFF"; // 記錄 GPIO27 狀態
JsonArray historyArray = doc.createNestedArray("history"); // 建立歷史紀錄 JSON 陣列
for (int i = 0; i < historyCount; i++) {
historyArray.add(ipHistory[i]); // 將歷史紀錄加入陣列
}
String jsonOutput; // 宣告輸出字串
serializeJson(doc, jsonOutput); // 序列化為 JSON
webSocket.broadcastTXT(jsonOutput); // 透過 WebSocket 向所有用戶端廣播訊息
}
// ==================== HTTP 路由:提供首頁網頁HTML ====================
void handleRoot() {
// 序列埠輸出連線紀錄
Serial.print("[HTTP] 用戶端連線,IP: ");
Serial.println(server.client().remoteIP().toString());
// 構造 HTML + CSS + JS 響應式單頁網頁
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>NodeMCU-32S RGB LED 控制面板</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
body { font-family: Arial, sans-serif; background-color: #121212; color: #ffffff; text-align: center; margin: 0; padding: 20px; }
h1 { color: #00e676; margin-bottom: 5px; }
p.guide { color: #bbb; font-size: 14px; margin-bottom: 25px; }
.container { max-width: 500px; margin: 0 auto; background: #1e1e1e; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); }
.control-group { margin-bottom: 25px; padding: 15px; background: #2a2a2a; border-radius: 8px; }
.status-msg { font-size: 18px; font-weight: bold; color: #00bcd4; margin-bottom: 10px; }
.switch-label { font-size: 16px; margin-bottom: 8px; font-weight: bold; display: block; color: #ffeb3b; }
.switch-wrapper { display: flex; align-items: center; justify-content: center; gap: 10px; }
.switch { position: relative; display: inline-block; width: 60px; height: 34px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #555; transition: .4s; border-radius: 34px; }
.slider:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: #00e676; }
input:checked + .slider:before { transform: translateX(26px); }
.history-box { margin-top: 30px; text-align: left; background: #000000; padding: 15px; border-radius: 8px; border: 1px solid #333; }
.history-box h3 { margin-top: 0; color: #ff9800; font-size: 16px; }
.history-list { font-family: monospace; color: #00ff00; font-size: 14px; white-space: pre-wrap; margin: 0; }
</style>
</head>
<body>
<div class="container">
<h1>RGB LED 遠端控制面板</h1>
<p class="guide">使用說明:切換下方開關可即時切換 ESP32 GPIO 開關狀態,系統將透過 WebSocket 與多使用者裝置同步狀態。</p>
<!-- GPIO25 (Red) 控制組 -->
<div class="control-group">
<div id="msg25" class="status-msg">GPIO25: OFF</div>
<span class="switch-label">作用腳位: GPIO25 (R)</span>
<div class="switch-wrapper">
<span>OFF</span>
<label class="switch">
<input type="checkbox" id="btn25" onchange="toggleLED(25, this.checked)">
<span class="slider"></span>
</label>
<span>ON</span>
</div>
</div>
<!-- GPIO26 (Green) 控制組 -->
<div class="control-group">
<div id="msg26" class="status-msg">GPIO26: OFF</div>
<span class="switch-label">作用腳位: GPIO26 (G)</span>
<div class="switch-wrapper">
<span>OFF</span>
<label class="switch">
<input type="checkbox" id="btn26" onchange="toggleLED(26, this.checked)">
<span class="slider"></span>
</label>
<span>ON</span>
</div>
</div>
<!-- GPIO27 (Blue) 控制組 -->
<div class="control-group">
<div id="msg27" class="status-msg">GPIO27: OFF</div>
<span class="switch-label">作用腳位: GPIO27 (B)</span>
<div class="switch-wrapper">
<span>OFF</span>
<label class="switch">
<input type="checkbox" id="btn27" onchange="toggleLED(27, this.checked)">
<span class="slider"></span>
</label>
<span>ON</span>
</div>
</div>
<!-- 最近 5 次連線 IP 回應框 -->
<div class="history-box">
<h3>最近 5 次發送控制請求 IP 紀錄</h3>
<div id="historyList" class="history-list">尚無控制紀錄</div>
</div>
</div>
<script>
var ws;
// 初始化 WebSocket 連線
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81/');
ws.onmessage = function(event) {
var data = JSON.parse(event.data);
updateUI(data);
};
ws.onclose = function() {
setTimeout(initWebSocket, 2000); // 斷線自動重連
};
}
// 更新前端 UI 介面顯示
function updateUI(data) {
if(data.GPIO25 !== undefined) {
$("#btn25").prop("checked", data.GPIO25 === "ON");
$("#msg25").text("GPIO25: " + data.GPIO25);
}
if(data.GPIO26 !== undefined) {
$("#btn26").prop("checked", data.GPIO26 === "ON");
$("#msg26").text("GPIO26: " + data.GPIO26);
}
if(data.GPIO27 !== undefined) {
$("#btn27").prop("checked", data.GPIO27 === "ON");
$("#msg27").text("GPIO27: " + data.GPIO27);
}
if(data.history !== undefined) {
if(data.history.length > 0) {
$("#historyList").html(data.history.join("<br>"));
} else {
$("#historyList").text("尚無控制紀錄");
}
}
}
// 發送 Ajax 控制請求
function toggleLED(gpio, state) {
var stateStr = state ? "ON" : "OFF";
$.ajax({
url: "/toggle?gpio=" + gpio + "&state=" + stateStr,
type: "GET",
success: function(response) {
// 操作成功的處理,由 WebSocket 自動廣播更新
}
});
}
// 頁面載入完成後初始化 WebSocket
$(document).ready(function() {
initWebSocket();
});
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html); // 發送 HTTP 200 回應與網頁 HTML 內容
}
// ==================== HTTP 路由:處理切換控制 API (/toggle) ====================
void handleToggle() {
String clientIP = server.client().remoteIP().toString(); // 獲取請求者的 IP 位址
if (server.hasArg("gpio") && server.hasArg("state")) { // 檢查參數是否存在
int gpio = server.arg("gpio").toInt(); // 讀取 GPIO 腳位編號
String stateStr = server.arg("state"); // 讀取 ON/OFF 狀態
bool isOn = (stateStr == "ON"); // 轉換為邏輯布林值
// 根據腳位更新燈光實體狀態
if (gpio == 25) {
stateR = isOn;
digitalWrite(pinR, stateR ? HIGH : LOW);
} else if (gpio == 26) {
stateG = isOn;
digitalWrite(pinG, stateG ? HIGH : LOW);
} else if (gpio == 27) {
stateB = isOn;
digitalWrite(pinB, stateB ? HIGH : LOW);
}
// 新增歷史紀錄
addHistory(clientIP, stateStr);
// 印出序列埠除錯追蹤訊息
Serial.print("[HTTP Request] IP: ");
Serial.print(clientIP);
Serial.print(" | Action: GPIO");
Serial.print(gpio);
Serial.print(" -> ");
Serial.print(stateStr);
Serial.print(" | JSON Resp: ");
Serial.println(getStatusJSON());
// 回傳狀態 JSON 格式數據給請求端
server.send(200, "application/json", getStatusJSON());
// 透過 WebSocket 廣播最新狀態給所有線上裝置
broadcastStatus();
} else {
server.send(400, "text/plain", "Bad Request"); // 參數缺失,回傳 HTTP 400 錯誤
}
}
// ==================== WebSocket 事件處理核心 ====================
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
if (type == WStype_CONNECTED) { // 用護端連線成功事件
IPAddress ip = webSocket.remoteIP(num); // 讀取用戶端 IP
Serial.print("[WebSocket] 用戶端 #");
Serial.print(num);
Serial.print(" 已連線,IP: ");
Serial.println(ip.toString());
// 連線時立即將當前所有 GPIO 狀態與歷史紀錄傳送給該連線用戶
broadcastStatus();
} else if (type == WStype_DISCONNECTED) { // 用戶端斷線事件
Serial.print("[WebSocket] 用戶端 #");
Serial.print(num);
Serial.println(" 已離線");
}
}
// ==================== 程式初始化設定 (setup) ====================
void setup() {
Serial.begin(115200); // 初始化序列埠監控器,傳輸速率設定為 115200 baud
delay(100);
// 初始化 GPIO 腳位輸出模式
pinMode(pinR, OUTPUT);
pinMode(pinG, OUTPUT);
pinMode(pinB, OUTPUT);
// 預設關閉 RGB LED 所有燈光 (共陰極預設低電位 LOW)
digitalWrite(pinR, LOW);
digitalWrite(pinG, LOW);
digitalWrite(pinB, LOW);
// 設定 WiFi 模式為 Station (STA) 模式
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password); // 開始連線至指定無線網路
Serial.println("\n正在建立 Station Mode WiFi 連線...");
while (WiFi.status() != WL_CONNECTED) { // 重新等待直到 WiFi 連線成功
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi 連線成功!");
Serial.print("IP 位址: ");
Serial.println(WiFi.localIP()); // 輸出 ESP32 獲取的區域網路 IP
// 註冊 HTTP 路由處理解析器
server.on("/", handleRoot); // 根目錄路由,載入 Web UI 網頁
server.on("/toggle", handleToggle); // Toggle API 路由,處理 LED 切換動作
server.begin(); // 啟動 WebServer 服務
Serial.println("HTTP Web Server 服務已啟動");
// 初始化 WebSocket 伺服器
webSocket.begin();
webSocket.onEvent(webSocketEvent); // 綁定 WebSocket 事件監聽處理函式
Serial.println("WebSocket 服務已啟動 (Port 81)");
}
// ==================== 程式主要循環執行區塊 (loop) ====================
void loop() {
// 自動檢查 WiFi 連線狀態,斷線時自動重新連線
if (WiFi.status() != WL_CONNECTED) {
Serial.println("[WiFi] 偵測到連線中斷,正在嘗試重新建立連線...");
WiFi.disconnect();
WiFi.reconnect();
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\n[WiFi] 連線已重新建立!IP 位址: ");
Serial.println(WiFi.localIP());
}
server.handleClient(); // 處理待處理的 HTTP 請求
webSocket.loop(); // 處理待處理的 WebSocket 封包與事件
}
草稿碼 > 匯入程式庫 > 管理程式庫 > 搜尋 WebSockets > 安裝 2.7.2
草稿碼 > 匯入程式庫 > 管理程式庫 > 搜尋 ArduinoJson > 安裝 6.21.6
筆電連 192.168.50.105
手機連 192.168.50.105
筆電 GPIO25 ON
手機 GPIO25 OFF
手機 GPIO26 ON
筆電 GPIO26 OFF
手機 GPIO27 ON
筆電 GPIO27 OFF
筆電 GPIO25 ON GPIO26 ON
手機 GPIO26 OFF GPIO27 ON
提示詞
程式運作正確,我要進一步優化。開始優化修改程式前,依需求提供你的規劃給我確認
#增加一個切換按鈕,一次控制全部LED燈開或關
##切換按鈕樣式:
-往左往右分別是ON OFF
-切換開關的標籤改為 ALL
-切換開關的左右兩端,以文字明確標示 ON OFF
##位置
-目前切換按鈕的上方
#修改顯示最近五次發出 ON OFF 的連線 IP訊息的回應框樣式
IP:xxxxxx, GPIO25|ON
IP:xxxxxx, GPIO26|OFF
...
Code
#include <WiFi.h> // 引用 ESP32 原生 WiFi 標頭檔
#include <WebServer.h> // 引用 ESP32 原生 WebServer 標頭檔
#include <WebSocketsServer.h> // 引用第三方 WebSocketsServer 標頭檔
#include <ArduinoJson.h> // 引用第三方 ArduinoJson 標頭檔
// ==================== 網路設定 ====================
const char* ssid = "chuangHome"; // WiFi SSID 帳號
const char* password = "0988087024"; // WiFi 密碼
// ==================== 硬體腳位設定 ====================
const int pinR = 25; // RGB LED 紅燈連接至 GPIO25
const int pinG = 26; // RGB LED 綠燈連接至 GPIO26
const int pinB = 27; // RGB LED 藍燈連接至 GPIO27
// ==================== 物件建立 ====================
WebServer server(80); // 建立 HTTP 伺服器,監聽 80 埠
WebSocketsServer webSocket = WebSocketsServer(81); // 建立 WebSocket 伺服器,監聽 81 埠
// ==================== 全域變數 ====================
bool stateR = false; // GPIO25 (R) 狀態紀錄 (false: OFF, true: ON)
bool stateG = false; // GPIO26 (G) 狀態紀錄 (false: OFF, true: ON)
bool stateB = false; // GPIO27 (B) 狀態紀錄 (false: OFF, true: ON)
String ipHistory[5]; // 儲存最近 5 次發出 ON/OFF 控制的紀錄訊息
int historyCount = 0; // 紀錄目前已儲存的歷史訊息數量
// ==================== 輔助函式:新增控制歷史紀錄 (新格式) ====================
void addHistory(String ip, String target, String action) {
// 新格式:IP:xxxxxx, GPIO25|ON 或 IP:xxxxxx, ALL|ON
String entry = "IP:" + ip + ", " + target + "|" + action;
// 陣列內容往後推移,騰出第 0 個位置
for (int i = 4; i > 0; i--) {
ipHistory[i] = ipHistory[i - 1];
}
ipHistory[0] = entry; // 最新紀錄放入第 0 位
if (historyCount < 5) historyCount++; // 更新總數(最多 5 筆)
}
// ==================== 輔助函式:產生 GPIO 狀態 JSON 字串 ====================
String getStatusJSON() {
StaticJsonDocument<200> doc; // 建立容量為 200 位元的 JSON 文件物件
doc["GPIO25"] = stateR ? "ON" : "OFF"; // 設定 GPIO25 鍵值
doc["GPIO26"] = stateG ? "ON" : "OFF"; // 設定 GPIO26 鍵值
doc["GPIO27"] = stateB ? "ON" : "OFF"; // 設定 GPIO27 鍵值
String jsonString; // 宣告字串變數
serializeJson(doc, jsonString); // 將 JSON 物件轉為字串
return jsonString; // 回傳 JSON 字串
}
// ==================== 輔助函式:廣播最新狀態至所有 WebSocket 用戶端 ====================
void broadcastStatus() {
StaticJsonDocument<500> doc; // 建立 JSON 文件物件
doc["GPIO25"] = stateR ? "ON" : "OFF"; // 記錄 GPIO25 狀態
doc["GPIO26"] = stateG ? "ON" : "OFF"; // 記錄 GPIO26 狀態
doc["GPIO27"] = stateB ? "ON" : "OFF"; // 記錄 GPIO27 狀態
JsonArray historyArray = doc.createNestedArray("history"); // 建立歷史紀錄 JSON 陣列
for (int i = 0; i < historyCount; i++) {
historyArray.add(ipHistory[i]); // 將歷史紀錄加入陣列
}
String jsonOutput; // 宣告輸出字串
serializeJson(doc, jsonOutput); // 序列化為 JSON
webSocket.broadcastTXT(jsonOutput); // 透過 WebSocket 向所有用戶端廣播訊息
}
// ==================== HTTP 路由:提供首頁網頁HTML ====================
void handleRoot() {
// 序列埠輸出連線紀錄
Serial.print("[HTTP] 用戶端連線,IP: ");
Serial.println(server.client().remoteIP().toString());
// 構造 HTML + CSS + JS 響應式單頁網頁
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>NodeMCU-32S RGB LED 控制面板</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
body { font-family: Arial, sans-serif; background-color: #121212; color: #ffffff; text-align: center; margin: 0; padding: 20px; }
h1 { color: #00e676; margin-bottom: 5px; }
p.guide { color: #bbb; font-size: 14px; margin-bottom: 25px; }
.container { max-width: 500px; margin: 0 auto; background: #1e1e1e; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); }
.control-group { margin-bottom: 20px; padding: 15px; background: #2a2a2a; border-radius: 8px; }
.all-group { background: #332900; border: 1px solid #ffb300; } /* 全區按鈕使用醒目背景色 */
.status-msg { font-size: 18px; font-weight: bold; color: #00bcd4; margin-bottom: 10px; }
.switch-label { font-size: 16px; margin-bottom: 8px; font-weight: bold; display: block; color: #ffeb3b; }
.switch-wrapper { display: flex; align-items: center; justify-content: center; gap: 10px; }
.switch { position: relative; display: inline-block; width: 60px; height: 34px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #555; transition: .4s; border-radius: 34px; }
.slider:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: #00e676; }
input:checked + .slider:before { transform: translateX(26px); }
.history-box { margin-top: 25px; text-align: left; background: #000000; padding: 15px; border-radius: 8px; border: 1px solid #333; }
.history-box h3 { margin-top: 0; color: #ff9800; font-size: 16px; }
.history-list { font-family: monospace; color: #00ff00; font-size: 14px; line-height: 1.6; white-space: pre-wrap; margin: 0; }
</style>
</head>
<body>
<div class="container">
<h1>RGB LED 遠端控制面板</h1>
<p class="guide">使用說明:切換切換開關可即時控制 ESP32 GPIO 開關狀態,系統將透過 WebSocket 與多裝置同步狀態。</p>
<!-- 全區按鈕 (ALL) 控制組 -->
<div class="control-group all-group">
<div id="msgALL" class="status-msg">ALL: OFF</div>
<span class="switch-label">作用腳位: ALL</span>
<div class="switch-wrapper">
<span>OFF</span>
<label class="switch">
<input type="checkbox" id="btnALL" onchange="toggleLED('ALL', this.checked)">
<span class="slider"></span>
</label>
<span>ON</span>
</div>
</div>
<!-- GPIO25 (Red) 控制組 -->
<div class="control-group">
<div id="msg25" class="status-msg">GPIO25: OFF</div>
<span class="switch-label">作用腳位: GPIO25 (R)</span>
<div class="switch-wrapper">
<span>OFF</span>
<label class="switch">
<input type="checkbox" id="btn25" onchange="toggleLED('25', this.checked)">
<span class="slider"></span>
</label>
<span>ON</span>
</div>
</div>
<!-- GPIO26 (Green) 控制組 -->
<div class="control-group">
<div id="msg26" class="status-msg">GPIO26: OFF</div>
<span class="switch-label">作用腳位: GPIO26 (G)</span>
<div class="switch-wrapper">
<span>OFF</span>
<label class="switch">
<input type="checkbox" id="btn26" onchange="toggleLED('26', this.checked)">
<span class="slider"></span>
</label>
<span>ON</span>
</div>
</div>
<!-- GPIO27 (Blue) 控制組 -->
<div class="control-group">
<div id="msg27" class="status-msg">GPIO27: OFF</div>
<span class="switch-label">作用腳位: GPIO27 (B)</span>
<div class="switch-wrapper">
<span>OFF</span>
<label class="switch">
<input type="checkbox" id="btn27" onchange="toggleLED('27', this.checked)">
<span class="slider"></span>
</label>
<span>ON</span>
</div>
</div>
<!-- 最近 5 次連線 IP 回應框 -->
<div class="history-box">
<h3>最近 5 次發送控制請求 IP 紀錄</h3>
<div id="historyList" class="history-list">尚無控制紀錄</div>
</div>
</div>
<script>
var ws;
// 初始化 WebSocket 連線
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81/');
ws.onmessage = function(event) {
var data = JSON.parse(event.data);
updateUI(data);
};
ws.onclose = function() {
setTimeout(initWebSocket, 2000); // 斷線自動重連
};
}
// 更新前端 UI 介面顯示
function updateUI(data) {
if(data.GPIO25 !== undefined) {
$("#btn25").prop("checked", data.GPIO25 === "ON");
$("#msg25").text("GPIO25: " + data.GPIO25);
}
if(data.GPIO26 !== undefined) {
$("#btn26").prop("checked", data.GPIO26 === "ON");
$("#msg26").text("GPIO26: " + data.GPIO26);
}
if(data.GPIO27 !== undefined) {
$("#btn27").prop("checked", data.GPIO27 === "ON");
$("#msg27").text("GPIO27: " + data.GPIO27);
}
// 計算並更新 ALL 按鈕狀態
var allOn = (data.GPIO25 === "ON" && data.GPIO26 === "ON" && data.GPIO27 === "ON");
$("#btnALL").prop("checked", allOn);
$("#msgALL").text("ALL: " + (allOn ? "ON" : "OFF"));
// 更新歷史紀錄框
if(data.history !== undefined) {
if(data.history.length > 0) {
$("#historyList").html(data.history.join("<br>"));
} else {
$("#historyList").text("尚無控制紀錄");
}
}
}
// 發送 Ajax 控制請求
function toggleLED(target, state) {
var stateStr = state ? "ON" : "OFF";
$.ajax({
url: "/toggle?target=" + target + "&state=" + stateStr,
type: "GET",
success: function(response) {
// 操作成功的處理,由 WebSocket 自動廣播更新全域畫面
}
});
}
// 頁面載入完成後初始化 WebSocket
$(document).ready(function() {
initWebSocket();
});
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html); // 發送 HTTP 200 回應與網頁 HTML 內容
}
// ==================== HTTP 路由:處理切換控制 API (/toggle) ====================
void handleToggle() {
String clientIP = server.client().remoteIP().toString(); // 獲取請求者的 IP 位址
if (server.hasArg("target") && server.hasArg("state")) { // 檢查參數是否存在
String target = server.arg("target"); // 讀取控制目標 ("ALL", "25", "26", "27")
String stateStr = server.arg("state"); // 讀取 ON/OFF 狀態
bool isOn = (stateStr == "ON"); // 轉換為邏輯布林值
if (target == "ALL") {
// 一次全選控制所有 LED 燈
stateR = isOn;
stateG = isOn;
stateB = isOn;
digitalWrite(pinR, stateR ? HIGH : LOW);
digitalWrite(pinG, stateG ? HIGH : LOW);
digitalWrite(pinB, stateB ? HIGH : LOW);
// 新增歷史紀錄 (格式: ALL)
addHistory(clientIP, "ALL", stateStr);
}
else {
int gpio = target.toInt();
// 根據單一腳位更新燈光實體狀態
if (gpio == 25) {
stateR = isOn;
digitalWrite(pinR, stateR ? HIGH : LOW);
addHistory(clientIP, "GPIO25", stateStr);
} else if (gpio == 26) {
stateG = isOn;
digitalWrite(pinG, stateG ? HIGH : LOW);
addHistory(clientIP, "GPIO26", stateStr);
} else if (gpio == 27) {
stateB = isOn;
digitalWrite(pinB, stateB ? HIGH : LOW);
addHistory(clientIP, "GPIO27", stateStr);
}
}
// 印出序列埠除錯追蹤訊息
Serial.print("[HTTP Request] IP: ");
Serial.print(clientIP);
Serial.print(" | Target: ");
Serial.print(target);
Serial.print(" -> ");
Serial.print(stateStr);
Serial.print(" | JSON Resp: ");
Serial.println(getStatusJSON());
// 回傳狀態 JSON 格式數據給請求端
server.send(200, "application/json", getStatusJSON());
// 透過 WebSocket 廣播最新狀態給所有線上裝置
broadcastStatus();
} else {
server.send(400, "text/plain", "Bad Request"); // 參數缺失,回傳 HTTP 400 錯誤
}
}
// ==================== WebSocket 事件處理核心 ====================
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
if (type == WStype_CONNECTED) { // 用戶端連線成功事件
IPAddress ip = webSocket.remoteIP(num); // 讀取用戶端 IP
Serial.print("[WebSocket] 用戶端 #");
Serial.print(num);
Serial.print(" 已連線,IP: ");
Serial.println(ip.toString());
// 連線時立即將當前所有 GPIO 狀態與歷史紀錄傳送給該連線用戶
broadcastStatus();
} else if (type == WStype_DISCONNECTED) { // 用戶端斷線事件
Serial.print("[WebSocket] 用戶端 #");
Serial.print(num);
Serial.println(" 已離線");
}
}
// ==================== 程式初始化設定 (setup) ====================
void setup() {
Serial.begin(115200); // 初始化序列埠監控器,傳輸速率設定為 115200 baud
delay(100);
// 初始化 GPIO 腳位輸出模式
pinMode(pinR, OUTPUT);
pinMode(pinG, OUTPUT);
pinMode(pinB, OUTPUT);
// 預設關閉 RGB LED 所有燈光 (共陰極預設低電位 LOW)
digitalWrite(pinR, LOW);
digitalWrite(pinG, LOW);
digitalWrite(pinB, LOW);
// 設定 WiFi 模式為 Station (STA) 模式
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password); // 開始連線至指定無線網路
Serial.println("\n正在建立 Station Mode WiFi 連線...");
while (WiFi.status() != WL_CONNECTED) { // 重新等待直到 WiFi 連線成功
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi 連線成功!");
Serial.print("IP 位址: ");
Serial.println(WiFi.localIP()); // 輸出 ESP32 獲取的區域網路 IP
// 註冊 HTTP 路由處理解析器
server.on("/", handleRoot); // 根目錄路由,載入 Web UI 網頁
server.on("/toggle", handleToggle); // Toggle API 路由,處理 LED 切換動作
server.begin(); // 啟動 WebServer 服務
Serial.println("HTTP Web Server 服務已啟動");
// 初始化 WebSocket 伺服器
webSocket.begin();
webSocket.onEvent(webSocketEvent); // 綁定 WebSocket 事件監聽處理函式
Serial.println("WebSocket 服務已啟動 (Port 81)");
}
// ==================== 程式主要循環執行區塊 (loop) ====================
void loop() {
// 自動檢查 WiFi 連線狀態,斷線時自動重新連線
if (WiFi.status() != WL_CONNECTED) {
Serial.println("[WiFi] 偵測到連線中斷,正在嘗試重新建立連線...");
WiFi.disconnect();
WiFi.reconnect();
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\n[WiFi] 連線已重新建立!IP 位址: ");
Serial.println(WiFi.localIP());
}
server.handleClient(); // 處理待處理的 HTTP 請求
webSocket.loop(); // 處理待處理的 WebSocket 封包與事件
}
筆電連 192.168.50.105
手機連 192.168.50.105
筆電 ALL ON
手機 GPIO 25 OFF
筆電 GPIO 27 OFF
手機 GPIO 25 ON GPIO 26 OFF GPIO 27 ON
提示詞-1
程式運作正確。我要進一步優化。請依需求,提供你的規劃給我確認
#問題
-原網頁內容太長,無法在電腦或手機畫面完全呈現,需要往下捲動
#需求
-優化UI設計,能在單一頁面呈現全部內容
-務必維持響應式設計
-規劃三種UI設計
提示詞-2
我需要真實視覺呈現,模擬三種方案的介面布局給我評估
提示詞-3
採用方案2分頁籤卡片型 (Tabbed Card Layout)的布局,但LED控制分頁更改如下
-以4列2欄設計
-第一欄分別為目前的4個單一按鈕
-第二欄分別為回應的腳為狀態,格式如 GPIO25:ON
-刪除作用腳位的回應
開始修改前,依需求提出你的規劃給我確認
Code
#include <WiFi.h> // 引用 ESP32 原生 WiFi 標頭檔
#include <WebServer.h> // 引用 ESP32 原生 WebServer 標頭檔
#include <WebSocketsServer.h> // 引用第三方 WebSocketsServer 標頭檔
#include <ArduinoJson.h> // 引用第三方 ArduinoJson 標頭檔
// ==================== 網路設定 ====================
const char* ssid = "chuangHome"; // WiFi SSID 帳號
const char* password = "0988087024"; // WiFi 密碼
// ==================== 硬體腳位設定 ====================
const int pinR = 25; // RGB LED 紅燈連接至 GPIO25
const int pinG = 26; // RGB LED 綠燈連接至 GPIO26
const int pinB = 27; // RGB LED 藍燈連接至 GPIO27
// ==================== 物件建立 ====================
WebServer server(80); // 建立 HTTP 伺服器,監聽 80 埠
WebSocketsServer webSocket = WebSocketsServer(81); // 建立 WebSocket 伺服器,監聽 81 埠
// ==================== 全域變數 ====================
bool stateR = false; // GPIO25 (R) 狀態紀錄 (false: OFF, true: ON)
bool stateG = false; // GPIO26 (G) 狀態紀錄 (false: OFF, true: ON)
bool stateB = false; // GPIO27 (B) 狀態紀錄 (false: OFF, true: ON)
String ipHistory[5]; // 儲存最近 5 次發出 ON/OFF 控制的紀錄訊息
int historyCount = 0; // 紀錄目前已儲存的歷史訊息數量
// ==================== 輔助函式:新增控制歷史紀錄 ====================
void addHistory(String ip, String target, String action) {
// 格式:IP:xxxxxx, GPIO25|ON 或 IP:xxxxxx, ALL|ON
String entry = "IP:" + ip + ", " + target + "|" + action;
// 陣列內容往後推移,騰出第 0 個位置
for (int i = 4; i > 0; i--) {
ipHistory[i] = ipHistory[i - 1];
}
ipHistory[0] = entry; // 最新紀錄放入第 0 位
if (historyCount < 5) historyCount++; // 更新總數(最多 5 筆)
}
// ==================== 輔助函式:產生 GPIO 狀態 JSON 字串 ====================
String getStatusJSON() {
StaticJsonDocument<200> doc; // 建立容量為 200 位元的 JSON 文件物件
doc["GPIO25"] = stateR ? "ON" : "OFF"; // 設定 GPIO25 鍵值
doc["GPIO26"] = stateG ? "ON" : "OFF"; // 設定 GPIO26 鍵值
doc["GPIO27"] = stateB ? "ON" : "OFF"; // 設定 GPIO27 鍵值
String jsonString; // 宣告字串變數
serializeJson(doc, jsonString); // 將 JSON 物件轉為字串
return jsonString; // 回傳 JSON 字串
}
// ==================== 輔助函式:廣播最新狀態至所有 WebSocket 用戶端 ====================
void broadcastStatus() {
StaticJsonDocument<500> doc; // 建立 JSON 文件物件
doc["GPIO25"] = stateR ? "ON" : "OFF"; // 記錄 GPIO25 狀態
doc["GPIO26"] = stateG ? "ON" : "OFF"; // 記錄 GPIO26 狀態
doc["GPIO27"] = stateB ? "ON" : "OFF"; // 記錄 GPIO27 狀態
JsonArray historyArray = doc.createNestedArray("history"); // 建立歷史紀錄 JSON 陣列
for (int i = 0; i < historyCount; i++) {
historyArray.add(ipHistory[i]); // 將歷史紀錄加入陣列
}
String jsonOutput; // 宣告輸出字串
serializeJson(doc, jsonOutput); // 序列化為 JSON
webSocket.broadcastTXT(jsonOutput); // 透過 WebSocket 向所有用戶端廣播訊息
}
// ==================== HTTP 路由:提供首頁網頁HTML ====================
void handleRoot() {
// 序列埠輸出連線紀錄
Serial.print("[HTTP] 用戶端連線,IP: ");
Serial.println(server.client().remoteIP().toString());
// 構造 HTML + CSS + JS 響應式單頁網頁 (Tabbed Card Layout 免捲動設計)
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>NodeMCU-32S RGB LED 控制面板</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; background-color: #121212; color: #ffffff; text-align: center; display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 10px; }
.container { width: 100%; max-width: 480px; background: #1e1e1e; padding: 20px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.6); }
h1 { color: #00e676; font-size: 20px; margin-bottom: 15px; }
/* 頁籤選單樣式 */
.tabs { display: flex; border-bottom: 2px solid #333; margin-bottom: 15px; }
.tab-btn { flex: 1; padding: 10px; background: none; border: none; color: #888; font-size: 15px; font-weight: bold; cursor: pointer; transition: 0.3s; }
.tab-btn.active { color: #00e676; border-bottom: 3px solid #00e676; }
/* 頁籤內容顯示區域 */
.tab-content { display: none; }
.tab-content.active { display: block; }
/* 4 列 2 欄表格佈局 */
.control-table { width: 100%; border-collapse: collapse; background: #2a2a2a; border-radius: 8px; overflow: hidden; }
.control-row { display: flex; align-items: center; border-bottom: 1px solid #3d3d3d; padding: 12px 10px; }
.control-row:last-child { border-bottom: none; }
.col-switch { flex: 1.2; display: flex; align-items: center; justify-content: center; gap: 6px; }
.col-status { flex: 1; font-size: 16px; font-weight: bold; color: #00bcd4; text-align: center; }
.btn-tag { font-size: 13px; font-weight: bold; color: #ffeb3b; width: 55px; text-align: left; }
/* 切換開關 CSS Slider */
.switch { position: relative; display: inline-block; width: 50px; height: 26px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #555; transition: .3s; border-radius: 26px; }
.slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; }
input:checked + .slider { background-color: #00e676; }
input:checked + .slider:before { transform: translateX(24px); }
.switch-text { font-size: 12px; color: #bbb; }
/* 歷史紀錄框樣式 */
.history-box { background: #000000; padding: 15px; border-radius: 8px; border: 1px solid #333; min-height: 180px; text-align: left; }
.history-list { font-family: monospace; color: #00ff00; font-size: 13px; line-height: 1.8; white-space: pre-wrap; margin: 0; }
</style>
</head>
<body>
<div class="container">
<h1>RGB LED 遠端控制面板</h1>
<!-- 分頁選單按鈕 -->
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('control')">LED 控制</button>
<button class="tab-btn" onclick="switchTab('history')">連線歷史紀錄</button>
</div>
<!-- 分頁 1: LED 控制 (4 列 2 欄) -->
<div id="tab-control" class="tab-content active">
<div class="control-table">
<!-- 列 1: ALL -->
<div class="control-row">
<div class="col-switch">
<span class="switch-text">OFF</span>
<label class="switch">
<input type="checkbox" id="btnALL" onchange="toggleLED('ALL', this.checked)">
<span class="slider"></span>
</label>
<span class="switch-text">ON</span>
<span class="btn-tag">ALL</span>
</div>
<div id="msgALL" class="col-status">ALL: OFF</div>
</div>
<!-- 列 2: GPIO25 -->
<div class="control-row">
<div class="col-switch">
<span class="switch-text">OFF</span>
<label class="switch">
<input type="checkbox" id="btn25" onchange="toggleLED('25', this.checked)">
<span class="slider"></span>
</label>
<span class="switch-text">ON</span>
<span class="btn-tag">GPIO25</span>
</div>
<div id="msg25" class="col-status">GPIO25: OFF</div>
</div>
<!-- 列 3: GPIO26 -->
<div class="control-row">
<div class="col-switch">
<span class="switch-text">OFF</span>
<label class="switch">
<input type="checkbox" id="btn26" onchange="toggleLED('26', this.checked)">
<span class="slider"></span>
</label>
<span class="switch-text">ON</span>
<span class="btn-tag">GPIO26</span>
</div>
<div id="msg26" class="col-status">GPIO26: OFF</div>
</div>
<!-- 列 4: GPIO27 -->
<div class="control-row">
<div class="col-switch">
<span class="switch-text">OFF</span>
<label class="switch">
<input type="checkbox" id="btn27" onchange="toggleLED('27', this.checked)">
<span class="slider"></span>
</label>
<span class="switch-text">ON</span>
<span class="btn-tag">GPIO27</span>
</div>
<div id="msg27" class="col-status">GPIO27: OFF</div>
</div>
</div>
</div>
<!-- 分頁 2: 連線歷史紀錄 -->
<div id="tab-history" class="tab-content">
<div class="history-box">
<div id="historyList" class="history-list">尚無控制紀錄</div>
</div>
</div>
</div>
<script>
var ws;
// 切換 Tab 分頁
function switchTab(tabName) {
$(".tab-btn").removeClass("active");
$(".tab-content").removeClass("active");
if (tabName === 'control') {
$(".tab-btn:nth-child(1)").addClass("active");
$("#tab-control").addClass("active");
} else {
$(".tab-btn:nth-child(2)").addClass("active");
$("#tab-history").addClass("active");
}
}
// 初始化 WebSocket 連線
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81/');
ws.onmessage = function(event) {
var data = JSON.parse(event.data);
updateUI(data);
};
ws.onclose = function() {
setTimeout(initWebSocket, 2000); // 斷線自動重連
};
}
// 更新前端 UI 介面顯示
function updateUI(data) {
if(data.GPIO25 !== undefined) {
$("#btn25").prop("checked", data.GPIO25 === "ON");
$("#msg25").text("GPIO25: " + data.GPIO25);
}
if(data.GPIO26 !== undefined) {
$("#btn26").prop("checked", data.GPIO26 === "ON");
$("#msg26").text("GPIO26: " + data.GPIO26);
}
if(data.GPIO27 !== undefined) {
$("#btn27").prop("checked", data.GPIO27 === "ON");
$("#msg27").text("GPIO27: " + data.GPIO27);
}
// 動態判定並更新 ALL 開關狀態
var allOn = (data.GPIO25 === "ON" && data.GPIO26 === "ON" && data.GPIO27 === "ON");
$("#btnALL").prop("checked", allOn);
$("#msgALL").text("ALL: " + (allOn ? "ON" : "OFF"));
// 更新歷史紀錄框
if(data.history !== undefined) {
if(data.history.length > 0) {
$("#historyList").html(data.history.join("<br>"));
} else {
$("#historyList").text("尚無控制紀錄");
}
}
}
// 發送 Ajax 控制請求
function toggleLED(target, state) {
var stateStr = state ? "ON" : "OFF";
$.ajax({
url: "/toggle?target=" + target + "&state=" + stateStr,
type: "GET",
success: function(response) {
// 操作成功,由 WebSocket 廣播自動同步更新 UI
}
});
}
// 頁面載入完成後初始化 WebSocket
$(document).ready(function() {
initWebSocket();
});
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html); // 發送 HTTP 200 回應與網頁 HTML 內容
}
// ==================== HTTP 路由:處理切換控制 API (/toggle) ====================
void handleToggle() {
String clientIP = server.client().remoteIP().toString(); // 獲取請求者的 IP 位址
if (server.hasArg("target") && server.hasArg("state")) { // 檢查參數是否存在
String target = server.arg("target"); // 讀取控制目標 ("ALL", "25", "26", "27")
String stateStr = server.arg("state"); // 讀取 ON/OFF 狀態
bool isOn = (stateStr == "ON"); // 轉換為邏輯布林值
if (target == "ALL") {
// 一次全選控制所有 LED 燈
stateR = isOn;
stateG = isOn;
stateB = isOn;
digitalWrite(pinR, stateR ? HIGH : LOW);
digitalWrite(pinG, stateG ? HIGH : LOW);
digitalWrite(pinB, stateB ? HIGH : LOW);
// 新增歷史紀錄 (格式: ALL)
addHistory(clientIP, "ALL", stateStr);
}
else {
int gpio = target.toInt();
// 根據單一腳位更新燈光實體狀態
if (gpio == 25) {
stateR = isOn;
digitalWrite(pinR, stateR ? HIGH : LOW);
addHistory(clientIP, "GPIO25", stateStr);
} else if (gpio == 26) {
stateG = isOn;
digitalWrite(pinG, stateG ? HIGH : LOW);
addHistory(clientIP, "GPIO26", stateStr);
} else if (gpio == 27) {
stateB = isOn;
digitalWrite(pinB, stateB ? HIGH : LOW);
addHistory(clientIP, "GPIO27", stateStr);
}
}
// 印出序列埠除錯追蹤訊息
Serial.print("[HTTP Request] IP: ");
Serial.print(clientIP);
Serial.print(" | Target: ");
Serial.print(target);
Serial.print(" -> ");
Serial.print(stateStr);
Serial.print(" | JSON Resp: ");
Serial.println(getStatusJSON());
// 回傳狀態 JSON 格式數據給請求端
server.send(200, "application/json", getStatusJSON());
// 透過 WebSocket 廣播最新狀態給所有線上裝置
broadcastStatus();
} else {
server.send(400, "text/plain", "Bad Request"); // 參數缺失,回傳 HTTP 400 錯誤
}
}
// ==================== WebSocket 事件處理核心 ====================
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
if (type == WStype_CONNECTED) { // 用戶端連線成功事件
IPAddress ip = webSocket.remoteIP(num); // 讀取用戶端 IP
Serial.print("[WebSocket] 用戶端 #");
Serial.print(num);
Serial.print(" 已連線,IP: ");
Serial.println(ip.toString());
// 連線時立即將當前所有 GPIO 狀態與歷史紀錄傳送給該連線用戶
broadcastStatus();
} else if (type == WStype_DISCONNECTED) { // 用戶端斷線事件
Serial.print("[WebSocket] 用戶端 #");
Serial.print(num);
Serial.println(" 已離線");
}
}
// ==================== 程式初始化設定 (setup) ====================
void setup() {
Serial.begin(115200); // 初始化序列埠監控器,傳輸速率設定為 115200 baud
delay(100);
// 初始化 GPIO 腳位輸出模式
pinMode(pinR, OUTPUT);
pinMode(pinG, OUTPUT);
pinMode(pinB, OUTPUT);
// 預設關閉 RGB LED 所有燈光 (共陰極預設低電位 LOW)
digitalWrite(pinR, LOW);
digitalWrite(pinG, LOW);
digitalWrite(pinB, LOW);
// 設定 WiFi 模式為 Station (STA) 模式
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password); // 開始連線至指定無線網路
Serial.println("\n正在建立 Station Mode WiFi 連線...");
while (WiFi.status() != WL_CONNECTED) { // 重新等待直到 WiFi 連線成功
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi 連線成功!");
Serial.print("IP 位址: ");
Serial.println(WiFi.localIP()); // 輸出 ESP32 獲取的區域網路 IP
// 註冊 HTTP 路由處理解析器
server.on("/", handleRoot); // 根目錄路由,載入 Web UI 網頁
server.on("/toggle", handleToggle); // Toggle API 路由,處理 LED 切換動作
server.begin(); // 啟動 WebServer 服務
Serial.println("HTTP Web Server 服務已啟動");
// 初始化 WebSocket 伺服器
webSocket.begin();
webSocket.onEvent(webSocketEvent); // 綁定 WebSocket 事件監聽處理函式
Serial.println("WebSocket 服務已啟動 (Port 81)");
}
// ==================== 程式主要循環執行區塊 (loop) ====================
void loop() {
// 自動檢查 WiFi 連線狀態,斷線時自動重新連線
if (WiFi.status() != WL_CONNECTED) {
Serial.println("[WiFi] 偵測到連線中斷,正在嘗試重新建立連線...");
WiFi.disconnect();
WiFi.reconnect();
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\n[WiFi] 連線已重新建立!IP 位址: ");
Serial.println(WiFi.localIP());
}
server.handleClient(); // 處理待處理的 HTTP 請求
webSocket.loop(); // 處理待處理的 WebSocket 封包與事件
}
提示詞-1
程式運作正常,我要進一步優化,需求
#WiFi連線
-不要寫死在程式端,改成動態輸入方式
-評估可行的方式,分析優缺點,規劃三種可行方案,給我參考
提示詞-2
我選擇方案 1(WiFiManager 模式),請開始規劃與撰寫程式碼。
Code
草稿碼 > 匯入程式庫 > 管理程式庫 > 搜尋 WiFiManager > 安裝 2.0.17
如何進行第一次配網
上傳程式:將上述程式碼燒錄至 ESP32 後開啟序列埠監控器 (115200 baud)。
連線至熱點:拿起手機或電腦,開啟 WiFi 搜尋並連線至 ESP32-RGB-Setup(預設無密碼)。
自動跳頁配網:手機會自動彈出設定網頁(若未彈出,請開啟瀏覽器輸入 192.168.4.1)。
選擇 WiFi 並儲存:點選 Configure WiFi ➔ 選擇您家的 WiFi SSID ➔ 輸入密碼 ➔ 點擊 Save。
配網完成:ESP32 會自動儲存密碼並重啟連線。連線成功後,序列埠會印出分配到的 IP 位址,即可直接開啟控制面板。
提示詞-1
我之前曾經連線至WiFi。在相同的WiFi下,要測試 WiFiManager 的Captive Portal 強制跳轉配網程序,要如何做
提示詞-2
詳細說明方法 3:加入「實體重置按鍵」邏輯 的詳細運作程序與測試 給我評估
提示詞-3
#將這套 BOOT 鍵重置邏輯整合進上一版的完整 ESP32 程式碼中
#提供詳細的測試步驟
先提供規劃給我確認
提示詞-4
我需要再確認:當ESP32 直接跳過熱點,約 3~5 秒內自動連上設定好的 WiFi,並印出分配到的 IP,是將IP輸出至序列埠或呈現在網頁上?
Code
#include <WiFi.h> // 引用 ESP32 原生 WiFi 標頭檔
#include <WebServer.h> // 引用 ESP32 原生 WebServer 標頭檔
#include <WebSocketsServer.h> // 引用第三方 WebSocketsServer 標頭檔
#include <ArduinoJson.h> // 引用第三方 ArduinoJson 標頭檔
#include <WiFiManager.h> // 引用第三方 WiFiManager 標頭檔 (by tzapu)
// ==================== 硬體腳位設定 ====================
const int pinR = 25; // RGB LED 紅燈連接至 GPIO25
const int pinG = 26; // RGB LED 綠燈連接至 GPIO26
const int pinB = 27; // RGB LED 藍燈連接至 GPIO27
const int TRIGGER_PIN = 0; // 板載 BOOT 按鈕連接至 GPIO0
// ==================== 物件建立 ====================
WebServer server(80); // 建立 HTTP 伺服器,監聽 80 埠
WebSocketsServer webSocket = WebSocketsServer(81); // 建立 WebSocket 伺服器,監聽 81 埠
// ==================== 全域變數 ====================
bool stateR = false; // GPIO25 (R) 狀態紀錄 (false: OFF, true: ON)
bool stateG = false; // GPIO26 (G) 狀態紀錄 (false: OFF, true: ON)
bool stateB = false; // GPIO27 (B) 狀態紀錄 (false: OFF, true: ON)
String ipHistory[5]; // 儲存最近 5 次發出 ON/OFF 控制的紀錄訊息
int historyCount = 0; // 紀錄目前已儲存的歷史訊息數量
// ==================== 輔助函式:新增控制歷史紀錄 ====================
void addHistory(String ip, String target, String action) {
String entry = "IP:" + ip + ", " + target + "|" + action;
for (int i = 4; i > 0; i--) {
ipHistory[i] = ipHistory[i - 1];
}
ipHistory[0] = entry;
if (historyCount < 5) historyCount++;
}
// ==================== 輔助函式:產生 GPIO 狀態 JSON 字串 ====================
String getStatusJSON() {
StaticJsonDocument<200> doc;
doc["GPIO25"] = stateR ? "ON" : "OFF";
doc["GPIO26"] = stateG ? "ON" : "OFF";
doc["GPIO27"] = stateB ? "ON" : "OFF";
String jsonString;
serializeJson(doc, jsonString);
return jsonString;
}
// ==================== 輔助函式:廣播最新狀態至所有 WebSocket 用戶端 ====================
void broadcastStatus() {
StaticJsonDocument<500> doc;
doc["GPIO25"] = stateR ? "ON" : "OFF";
doc["GPIO26"] = stateG ? "ON" : "OFF";
doc["GPIO27"] = stateB ? "ON" : "OFF";
JsonArray historyArray = doc.createNestedArray("history");
for (int i = 0; i < historyCount; i++) {
historyArray.add(ipHistory[i]);
}
String jsonOutput;
serializeJson(doc, jsonOutput);
webSocket.broadcastTXT(jsonOutput);
}
// ==================== HTTP 路由:提供首頁網頁 HTML ====================
void handleRoot() {
Serial.print("[HTTP] 用戶端連線,IP: ");
Serial.println(server.client().remoteIP().toString());
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>NodeMCU-32S RGB LED 控制面板</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; background-color: #121212; color: #ffffff; text-align: center; display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 10px; }
.container { width: 100%; max-width: 480px; background: #1e1e1e; padding: 20px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.6); }
h1 { color: #00e676; font-size: 20px; margin-bottom: 15px; }
.tabs { display: flex; border-bottom: 2px solid #333; margin-bottom: 15px; }
.tab-btn { flex: 1; padding: 10px; background: none; border: none; color: #888; font-size: 15px; font-weight: bold; cursor: pointer; transition: 0.3s; }
.tab-btn.active { color: #00e676; border-bottom: 3px solid #00e676; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.control-table { width: 100%; border-collapse: collapse; background: #2a2a2a; border-radius: 8px; overflow: hidden; }
.control-row { display: flex; align-items: center; border-bottom: 1px solid #3d3d3d; padding: 12px 10px; }
.control-row:last-child { border-bottom: none; }
.col-switch { flex: 1.2; display: flex; align-items: center; justify-content: center; gap: 6px; }
.col-status { flex: 1; font-size: 16px; font-weight: bold; color: #00bcd4; text-align: center; }
.btn-tag { font-size: 13px; font-weight: bold; color: #ffeb3b; width: 55px; text-align: left; }
.switch { position: relative; display: inline-block; width: 50px; height: 26px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #555; transition: .3s; border-radius: 26px; }
.slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; }
input:checked + .slider { background-color: #00e676; }
input:checked + .slider:before { transform: translateX(24px); }
.switch-text { font-size: 12px; color: #bbb; }
.history-box { background: #000000; padding: 15px; border-radius: 8px; border: 1px solid #333; min-height: 180px; text-align: left; }
.history-list { font-family: monospace; color: #00ff00; font-size: 13px; line-height: 1.8; white-space: pre-wrap; margin: 0; }
</style>
</head>
<body>
<div class="container">
<h1>RGB LED 遠端控制面板</h1>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('control')">LED 控制</button>
<button class="tab-btn" onclick="switchTab('history')">連線歷史紀錄</button>
</div>
<div id="tab-control" class="tab-content active">
<div class="control-table">
<div class="control-row">
<div class="col-switch">
<span class="switch-text">OFF</span>
<label class="switch">
<input type="checkbox" id="btnALL" onchange="toggleLED('ALL', this.checked)">
<span class="slider"></span>
</label>
<span class="switch-text">ON</span>
<span class="btn-tag">ALL</span>
</div>
<div id="msgALL" class="col-status">ALL: OFF</div>
</div>
<div class="control-row">
<div class="col-switch">
<span class="switch-text">OFF</span>
<label class="switch">
<input type="checkbox" id="btn25" onchange="toggleLED('25', this.checked)">
<span class="slider"></span>
</label>
<span class="switch-text">ON</span>
<span class="btn-tag">GPIO25</span>
</div>
<div id="msg25" class="col-status">GPIO25: OFF</div>
</div>
<div class="control-row">
<div class="col-switch">
<span class="switch-text">OFF</span>
<label class="switch">
<input type="checkbox" id="btn26" onchange="toggleLED('26', this.checked)">
<span class="slider"></span>
</label>
<span class="switch-text">ON</span>
<span class="btn-tag">GPIO26</span>
</div>
<div id="msg26" class="col-status">GPIO26: OFF</div>
</div>
<div class="control-row">
<div class="col-switch">
<span class="switch-text">OFF</span>
<label class="switch">
<input type="checkbox" id="btn27" onchange="toggleLED('27', this.checked)">
<span class="slider"></span>
</label>
<span class="switch-text">ON</span>
<span class="btn-tag">GPIO27</span>
</div>
<div id="msg27" class="col-status">GPIO27: OFF</div>
</div>
</div>
</div>
<div id="tab-history" class="tab-content">
<div class="history-box">
<div id="historyList" class="history-list">尚無控制紀錄</div>
</div>
</div>
</div>
<script>
var ws;
function switchTab(tabName) {
$(".tab-btn").removeClass("active");
$(".tab-content").removeClass("active");
if (tabName === 'control') {
$(".tab-btn:nth-child(1)").addClass("active");
$("#tab-control").addClass("active");
} else {
$(".tab-btn:nth-child(2)").addClass("active");
$("#tab-history").addClass("active");
}
}
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81/');
ws.onmessage = function(event) {
var data = JSON.parse(event.data);
updateUI(data);
};
ws.onclose = function() {
setTimeout(initWebSocket, 2000);
};
}
function updateUI(data) {
if(data.GPIO25 !== undefined) {
$("#btn25").prop("checked", data.GPIO25 === "ON");
$("#msg25").text("GPIO25: " + data.GPIO25);
}
if(data.GPIO26 !== undefined) {
$("#btn26").prop("checked", data.GPIO26 === "ON");
$("#msg26").text("GPIO26: " + data.GPIO26);
}
if(data.GPIO27 !== undefined) {
$("#btn27").prop("checked", data.GPIO27 === "ON");
$("#msg27").text("GPIO27: " + data.GPIO27);
}
var allOn = (data.GPIO25 === "ON" && data.GPIO26 === "ON" && data.GPIO27 === "ON");
$("#btnALL").prop("checked", allOn);
$("#msgALL").text("ALL: " + (allOn ? "ON" : "OFF"));
if(data.history !== undefined) {
if(data.history.length > 0) {
$("#historyList").html(data.history.join("<br>"));
} else {
$("#historyList").text("尚無控制紀錄");
}
}
}
function toggleLED(target, state) {
var stateStr = state ? "ON" : "OFF";
$.ajax({
url: "/toggle?target=" + target + "&state=" + stateStr,
type: "GET",
success: function(response) {}
});
}
$(document).ready(function() {
initWebSocket();
});
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}
// ==================== HTTP 路由:處理切換控制 API (/toggle) ====================
void handleToggle() {
String clientIP = server.client().remoteIP().toString();
if (server.hasArg("target") && server.hasArg("state")) {
String target = server.arg("target");
String stateStr = server.arg("state");
bool isOn = (stateStr == "ON");
if (target == "ALL") {
stateR = isOn;
stateG = isOn;
stateB = isOn;
digitalWrite(pinR, stateR ? HIGH : LOW);
digitalWrite(pinG, stateG ? HIGH : LOW);
digitalWrite(pinB, stateB ? HIGH : LOW);
addHistory(clientIP, "ALL", stateStr);
}
else {
int gpio = target.toInt();
if (gpio == 25) {
stateR = isOn;
digitalWrite(pinR, stateR ? HIGH : LOW);
addHistory(clientIP, "GPIO25", stateStr);
} else if (gpio == 26) {
stateG = isOn;
digitalWrite(pinG, stateG ? HIGH : LOW);
addHistory(clientIP, "GPIO26", stateStr);
} else if (gpio == 27) {
stateB = isOn;
digitalWrite(pinB, stateB ? HIGH : LOW);
addHistory(clientIP, "GPIO27", stateStr);
}
}
Serial.print("[HTTP Request] IP: ");
Serial.print(clientIP);
Serial.print(" | Target: ");
Serial.print(target);
Serial.print(" -> ");
Serial.println(stateStr);
server.send(200, "application/json", getStatusJSON());
broadcastStatus();
} else {
server.send(400, "text/plain", "Bad Request");
}
}
// ==================== WebSocket 事件處理核心 ====================
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
if (type == WStype_CONNECTED) {
IPAddress ip = webSocket.remoteIP(num);
Serial.print("[WebSocket] 用戶端 #");
Serial.print(num);
Serial.print(" 已連線,IP: ");
Serial.println(ip.toString());
broadcastStatus();
} else if (type == WStype_DISCONNECTED) {
Serial.print("[WebSocket] 用戶端 #");
Serial.print(num);
Serial.println(" 已離線");
}
}
// ==================== 程式初始化設定 (setup) ====================
void setup() {
Serial.begin(115200);
delay(100);
pinMode(pinR, OUTPUT);
pinMode(pinG, OUTPUT);
pinMode(pinB, OUTPUT);
digitalWrite(pinR, LOW);
digitalWrite(pinG, LOW);
digitalWrite(pinB, LOW);
// 設定 BOOT 按鍵為內部上拉輸入模式
pinMode(TRIGGER_PIN, INPUT_PULLUP);
WiFiManager wm;
// ----------------【開機重置判斷邏輯】----------------
if (digitalRead(TRIGGER_PIN) == LOW) {
Serial.println("\n------------------------------------------------");
Serial.println("[系統提示] 偵測到 BOOT 按鍵按下!");
Serial.println("[系統提示] 正在清除快閃記憶體 (NVS) 中的 WiFi 帳密...");
wm.resetSettings(); // 抹除儲存的 WiFi 資訊
Serial.println("[系統提示] 清除完成,即將進入 Captive Portal 配網模式");
Serial.println("------------------------------------------------\n");
delay(1000);
}
wm.setConfigPortalTimeout(180);
Serial.println("正在啟動 WiFiManager 服務...");
bool res = wm.autoConnect("ESP32-RGB-Setup");
if (!res) {
Serial.println("[WiFiManager] 連線失敗或配網逾時,系統重啟中...");
ESP.restart();
} else {
Serial.println("\nWiFi 連線成功!");
Serial.print("ESP32 區域網路 IP 位址: ");
Serial.println(WiFi.localIP()); // 印出 IP 至序列埠
}
server.on("/", handleRoot);
server.on("/toggle", handleToggle);
server.begin();
Serial.println("HTTP Web Server 服務已啟動");
webSocket.begin();
webSocket.onEvent(webSocketEvent);
Serial.println("WebSocket 服務已啟動 (Port 81)");
}
// ==================== 程式主要循環執行區塊 (loop) ====================
void loop() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("[WiFi] 偵測到連線中斷,正在嘗試重新建立連線...");
WiFi.disconnect();
WiFi.reconnect();
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\n[WiFi] 連線已重新建立!IP 位址: ");
Serial.println(WiFi.localIP());
}
server.handleClient();
webSocket.loop();
}
步驟 2:測試 Captive Portal 配網 (首次連線 / 重置測試)
用手指 按住板子上的 BOOT 按鈕(GPIO0)不放。
按一下 EN 鍵 (Reset) 重啟 ESP32。
觀察序列埠監控器,看到印出 [系統提示] 偵測到 BOOT 按鍵按下! 後放開 BOOT 鍵。
拿起手機或電腦,搜尋 WiFi 列表並連線至 ESP32-RGB-Setup。
手機將自動跳出配網視窗(若未跳出,請開啟瀏覽器輸入 192.168.4.1)。
點擊 Configure WiFi ➔ 選擇您家中的 WiFi 名稱 ➔ 輸入正確密碼 ➔ 點擊 Save。
儲存後 ESP32 會自動連線並重啟。
步驟 3:獲取 IP 與開啟控制頁面
觀察序列埠監控器,連線成功後會印出:WiFi 連線成功!
ESP32 區域網路 IP 位址: 192.168.x.x
將手機或電腦連接到與 ESP32 相同的家用 WiFi。
在瀏覽器網址列輸入序列埠看到的 IP(例如 [http://192.168.1.105](http://192.168.1.105))。
即可順利進入 RGB LED Web 控制面板!
步驟 4:測試自動連線 (情境 2)
試著按下 ESP32 板子上的 EN 鍵 (Reset) 直接重啟(不按 BOOT 鍵)。
觀察序列埠監控器,ESP32 會跳過熱點,在 3~5 秒內自動連上剛才設定的 WiFi 並印出 IP,證實設定已成功永久儲存。
提示詞
請幫我在程式碼中加入 mDNS 功能,讓我可以用 esp32-rgb.local 這種網址直接開啟網頁,不用每次都找 IP。
Code
#include <WiFi.h> // 引用 ESP32 原生 WiFi 標頭檔
#include <WebServer.h> // 引用 ESP32 原生 WebServer 標頭檔
#include <WebSocketsServer.h> // 引用第三方 WebSocketsServer 標頭檔
#include <ArduinoJson.h> // 引用第三方 ArduinoJson 標頭檔
#include <WiFiManager.h> // 引用第三方 WiFiManager 標頭檔 (by tzapu)
#include <ESPmDNS.h> // 引用 ESP32 原生 mDNS 標頭檔
// ==================== 硬體腳位設定 ====================
const int pinR = 25; // RGB LED 紅燈連接至 GPIO25
const int pinG = 26; // RGB LED 綠燈連接至 GPIO26
const int pinB = 27; // RGB LED 藍燈連接至 GPIO27
const int TRIGGER_PIN = 0; // 板載 BOOT 按鈕連接至 GPIO0
// ==================== 物件與自訂域名設定 ====================
const char* HOST_NAME = "esp32-rgb"; // 自訂 mDNS 網域名稱 (即 esp32-rgb.local)
WebServer server(80); // 建立 HTTP 伺服器,監聽 80 埠
WebSocketsServer webSocket = WebSocketsServer(81); // 建立 WebSocket 伺服器,監聽 81 埠
// ==================== 全域變數 ====================
bool stateR = false; // GPIO25 (R) 狀態紀錄 (false: OFF, true: ON)
bool stateG = false; // GPIO26 (G) 狀態紀錄 (false: OFF, true: ON)
bool stateB = false; // GPIO27 (B) 狀態紀錄 (false: OFF, true: ON)
String ipHistory[5]; // 儲存最近 5 次發出 ON/OFF 控制的紀錄訊息
int historyCount = 0; // 紀錄目前已儲存的歷史訊息數量
// ==================== 輔助函式:新增控制歷史紀錄 ====================
void addHistory(String ip, String target, String action) {
String entry = "IP:" + ip + ", " + target + "|" + action;
for (int i = 4; i > 0; i--) {
ipHistory[i] = ipHistory[i - 1];
}
ipHistory[0] = entry;
if (historyCount < 5) historyCount++;
}
// ==================== 輔助函式:產生 GPIO 狀態 JSON 字串 ====================
String getStatusJSON() {
StaticJsonDocument<200> doc;
doc["GPIO25"] = stateR ? "ON" : "OFF";
doc["GPIO26"] = stateG ? "ON" : "OFF";
doc["GPIO27"] = stateB ? "ON" : "OFF";
String jsonString;
serializeJson(doc, jsonString);
return jsonString;
}
// ==================== 輔助函式:廣播最新狀態至所有 WebSocket 用戶端 ====================
void broadcastStatus() {
StaticJsonDocument<500> doc;
doc["GPIO25"] = stateR ? "ON" : "OFF";
doc["GPIO26"] = stateG ? "ON" : "OFF";
doc["GPIO27"] = stateB ? "ON" : "OFF";
JsonArray historyArray = doc.createNestedArray("history");
for (int i = 0; i < historyCount; i++) {
historyArray.add(ipHistory[i]);
}
String jsonOutput;
serializeJson(doc, jsonOutput);
webSocket.broadcastTXT(jsonOutput);
}
// ==================== HTTP 路由:提供首頁網頁 HTML ====================
void handleRoot() {
Serial.print("[HTTP] 用戶端連線,IP: ");
Serial.println(server.client().remoteIP().toString());
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>NodeMCU-32S RGB LED 控制面板</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; background-color: #121212; color: #ffffff; text-align: center; display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 10px; }
.container { width: 100%; max-width: 480px; background: #1e1e1e; padding: 20px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.6); }
h1 { color: #00e676; font-size: 20px; margin-bottom: 15px; }
.tabs { display: flex; border-bottom: 2px solid #333; margin-bottom: 15px; }
.tab-btn { flex: 1; padding: 10px; background: none; border: none; color: #888; font-size: 15px; font-weight: bold; cursor: pointer; transition: 0.3s; }
.tab-btn.active { color: #00e676; border-bottom: 3px solid #00e676; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.control-table { width: 100%; border-collapse: collapse; background: #2a2a2a; border-radius: 8px; overflow: hidden; }
.control-row { display: flex; align-items: center; border-bottom: 1px solid #3d3d3d; padding: 12px 10px; }
.control-row:last-child { border-bottom: none; }
.col-switch { flex: 1.2; display: flex; align-items: center; justify-content: center; gap: 6px; }
.col-status { flex: 1; font-size: 16px; font-weight: bold; color: #00bcd4; text-align: center; }
.btn-tag { font-size: 13px; font-weight: bold; color: #ffeb3b; width: 55px; text-align: left; }
.switch { position: relative; display: inline-block; width: 50px; height: 26px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #555; transition: .3s; border-radius: 26px; }
.slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 3px; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%; }
input:checked + .slider { background-color: #00e676; }
input:checked + .slider:before { transform: translateX(24px); }
.switch-text { font-size: 12px; color: #bbb; }
.history-box { background: #000000; padding: 15px; border-radius: 8px; border: 1px solid #333; min-height: 180px; text-align: left; }
.history-list { font-family: monospace; color: #00ff00; font-size: 13px; line-height: 1.8; white-space: pre-wrap; margin: 0; }
</style>
</head>
<body>
<div class="container">
<h1>RGB LED 遠端控制面板</h1>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('control')">LED 控制</button>
<button class="tab-btn" onclick="switchTab('history')">連線歷史紀錄</button>
</div>
<div id="tab-control" class="tab-content active">
<div class="control-table">
<div class="control-row">
<div class="col-switch">
<span class="switch-text">OFF</span>
<label class="switch">
<input type="checkbox" id="btnALL" onchange="toggleLED('ALL', this.checked)">
<span class="slider"></span>
</label>
<span class="switch-text">ON</span>
<span class="btn-tag">ALL</span>
</div>
<div id="msgALL" class="col-status">ALL: OFF</div>
</div>
<div class="control-row">
<div class="col-switch">
<span class="switch-text">OFF</span>
<label class="switch">
<input type="checkbox" id="btn25" onchange="toggleLED('25', this.checked)">
<span class="slider"></span>
</label>
<span class="switch-text">ON</span>
<span class="btn-tag">GPIO25</span>
</div>
<div id="msg25" class="col-status">GPIO25: OFF</div>
</div>
<div class="control-row">
<div class="col-switch">
<span class="switch-text">OFF</span>
<label class="switch">
<input type="checkbox" id="btn26" onchange="toggleLED('26', this.checked)">
<span class="slider"></span>
</label>
<span class="switch-text">ON</span>
<span class="btn-tag">GPIO26</span>
</div>
<div id="msg26" class="col-status">GPIO26: OFF</div>
</div>
<div class="control-row">
<div class="col-switch">
<span class="switch-text">OFF</span>
<label class="switch">
<input type="checkbox" id="btn27" onchange="toggleLED('27', this.checked)">
<span class="slider"></span>
</label>
<span class="switch-text">ON</span>
<span class="btn-tag">GPIO27</span>
</div>
<div id="msg27" class="col-status">GPIO27: OFF</div>
</div>
</div>
</div>
<div id="tab-history" class="tab-content">
<div class="history-box">
<div id="historyList" class="history-list">尚無控制紀錄</div>
</div>
</div>
</div>
<script>
var ws;
function switchTab(tabName) {
$(".tab-btn").removeClass("active");
$(".tab-content").removeClass("active");
if (tabName === 'control') {
$(".tab-btn:nth-child(1)").addClass("active");
$("#tab-control").addClass("active");
} else {
$(".tab-btn:nth-child(2)").addClass("active");
$("#tab-history").addClass("active");
}
}
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81/');
ws.onmessage = function(event) {
var data = JSON.parse(event.data);
updateUI(data);
};
ws.onclose = function() {
setTimeout(initWebSocket, 2000);
};
}
function updateUI(data) {
if(data.GPIO25 !== undefined) {
$("#btn25").prop("checked", data.GPIO25 === "ON");
$("#msg25").text("GPIO25: " + data.GPIO25);
}
if(data.GPIO26 !== undefined) {
$("#btn26").prop("checked", data.GPIO26 === "ON");
$("#msg26").text("GPIO26: " + data.GPIO26);
}
if(data.GPIO27 !== undefined) {
$("#btn27").prop("checked", data.GPIO27 === "ON");
$("#msg27").text("GPIO27: " + data.GPIO27);
}
var allOn = (data.GPIO25 === "ON" && data.GPIO26 === "ON" && data.GPIO27 === "ON");
$("#btnALL").prop("checked", allOn);
$("#msgALL").text("ALL: " + (allOn ? "ON" : "OFF"));
if(data.history !== undefined) {
if(data.history.length > 0) {
$("#historyList").html(data.history.join("<br>"));
} else {
$("#historyList").text("尚無控制紀錄");
}
}
}
function toggleLED(target, state) {
var stateStr = state ? "ON" : "OFF";
$.ajax({
url: "/toggle?target=" + target + "&state=" + stateStr,
type: "GET",
success: function(response) {}
});
}
$(document).ready(function() {
initWebSocket();
});
</script>
</body>
</html>
)rawliteral";
server.send(200, "text/html", html);
}
// ==================== HTTP 路由:處理切換控制 API (/toggle) ====================
void handleToggle() {
String clientIP = server.client().remoteIP().toString();
if (server.hasArg("target") && server.hasArg("state")) {
String target = server.arg("target");
String stateStr = server.arg("state");
bool isOn = (stateStr == "ON");
if (target == "ALL") {
stateR = isOn;
stateG = isOn;
stateB = isOn;
digitalWrite(pinR, stateR ? HIGH : LOW);
digitalWrite(pinG, stateG ? HIGH : LOW);
digitalWrite(pinB, stateB ? HIGH : LOW);
addHistory(clientIP, "ALL", stateStr);
}
else {
int gpio = target.toInt();
if (gpio == 25) {
stateR = isOn;
digitalWrite(pinR, stateR ? HIGH : LOW);
addHistory(clientIP, "GPIO25", stateStr);
} else if (gpio == 26) {
stateG = isOn;
digitalWrite(pinG, stateG ? HIGH : LOW);
addHistory(clientIP, "GPIO26", stateStr);
} else if (gpio == 27) {
stateB = isOn;
digitalWrite(pinB, stateB ? HIGH : LOW);
addHistory(clientIP, "GPIO27", stateStr);
}
}
Serial.print("[HTTP Request] IP: ");
Serial.print(clientIP);
Serial.print(" | Target: ");
Serial.print(target);
Serial.print(" -> ");
Serial.println(stateStr);
server.send(200, "application/json", getStatusJSON());
broadcastStatus();
} else {
server.send(400, "text/plain", "Bad Request");
}
}
// ==================== WebSocket 事件處理核心 ====================
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) {
if (type == WStype_CONNECTED) {
IPAddress ip = webSocket.remoteIP(num);
Serial.print("[WebSocket] 用戶端 #");
Serial.print(num);
Serial.print(" 已連線,IP: ");
Serial.println(ip.toString());
broadcastStatus();
} else if (type == WStype_DISCONNECTED) {
Serial.print("[WebSocket] 用戶端 #");
Serial.print(num);
Serial.println(" 已離線");
}
}
// ==================== 程式初始化設定 (setup) ====================
void setup() {
Serial.begin(115200);
delay(100);
pinMode(pinR, OUTPUT);
pinMode(pinG, OUTPUT);
pinMode(pinB, OUTPUT);
digitalWrite(pinR, LOW);
digitalWrite(pinG, LOW);
digitalWrite(pinB, LOW);
pinMode(TRIGGER_PIN, INPUT_PULLUP);
WiFiManager wm;
// ----------------【開機重置判斷邏輯】----------------
if (digitalRead(TRIGGER_PIN) == LOW) {
Serial.println("\n------------------------------------------------");
Serial.println("[系統提示] 偵測到 BOOT 按鍵按下!");
Serial.println("[系統提示] 正在清除快閃記憶體 (NVS) 中的 WiFi 帳密...");
wm.resetSettings();
Serial.println("[系統提示] 清除完成,即將進入 Captive Portal 配網模式");
Serial.println("------------------------------------------------\n");
delay(1000);
}
wm.setConfigPortalTimeout(180);
Serial.println("正在啟動 WiFiManager 服務...");
bool res = wm.autoConnect("ESP32-RGB-Setup");
if (!res) {
Serial.println("[WiFiManager] 連線失敗或配網逾時,系統重啟中...");
ESP.restart();
} else {
Serial.println("\nWiFi 連線成功!");
Serial.print("ESP32 區域網路 IP 位址: ");
Serial.println(WiFi.localIP());
}
// ----------------【啟動 mDNS 服務】----------------
if (MDNS.begin(HOST_NAME)) {
Serial.print("mDNS 服務已啟動!網域: http://");
Serial.print(HOST_NAME);
Serial.println(".local");
// 註冊 HTTP 服務廣播
MDNS.addService("http", "tcp", 80);
} else {
Serial.println("mDNS 服務啟動失敗!");
}
server.on("/", handleRoot);
server.on("/toggle", handleToggle);
server.begin();
Serial.println("HTTP Web Server 服務已啟動");
webSocket.begin();
webSocket.onEvent(webSocketEvent);
Serial.println("WebSocket 服務已啟動 (Port 81)");
}
// ==================== 程式主要循環執行區塊 (loop) ====================
void loop() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("[WiFi] 偵測到連線中斷,正在嘗試重新建立連線...");
WiFi.disconnect();
WiFi.reconnect();
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\n[WiFi] 連線已重新建立!IP 位址: ");
Serial.println(WiFi.localIP());
}
server.handleClient();
webSocket.loop();
}
測試 mDNS 網址連線
燒錄完成並確保 ESP32 已連上家用 WiFi。
確保手機或電腦連接在同一台 WiFi 路由器下。
開啟瀏覽器,直接在網址列輸入:http://esp32-rgb.local