提示詞
依下列需求,寫一個 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: G2F1
-PASSWORD: yyyymmmm
##連網成功後 會檢查網路連線,當網路連線失敗,重新自動建立連線
##連網成功後,建立 web server 服務,產生一個單頁響應式網頁
-整合 HTML, CSS, jQuery(使用非同步 Ajax 請求)等語法
-網頁內容提供3個單一滑桿拉條,輸出 PWM 控制3個LED燈亮度
-滑桿拉條樣式:
--往左數值愈小往右數值愈大
--滑桿拉條上方標示作用腳位與拉動滑桿時的即時PWM值(如:GPIO2|PWM:256)
--滑桿拉條的左右兩端,以文字明確標示PWM最小值與最大值
-於按鈕動作結束後,讀取各GPIO腳位的PWM值,顯示在網頁上,讓使用者可以明確知道每個GPIO的狀態。
--網頁上以文字樣式顯示每個LED的回應訊息,訊息格式如下
目前狀態
GPIO:25|PWM:122
GPIO:26|PWM:122
GPIO:27|PWM:122
--顯示位置:所有滑桿拉條下方置中
--回傳 JSON 格式數據(例如:{"GPIO25":256, "GPIO26":128, "GPIO27":0})。
-網頁增加一個回應框,顯示最近五次發出PWM的連線IP訊息,位置在所有切換滑桿下方,顯示格式如
IP:xxxx|GPIO:25|PWM:128
IP:xxxx|GPIO:26|PWM:0
...
-網頁設計力求簡潔美觀、顏色活潑吸引人,配色對比強,顯示文字以繁體中文或英文適當呈現
-網頁內容中提供完整詳細操作指導
##多使用者裝置同步
-使用 websocket 連線方式
##程式運作將必要的互動追蹤訊息 完整輸出至序列埠
-速率 115200
-序列埠輸出訊息,需簡潔明確,方便開發者追蹤除錯
-HTTP請求部分,提供連線IP,作用GPIO,PWM請求,JSON 回應
##程式碼逐行以繁體中文註解
#程式安裝指導說明
##程式若使用第三方函式庫,須提供函式庫下載與安裝的詳細操作
##提供程式燒錄使用的詳細操作
Code
// 載入 ESP32 Wi-Fi 基礎函式庫,用於處理 Station 模式連線
#include <WiFi.h>
// 載入非同步 TCP 底層通訊函式庫
#include <AsyncTCP.h>
// 載入非同步 Web Server 與 WebSocket 服務函式庫
#include <ESPAsyncWebServer.h>
// 載入 ArduinoJson 函式庫,用於輕鬆解析與構建 JSON 數據
#include <ArduinoJson.h>
// 定義 Wi-Fi 連線 SSID 名稱
const char* ssid = "G2F1";
// 定義 Wi-Fi 連線密碼
const char* password = "yyyymmmm";
// 定義 RGB LED 的腳位 (根據硬體接線需求)
const int pinR = 25; // 紅光 GPIO 腳位
const int pinG = 26; // 綠光 GPIO 腳位
const int pinB = 27; // 藍光 GPIO 腳位
// 紀錄當前各 GPIO 腳位的 PWM 輸出數值 (0 ~ 255)
int pwmR = 0; // 紅光初始 PWM 值
int pwmG = 0; // 綠光初始 PWM 值
int pwmB = 0; // 藍光初始 PWM 值
// 定義歷史紀錄結構體,用於儲存近期連線紀錄
struct HistoryRecord {
String ip; // 發出請求的用戶端 IP 位址
int gpio; // 作用的 GPIO 腳位
int pwm; // 發出的 PWM 設定值
};
// 建立固定大小為 5 的陣列,儲存最近 5 次 PWM 操作紀錄
HistoryRecord historyLog[5];
// 紀錄當前歷史紀錄的總筆數 (上限 5 筆)
int historyCount = 0;
// 建立運行於 80 通訊埠的非同步 Web Server 物件
AsyncWebServer server(80);
// 建立運行於 "/ws" 路徑的 WebSocket 服務物件
AsyncWebSocket ws("/ws");
// 前端單頁網頁 HTML/CSS/JavaScript 原始碼 (使用 PROGMEM 儲存於 ESP32 Flash 記憶體)
const char index_html[] PROGMEM = 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 RGB LED PWM 控制儀表板</title>
<!-- 引入 jQuery CDN 函式庫以進行 Ajax 與 DOM 操作 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
/* CSS 全局樣式設定 */
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
body { background: linear-gradient(135deg, #1f1c2c, #928dab); color: #fff; display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 20px; }
.container { background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); border-radius: 16px; padding: 30px; max-width: 600px; width: 100%; box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); border: 1px solid rgba(255, 255, 255, 0.18); }
h1 { text-align: center; margin-bottom: 20px; font-size: 24px; color: #00f2fe; text-shadow: 0 0 10px rgba(0,242,254,0.5); }
/* 操作說明卡片 */
.instructions { background: rgba(0, 0, 0, 0.2); border-left: 4px solid #4facfe; padding: 12px; border-radius: 6px; font-size: 14px; margin-bottom: 25px; line-height: 1.5; }
/* 滑桿區域樣式 */
.slider-group { margin-bottom: 20px; background: rgba(0, 0, 0, 0.15); padding: 15px; border-radius: 10px; }
.slider-header { display: flex; justify-content: space-between; font-weight: bold; margin-bottom: 8px; font-size: 16px; }
.slider-header.red { color: #ff6b6b; }
.slider-header.green { color: #51cf66; }
.slider-header.blue { color: #339af0; }
.slider-wrapper { display: flex; align-items: center; gap: 10px; }
.limit-label { font-size: 12px; color: #ccc; }
input[type=range] { -webkit-appearance: none; width: 100%; height: 8px; border-radius: 5px; background: #444; outline: none; }
input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; border-radius: 50%; background: #00f2fe; cursor: pointer; box-shadow: 0 0 8px #00f2fe; }
/* 目前狀態顯示區塊 (置於滑桿下方置中) */
.status-box { text-align: center; background: rgba(0, 0, 0, 0.3); padding: 15px; border-radius: 10px; margin-top: 20px; border: 1px solid rgba(255,255,255,0.1); }
.status-title { font-size: 18px; font-weight: bold; color: #fcc419; margin-bottom: 10px; }
.status-text { font-family: 'Courier New', Courier, monospace; font-size: 15px; color: #e9ecef; line-height: 1.6; white-space: pre-line; }
/* 歷史紀錄顯示框 */
.history-box { margin-top: 20px; background: rgba(0, 0, 0, 0.25); padding: 15px; border-radius: 10px; }
.history-title { font-size: 16px; font-weight: bold; color: #38d9a9; margin-bottom: 8px; }
.history-list { font-family: 'Courier New', Courier, monospace; font-size: 13px; color: #ced4da; line-height: 1.5; }
</style>
</head>
<body>
<div class="container">
<h1>ESP32 RGB PWM 控制儀表板</h1>
<!-- 詳細操作指導 -->
<div class="instructions">
<strong>📖 操作指導說明:</strong><br>
1. 拖曳下方 3 個滑桿可分別調整紅 (GPIO25)、綠 (GPIO26)、藍 (GPIO27) LED 亮度。<br>
2. <strong>放開滑桿按鈕後</strong>,會自動將最新 PWM 設定傳送給 ESP32,並更新下方狀態與歷史紀錄。<br>
3. 支援多裝置 WebSocket 即時同步,多台手機/電腦開頁面皆可同步顯示最新狀態。
</div>
<!-- Red 滑桿 (GPIO25) -->
<div class="slider-group">
<div class="slider-header red">
<span>作用腳位: GPIO25</span>
<span id="label25">GPIO:25|PWM:0</span>
</div>
<div class="slider-wrapper">
<span class="limit-label">0</span>
<input type="range" id="slider25" min="0" max="255" value="0" oninput="updateLabel(25, this.value)" onchange="sendPWM(25, this.value)">
<span class="limit-label">255</span>
</div>
</div>
<!-- Green 滑桿 (GPIO26) -->
<div class="slider-group">
<div class="slider-header green">
<span>作用腳位: GPIO26</span>
<span id="label26">GPIO:26|PWM:0</span>
</div>
<div class="slider-wrapper">
<span class="limit-label">0</span>
<input type="range" id="slider26" min="0" max="255" value="0" oninput="updateLabel(26, this.value)" onchange="sendPWM(26, this.value)">
<span class="limit-label">255</span>
</div>
</div>
<!-- Blue 滑桿 (GPIO27) -->
<div class="slider-group">
<div class="slider-header blue">
<span>作用腳位: GPIO27</span>
<span id="label27">GPIO:27|PWM:0</span>
</div>
<div class="slider-wrapper">
<span class="limit-label">0</span>
<input type="range" id="slider27" min="0" max="255" value="0" oninput="updateLabel(27, this.value)" onchange="sendPWM(27, this.value)">
<span class="limit-label">255</span>
</div>
</div>
<!-- 目前狀態顯示區 (所有滑桿下方置中) -->
<div class="status-box">
<div class="status-title">目前狀態</div>
<div class="status-text" id="statusMessage">
GPIO:25|PWM:0
GPIO:26|PWM:0
GPIO:27|PWM:0
</div>
</div>
<!-- 最近 5 次連線與 PWM 發出訊息紀錄框 -->
<div class="history-box">
<div class="history-title">最近五次連線 IP 與 PWM 訊息:</div>
<div class="history-list" id="historyList">尚無連線紀錄</div>
</div>
</div>
<script>
var socket; // WebSocket 物件
// 頁面載入完成後初始化 WebSocket 與 Ajax 取得初始狀態
$(document).ready(function() {
initWebSocket();
fetchStatus();
});
// 初始化 WebSocket 連線
function initWebSocket() {
var wsUri = "ws://" + window.location.host + "/ws";
socket = new WebSocket(wsUri);
// WebSocket 收到伺服器訊息時觸發
socket.onmessage = function(event) {
var data = JSON.parse(event.data);
updateUI(data);
};
//WebSocket 連線中斷時自動重連
socket.onclose = function() {
setTimeout(initWebSocket, 2000);
};
}
// 拖動滑桿時即時更新上方文字標籤
function updateLabel(gpio, val) {
$("#label" + gpio).text("GPIO:" + gpio + "|PWM:" + val);
}
// 滑桿放開 (按鈕動作結束) 時,透過 Ajax 發送 HTTP PWM 設定請求
function sendPWM(gpio, val) {
$.get("/setPWM?gpio=" + gpio + "&pwm=" + val, function(data) {
// UI 會經由 WebSocket 自動即時同步
});
}
// 使用 Ajax 取得 ESP32 目前狀態 (回傳 JSON)
function fetchStatus() {
$.getJSON("/status", function(data) {
updateUI(data);
});
}
// 更新 UI 介面數據 (包含滑桿位置、目前狀態與歷史紀錄)
function updateUI(data) {
// 更新滑桿與數值顯示
if(data.GPIO25 !== undefined) {
$("#slider25").val(data.GPIO25);
$("#label25").text("GPIO:25|PWM:" + data.GPIO25);
}
if(data.GPIO26 !== undefined) {
$("#slider26").val(data.GPIO26);
$("#label26").text("GPIO:26|PWM:" + data.GPIO26);
}
if(data.GPIO27 !== undefined) {
$("#slider27").val(data.GPIO27);
$("#label27").text("GPIO:27|PWM:" + data.GPIO27);
}
// 更新「目前狀態」顯示格式 (符合需求格式)
var statusStr = "GPIO:25|PWM:" + (data.GPIO25 !== undefined ? data.GPIO25 : $("#slider25").val()) + "\n" +
"GPIO:26|PWM:" + (data.GPIO26 !== undefined ? data.GPIO26 : $("#slider26").val()) + "\n" +
"GPIO:27|PWM:" + (data.GPIO27 !== undefined ? data.GPIO27 : $("#slider27").val());
$("#statusMessage").text(statusStr);
// 更新最近 5 次歷史紀錄框
if(data.history && data.history.length > 0) {
var historyHtml = "";
data.history.forEach(function(item) {
historyHtml += "IP:" + item.ip + "|GPIO:" + item.gpio + "|PWM:" + item.pwm + "<br>";
});
$("#historyList").html(historyHtml);
}
}
</script>
</body>
</html>
)rawliteral";
// 將當前狀態與歷史紀錄構建為 JSON 字串
String buildStatusJson() {
StaticJsonDocument<1024> doc; // 建立 JSON 文件物件
doc["GPIO25"] = pwmR; // 填入 GPIO25 PWM 值
doc["GPIO26"] = pwmG; // 填入 GPIO26 PWM 值
doc["GPIO27"] = pwmB; // 填入 GPIO27 PWM 值
JsonArray historyArr = doc.createNestedArray("history"); // 建立歷史紀錄 JSON 陣列
for (int i = 0; i < historyCount; i++) {
JsonObject obj = historyArr.createNestedObject();
obj["ip"] = historyLog[i].ip; // 紀錄用戶 IP
obj["gpio"] = historyLog[i].gpio; // 紀錄作用 GPIO
obj["pwm"] = historyLog[i].pwm; // 紀錄設定 PWM
}
String output;
serializeJson(doc, output); // 將 JSON 物件轉為字串
return output; // 回傳 JSON 字串
}
// 新增一筆 PWM 操作紀錄至歷史紀錄陣列 (維持最近 5 筆)
void addHistory(String ip, int gpio, int pwm) {
if (historyCount < 5) { // 若紀錄未滿 5 筆,直接新增
historyLog[historyCount].ip = ip;
historyLog[historyCount].gpio = gpio;
historyLog[historyCount].pwm = pwm;
historyCount++;
} else { // 若已滿 5 筆,整體前移一位並將最新筆放置於末端
for (int i = 0; i < 4; i++) {
historyLog[i] = historyLog[i + 1];
}
historyLog[4].ip = ip;
historyLog[4].gpio = gpio;
historyLog[4].pwm = pwm;
}
}
// 廣播最新 JSON 狀態給所有已連線的 WebSocket 用戶端
void broadcastStatus() {
String jsonStr = buildStatusJson(); // 取得 JSON 字串
ws.textAll(jsonStr); // 向全體連線者發送文字訊息
}
// 處理 WebSocket 事件回調函式
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
if (type == WS_EVT_CONNECT) { // 當有用戶端建立 WebSocket 連線時
Serial.printf("[WebSocket] 用戶端 #%u 已連線,來自 IP: %s\n", client->id(), client->remoteIP().toString().c_str());
client->text(buildStatusJson()); // 連線建立後立即傳送目前最新狀態
} else if (type == WS_EVT_DISCONNECT) { // 當用戶端斷開連線時
Serial.printf("[WebSocket] 用戶端 #%u 已離線\n", client->id());
}
}
// 檢查並保持 Wi-Fi 連線 (斷線時自動重新連線)
void checkWiFiConnection() {
if (WiFi.status() != WL_CONNECTED) { // 若檢測到 Wi-Fi 斷開
Serial.println("[Wi-Fi] 網路連線中斷,正在嘗試重新建立連線...");
WiFi.disconnect(); // 斷開現有狀態
WiFi.begin(ssid, password); // 重新連線
int timeout = 0;
while (WiFi.status() != WL_CONNECTED && timeout < 20) { // 等待最多 10 秒
delay(500);
Serial.print(".");
timeout++;
}
if (WiFi.status() == WL_CONNECTED) { // 連線成功
Serial.println("\n[Wi-Fi] 重新連線成功!");
Serial.print("[Wi-Fi] 新 IP 位址: ");
Serial.println(WiFi.localIP());
} else { // 連線失敗
Serial.println("\n[Wi-Fi] 重新連線失敗,將於下一次迴圈再次嘗試。");
}
}
}
// ESP32 初始化設定區塊
void setup() {
Serial.begin(115200); // 初始化序列埠,傳輸速率設定為 115200
delay(1000); // 簡短延遲等待序列埠穩定
Serial.println("\n=== ESP32 RGB PWM Web 控制系統啟動中 ===");
// 設定 ESP32 LEDC PWM 通道 (適配 ESP32 Core 3.1.1 API)
ledcAttach(pinR, 5000, 8); // 設定 GPIO25 為 5000Hz, 8-bit 解析度 (PWM 0~255)
ledcAttach(pinG, 5000, 8); // 設定 GPIO26 為 5000Hz, 8-bit 解析度 (PWM 0~255)
ledcAttach(pinB, 5000, 8); // 設定 GPIO27 為 5000Hz, 8-bit 解析度 (PWM 0~255)
// 初始輸出 PWM 值 0 (關閉所有 LED)
ledcWrite(pinR, pwmR);
ledcWrite(pinG, pwmG);
ledcWrite(pinB, pwmB);
// 開始建立 Wi-Fi Station Mode 連線
WiFi.mode(WIFI_STA); // 設定為 Station 模式
WiFi.begin(ssid, password); // 嘗試連接指定 AP
Serial.printf("[Wi-Fi] 正在連線至 SSID: %s ", ssid);
while (WiFi.status() != WL_CONNECTED) { // 等待 Wi-Fi 連線成功
delay(500);
Serial.print(".");
}
Serial.println("\n[Wi-Fi] 網路連線成功!");
Serial.print("[Wi-Fi] 取得 IP 位址: ");
Serial.println(WiFi.localIP()); // 輸出 IP 至序列埠
// 設定 WebSocket 事件處理常式
ws.onEvent(onWsEvent);
server.addHandler(&ws); // 將 WebSocket 服務註冊至 Web Server
// 處理首頁 GET 請求,回傳單頁 RWD 網頁
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send_P(200, "text/html", index_html);
});
// 處理取得 JSON 狀態請求 (/status)
server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) {
String jsonResponse = buildStatusJson();
request->send(200, "application/json", jsonResponse);
});
// 處理 PWM 設定請求 (/setPWM?gpio=XX&pwm=YY)
server.on("/setPWM", HTTP_GET, [](AsyncWebServerRequest *request) {
String clientIP = request->client()->remoteIP().toString(); // 取得發出請求的用戶端 IP
if (request->hasParam("gpio") && request->hasParam("pwm")) { // 檢查參數是否存在
int gpio = request->getParam("gpio")->value().toInt(); // 解析目標 GPIO
int pwm = request->getParam("pwm")->value().toInt(); // 解析目標 PWM 值
// 限制 PWM 數值在合法範圍 0 ~ 255
pwm = constrain(pwm, 0, 255);
// 根據目標 GPIO 寫入 PWM 輸出並更新變數
if (gpio == pinR) {
pwmR = pwm;
ledcWrite(pinR, pwmR);
} else if (gpio == pinG) {
pwmG = pwm;
ledcWrite(pinG, pwmG);
} else if (gpio == pinB) {
pwmB = pwm;
ledcWrite(pinB, pwmB);
}
// 新增連線歷史紀錄
addHistory(clientIP, gpio, pwm);
// 構建回傳 JSON 格式數據 (例如: {"GPIO25":256, "GPIO26":128, "GPIO27":0})
StaticJsonDocument<200> resDoc;
resDoc["GPIO25"] = pwmR;
resDoc["GPIO26"] = pwmG;
resDoc["GPIO27"] = pwmB;
String responseJson;
serializeJson(resDoc, responseJson);
// 序列埠明確輸出 HTTP 請求與 JSON 回應紀錄 (方便開發者除錯)
Serial.printf("[HTTP/WS Request] 連線 IP: %s | 作用 GPIO: %d | PWM 請求: %d | JSON 回應: %s\n",
clientIP.c_str(), gpio, pwm, responseJson.c_str());
// 回傳 JSON 響應給請求的 Ajax
request->send(200, "application/json", responseJson);
// 透過 WebSocket 將最新狀態廣播給所有線上的多使用者裝置
broadcastStatus();
} else {
// 參數不足回傳 400 錯誤
request->send(400, "text/plain", "Bad Request: Missing parameters");
}
});
// 啟動 Web Server
server.begin();
Serial.println("[Web Server] Web Server 與 WebSocket 服務已啟動!");
}
// 主程式重複執行迴圈
void loop() {
// 檢查 Wi-Fi 連線狀態,若斷線自動嘗試重新建立連線
checkWiFiConnection();
// 清理無效的 WebSocket 客戶端連線記憶體
ws.cleanupClients();
// 主迴圈適當延遲,維護系統穩定性
delay(100);
}
作者: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程式庫
提示詞
Code
提示詞
Code