第15周:个人作业 - 智幻走马灯Web控制界面设计与实现
和 AI 讨论需求
这周作业复杂度较高,所以从项目的一开始,我便尝试使用 AI 工具来确定项目方案。我日常使用的的是 Claude。
首先,我向 Claude 3.7 提供了尽可能详细的背景资料,并描述了初步的需求。
我给 Claude 提供了最终项目的介绍,week10,week11 的个人作业的 md 文档,以及我写的第 15 周的课程讲义的文本,以便让 Claude 了解项目所需的背景资料。然后通过提示词进一步提出了要求:
“我给你提供了我的 Fab Academy 第15周的课程讲义,里面你可以看到有关个人作业的要求。 然后我还给你提供了我的 Final Project 的目前版本的文档,还有第10周和第11周的个人作业文档。我希望能在第10周和第11周作业的基础上,增加有关软件控制的功能,并能作为我的 Final Project 的一部分。 请先充分理解这些背景资料,然后使用中文与独立的 markdown 文档给出系统目标,系统架构(硬件架构和软件架构分开说明,使用文本绘图语言),技术选型与实现方案。编程 部分先不要展开,我们先就项目的关键问题进行讨论”
Claude 随后给出了非常长的详细的方案说明,并给了 A、B、C 3 个不同复杂程度的方案,我意识到 AI 的完美主义倾向会把简单需求弄成一个需要数周时间的复杂项目。我保存了其中的一个方案文档,新开了一个聊天并向 Claude 提出简化项目的要求。
我的简化需求的提示词如下:
第15周是我作业的需求,第10和第11周的文档是参考,请先充分理解第15周的文档,目前第15周的规划我觉得有点复杂,我们只有1天时间。请尽量保持简洁,达成要求即可,避免随意扩展功能。请帮我修改第15周的需文档,取消4个阶段的规划,直接补全完整项目所需文档程序。我希望能按你的文档完成整个项目的部署和测试。
下面的文档主体包括代码部分基本都是 Claude 一次性输出的,我在实际操作的过程,做了小范围的修改,比如添加插图,修改或补全说明等。
项目介绍
本周个人项目的主要目标是基于已有的硬件设计,构建一个直观、功能完善的软件界面,实现对智幻走马灯的远程控制与监控。该系统将整合第10周的手势控制系统,通过 Web 应用的形式提供更加丰富的交互体验,并为最终项目奠定软件基础。
项目核心目标:
- 设计并实现基于 Web 的用户界面,用于控制智幻走马灯
- 整合已有的手势控制功能
- 提供 LED 状态可视化与反馈
- 实现系统状态监控
技术选择与理由
核心技术选择
技术层面 | 选择方案 | 选择理由 |
---|---|---|
服务器端 | ESP32C3 + Arduino + ESPAsyncWebServer | 1. 与现有硬件完全兼容 2. 资源占用小,适合嵌入式设备 3. 已在第11周验证过 ESP32C3 的网络能力 |
前端框架 | HTML + CSS + 少量 JavaScript | 1. 轻量级,适合 ESP32C3 存储容量有限的情况 2. 简单直观,开发效率高 3. 跨平台兼容性好 |
通信协议 | RESTful API | 1. 简单易用,适合基本控制功能 2. 符合 Web 标准,便于扩展 3. 开发与调试简便 |
数据存储 | SPIFFS | 1. ESP32C3 内置,无需额外硬件 2. 支持存储网页文件 3. Arduino 生态系统支持良好 |
技术选择详细说明
服务器端技术
选择在 ESP32C3 上直接实现 Web 服务器,而不是使用外部服务器或云平台的原因:
- 独立性:系统可以在无互联网环境下工作,只需本地 Wi-Fi 网络。
- 低延迟:控制指令直接在本地网络传输,无需经过互联网。
- 成本效益:无需额外硬件设备或云服务订阅。
- 与现有系统整合:可以直接扩展第11周已实现的网络功能。
ESP32C3 虽然资源有限(SRAM 仅 320KB),但通过精心优化的 Web 界面和资源压缩,完全可以实现所需功能。ESPAsyncWebServer 库支持异步请求处理,能够在处理 Web 请求的同时保持对手势传感器的响应。
前端技术
选择基于 HTML + CSS + 少量 JavaScript 开发前端界面的理由:
- 轻量级:最小化资源占用,适合 ESP32C3 的存储容量
- 开发效率:简单直观的技术栈,便于快速实现和调试
- 跨平台兼容:同一套界面可在手机、平板和电脑上使用
- 易于维护:简单的代码结构,便于理解和修改
避免使用 React 或 Vue 等复杂框架,因为这些框架虽然功能强大,但会占用较多存储空间,且对于简单控制界面来说过于复杂。
系统架构设计
整体架构
系统采用典型的客户端-服务器架构,分为三个主要层次:
这张草图是我要求 Claude 用 Mermaid 图表语言绘制
软件模块详细设计
服务器端模块(ESP32C3)
模块 | 功能描述 | 技术实现 |
---|---|---|
Web 服务器 | 提供静态文件服务 处理 RESTful API 请求 | ESPAsyncWebServer SPIFFS 文件系统 |
控制逻辑层 | 处理 LED 控制指令 响应手势识别事件 管理系统状态 | Arduino C++ 状态管理逻辑 |
数据管理 | 存储 LED 状态 记录系统状态 | 内存变量 可选的 SPIFFS 持久化 |
硬件抽象层 | LED 控制 手势传感器接口 | 直接 GPIO 控制 I2C 通信 |
前端模块(浏览器)
模块 | 功能描述 | 技术实现 |
---|---|---|
控制面板 | LED 亮灯数量控制 LED 全开/全关控制 | HTML 表单元素 JavaScript 事件处理 |
状态显示 | LED 状态可视化 设备连接状态 | CSS 样式 JavaScript DOM 更新 |
通信服务 | RESTful API 调用 状态轮询 | Fetch API JavaScript 定时器 |
接口设计
RESTful API 设计
LED 控制接口
接口路径 | 方法 | 功能描述 | 请求参数 | 响应数据 |
---|---|---|---|---|
/api/led/status | GET | 获取 LED 状态 | 无 | 亮灯数量(0-6) |
/api/led/control | POST | 控制LED | count : 亮灯数量 action : "all_on"/"all_off" | 成功/失败状态 |
示例请求和响应:
GET /api/led/status
// 响应
{
"count": 3
}
POST /api/led/control
// 请求 - 设置亮灯数量
count=4
// 或
// 请求 - 全部点亮
action=all_on
// 响应
{
"success": true,
"message": "LED数量已更新"
}
系统状态接口
接口路径 | 方法 | 功能描述 | 请求参数 | 响应数据 |
---|---|---|---|---|
/api/system/info | GET | 获取系统信息 | 无 | 系统名称、版本、IP地址、连接状态 |
示例响应:
GET /api/system/info
{
"name": "SmartLantern",
"version": "1.0.0",
"ip": "192.168.1.100",
"connected": true
}
用户界面设计
主界面布局
基于简化的界面设计,专注于核心功能和直观操作:
- 设备状态区:
- 显示当前设备连接状态(通过颜色编码:绿色表示已连接,红色表示未连接)。
- 显示设备IP地址。
- 位于界面顶部,便于快速查看。
- LED控制区:
- 6个独立LED状态指示器(对应6个物理LED)。
- 使用颜色区分开启/关闭状态(绿色表示开启,灰色表示关闭)。
- "增加"和"减少"按钮直接控制亮灯数量。
- "全开"和"全关"按钮提供快捷操作。
- 符合第10周手势控制的操作逻辑(上滑增加,下滑减少)。
控制面板功能
直观简洁的控制功能设计:
- LED 控制:
- 直观的 LED 指示器显示当前亮灯状态。
- "增加"和"减少"按钮控制亮灯数量。
- "全开"和"全关"按钮提供一键操作。
- 每次操作都有视觉反馈。
- 状态反馈:
- 连接状态指示器提供网络连接反馈。
- IP 地址显示便于多设备环境下识别。
- 定期自动刷新状态,保持界面同步。
界面线框图
这张草图是我要求 Claude 用 Mermaid 图表语言绘制
实现代码
主项目文件夹
/smart_lantern_control/
├── smart_lantern_control.ino # Arduino主程序文件
├── data/ # SPIFFS数据目录(网页文件)
│ └── index.html # Web界面HTML文件
└── libraries/ # 第三方库(Arduino IDE自动管理)
主程序代码 (smart_lantern_control.ino)
主程序负责初始化硬件、建立 Wi-Fi 连接、设置 Web 服务器,以及处理手势控制和LED状态更新。关键功能包括:
- 初始化 SPIFFS 文件系统和手势传感器
- 设置 Web 服务器路由,包括静态文件服务和 API 接口
- 实现手势识别逻辑和 LED 控制
- 添加文件管理功能,解决 ESP32FS 工具不可用的问题
添加的文件管理功能在访问/files
路径时提供界面,允许上传和管理 Web 文件,解决了 ESP32FS 工具不可用的问题。这是一个重要的解决方案,确保即使没有专门的工具也能上传 Web 界面文件。
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
#include <Wire.h>
#include <ArduinoJson.h>
#include "XLOT_APDS9960AD.h"
// WiFi设置
const char* ssid = "xiao";
const char* password = "12345678";
// 服务器和WebSocket
AsyncWebServer server(80);
// 文件上传
File fsUploadFile;
// 引脚重定义 - 适应自制扩展板
#define SDA_PIN 20 // RX/D7 (GPIO20)
#define SCL_PIN 21 // TX/D6 (GPIO21)
// LED引脚定义
const int LED_PINS[] = {D5, D4, D3, D2, D1, D0};
const int LED_COUNT = 6;
// 手势传感器
XLOT_APDS9960AD apds;
// 控制变量
int ledCount = 0; // 点亮的LED数量(0-6)
unsigned long lastGestureTime = 0; // 最后手势时间戳
const int gestureDelay = 500; // 手势识别间隔(毫秒)
// 系统信息
String deviceName = "SmartLantern";
String firmwareVersion = "1.0.0";
// 初始化SPIFFS
void initSPIFFS() {
if (!SPIFFS.begin(true)) {
Serial.println("SPIFFS挂载失败");
return;
}
Serial.println("SPIFFS挂载成功");
// 列出SPIFFS中的文件
File root = SPIFFS.open("/");
File file = root.openNextFile();
Serial.println("SPIFFS文件列表:");
while(file) {
Serial.print("- ");
Serial.print(file.name());
Serial.print(" (");
Serial.print(file.size());
Serial.println(" 字节)");
file = root.openNextFile();
}
}
// 连接WiFi
void connectToWiFi() {
Serial.println("连接到WiFi...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi连接成功!");
Serial.print("IP地址: ");
Serial.println(WiFi.localIP());
}
// 更新LED显示
void updateLEDs() {
for(int i = 0; i < LED_COUNT; i++) {
// 如果i小于ledCount,点亮LED,否则熄灭
digitalWrite(LED_PINS[i], (i < ledCount) ? HIGH : LOW);
}
}
// 处理手势
void processGesture(uint8_t gesture) {
switch(gesture) {
case APDS9960_RIGHT:
// 右划 - 全部点亮LED
if(ledCount < LED_COUNT) {
ledCount = LED_COUNT;
updateLEDs();
Serial.println("全部LED点亮");
}
break;
case APDS9960_LEFT:
// 左划 - 全部关闭LED
if(ledCount > 0) {
ledCount = 0;
updateLEDs();
Serial.println("全部LED关闭");
}
break;
case APDS9960_UP:
// 上划 - 增加LED亮灯数量
if(ledCount < LED_COUNT) {
ledCount++;
updateLEDs();
Serial.print("LED亮灯数量: ");
Serial.println(ledCount);
}
break;
case APDS9960_DOWN:
// 下划 - 减少LED亮灯数量
if(ledCount > 0) {
ledCount--;
updateLEDs();
Serial.print("LED亮灯数量: ");
Serial.println(ledCount);
}
break;
}
}
// 设置Web服务器路由
void setupServer() {
// 静态文件处理
server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");
// API路由: 获取LED状态
server.on("/api/led/status", HTTP_GET, [](AsyncWebServerRequest *request) {
DynamicJsonDocument doc(128);
doc["count"] = ledCount;
String response;
serializeJson(doc, response);
request->send(200, "application/json", response);
});
// API路由: 控制LED
server.on("/api/led/control", HTTP_POST, [](AsyncWebServerRequest *request) {
// 默认响应
DynamicJsonDocument responseDoc(128);
responseDoc["success"] = false;
responseDoc["message"] = "无效请求";
if (request->hasParam("count", true)) {
int newCount = request->getParam("count", true)->value().toInt();
if (newCount >= 0 && newCount <= LED_COUNT) {
ledCount = newCount;
updateLEDs();
responseDoc["success"] = true;
responseDoc["message"] = "LED数量已更新";
} else {
responseDoc["message"] = "LED数量超出范围(0-6)";
}
} else if (request->hasParam("action", true)) {
String action = request->getParam("action", true)->value();
if (action == "all_on") {
ledCount = LED_COUNT;
updateLEDs();
responseDoc["success"] = true;
responseDoc["message"] = "所有LED已点亮";
} else if (action == "all_off") {
ledCount = 0;
updateLEDs();
responseDoc["success"] = true;
responseDoc["message"] = "所有LED已关闭";
} else {
responseDoc["message"] = "未知动作";
}
}
String response;
serializeJson(responseDoc, response);
request->send(200, "application/json", response);
});
// API路由: 获取系统信息
server.on("/api/system/info", HTTP_GET, [](AsyncWebServerRequest *request) {
DynamicJsonDocument doc(256);
doc["name"] = deviceName;
doc["version"] = firmwareVersion;
doc["ip"] = WiFi.localIP().toString();
doc["connected"] = WiFi.status() == WL_CONNECTED;
String response;
serializeJson(doc, response);
request->send(200, "application/json", response);
});
// 文件上传相关路由
// 文件管理界面 - 修复UTF-8编码问题
server.on("/files", HTTP_GET, [](AsyncWebServerRequest *request) {
String content = "<html><head>";
content += "<title>智幻走马灯 - 文件管理</title>";
content += "<meta charset='UTF-8'>"; // 添加UTF-8字符集声明
content += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
content += "<style>";
content += "body { font-family: Arial, sans-serif; margin: 20px; }";
content += "h1 { color: #2196F3; }";
content += ".file-list { margin: 20px 0; }";
content += ".file-item { padding: 8px; border-bottom: 1px solid #eee; }";
content += ".btn { padding: 8px 15px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; text-decoration: none; display: inline-block; }";
content += ".btn-danger { background-color: #F44336; }";
content += ".upload-form { margin: 20px 0; padding: 15px; background-color: #f5f5f5; border-radius: 5px; }";
content += "</style>";
content += "</head><body>";
content += "<h1>智幻走马灯 - 文件管理</h1>";
content += "<div class='upload-form'>";
content += "<h2>上传文件</h2>";
content += "<form method='POST' action='/upload' enctype='multipart/form-data'>";
content += "<input type='file' name='upload'>";
content += "<button class='btn' type='submit'>上传</button>";
content += "</form>";
content += "</div>";
content += "<h2>文件列表</h2>";
content += "<div class='file-list'>";
File root = SPIFFS.open("/");
File file = root.openNextFile();
while(file) {
String fileName = String(file.name());
content += "<div class='file-item'>";
content += fileName + " (" + String(file.size()) + " bytes) ";
content += "<a class='btn' href='" + fileName + "'>查看</a> ";
content += "<a class='btn btn-danger' href='/delete?file=" + fileName + "'>删除</a>";
content += "</div>";
file = root.openNextFile();
}
content += "</div>";
content += "<p><a class='btn' href='/'>返回控制面板</a></p>";
content += "</body></html>";
request->send(200, "text/html; charset=utf-8", content); // 指定字符集为UTF-8
});
// 文件上传处理
server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request) {
request->redirect("/files");
}, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if (!index) {
Serial.printf("接收上传: %s\n", filename.c_str());
// 打开文件写入
if (!filename.startsWith("/")) {
filename = "/" + filename;
}
fsUploadFile = SPIFFS.open(filename, "w");
}
if (fsUploadFile) {
// 写入文件
fsUploadFile.write(data, len);
}
if (final) {
if (fsUploadFile) {
// 关闭文件
fsUploadFile.close();
}
Serial.printf("上传完成: %s, %u bytes\n", filename.c_str(), index + len);
}
});
// 文件删除
server.on("/delete", HTTP_GET, [](AsyncWebServerRequest *request) {
if (request->hasParam("file")) {
String fileName = request->getParam("file")->value();
if (SPIFFS.remove(fileName)) {
request->redirect("/files?msg=删除成功");
} else {
request->send(500, "text/plain", "删除失败");
}
} else {
request->send(400, "text/plain", "缺少文件参数");
}
});
// 404处理
server.onNotFound([](AsyncWebServerRequest *request) {
request->send(404, "text/plain", "页面未找到");
});
// 启动服务器
server.begin();
Serial.println("HTTP服务器已启动");
}
void setup() {
Serial.begin(115200);
delay(3000); // 增加延迟,确保有足够时间上传新代码
Serial.println("\n智幻走马灯Web控制系统启动...");
// 使用重定义的引脚初始化I2C
Wire.begin(SDA_PIN, SCL_PIN);
// 初始化所有LED引脚
for(int i = 0; i < LED_COUNT; i++) {
pinMode(LED_PINS[i], OUTPUT);
digitalWrite(LED_PINS[i], LOW); // 初始状态全部关闭
}
// 初始化手势传感器
if(!apds.begin()){
Serial.println("手势传感器初始化失败! 请检查接线。");
// 错误指示 - 闪烁第一个LED
while(1) {
digitalWrite(LED_PINS[0], HIGH);
delay(100);
digitalWrite(LED_PINS[0], LOW);
delay(100);
}
} else {
Serial.println("手势传感器初始化成功!");
apds.enableProximity(true);
apds.enableGesture(true);
apds.setProxGain(APDS9960_PGAIN_8X);
apds.setGestureGain(APDS9960_PGAIN_8X);
apds.setGestureGain(APDS9960_AGAIN_64X);
apds.setGestureGain(APDS9960_GGAIN_8);
// 成功指示 - 所有LED依次亮起再熄灭
for(int i = 0; i < LED_COUNT; i++) {
digitalWrite(LED_PINS[i], HIGH);
delay(200);
}
delay(500);
for(int i = 0; i < LED_COUNT; i++) {
digitalWrite(LED_PINS[i], LOW);
delay(200);
}
}
// 初始化SPIFFS
initSPIFFS();
// 连接WiFi
connectToWiFi();
// 设置服务器
setupServer();
Serial.println("系统初始化完成!");
}
void loop() {
// 读取手势
uint8_t gesture = apds.readGesture();
// 处理手势(添加延迟以防止过快响应)
if(gesture != 0 && millis() - lastGestureTime > gestureDelay) {
lastGestureTime = millis();
processGesture(gesture);
}
// 检查WiFi连接
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi连接断开,尝试重连...");
WiFi.reconnect();
}
// 添加短延迟,减少CPU使用
delay(10);
}
HTML 界面代码 (index.html)
HTML 文件实现了响应式设计的 Web 界面:
- 状态指示区显示设备连接状态和 IP
- LED 控制区提供视觉反馈和控制按钮
- JavaScript 部分处理 API 调用和状态更新
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智幻走马灯控制</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.header {
text-align: center;
margin-bottom: 20px;
}
.status-bar {
background-color: #fff;
padding: 10px;
border-radius: 5px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.status-indicator {
display: flex;
align-items: center;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.connected {
background-color: #4CAF50;
}
.disconnected {
background-color: #F44336;
}
.control-card {
background-color: #fff;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.led-container {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.led {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #ddd;
margin: 0 5px;
}
.led.active {
background-color: #4CAF50;
box-shadow: 0 0 10px #4CAF50;
}
.control-buttons {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.button-group {
display: flex;
}
button {
padding: 10px 15px;
margin: 0 5px;
border: none;
border-radius: 4px;
background-color: #2196F3;
color: white;
cursor: pointer;
font-size: 14px;
}
button:hover {
background-color: #0b7dda;
}
.footer {
text-align: center;
font-size: 12px;
color: #666;
margin-top: 30px;
}
</style>
</head>
<body>
<div class="header">
<h1>智幻走马灯控制</h1>
</div>
<div class="status-bar">
<div class="status-indicator">
<div id="status-dot" class="status-dot disconnected"></div>
<span id="status-text">未连接</span>
</div>
<div id="ip-address">IP: -</div>
</div>
<div class="control-card">
<h2>LED控制</h2>
<div class="led-container" id="led-display">
<div class="led" id="led-0"></div>
<div class="led" id="led-1"></div>
<div class="led" id="led-2"></div>
<div class="led" id="led-3"></div>
<div class="led" id="led-4"></div>
<div class="led" id="led-5"></div>
</div>
<div class="control-buttons">
<div class="button-group">
<button id="btn-decrease">减少 ⬇️</button>
<button id="btn-increase">增加 ⬆️</button>
</div>
<div class="button-group">
<button id="btn-all-off">全部关闭 ◀️</button>
<button id="btn-all-on">全部点亮 ▶️</button>
</div>
</div>
</div>
<div class="footer">
<p>智幻走马灯控制系统 v1.0.0</p>
<p>基于ESP32C3 + Web技术</p>
</div>
<script>
// 状态变量
let ledCount = 0;
let isConnected = false;
// DOM元素
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
const ipAddress = document.getElementById('ip-address');
const ledElements = Array.from(document.querySelectorAll('.led'));
// 按钮元素
const btnIncrease = document.getElementById('btn-increase');
const btnDecrease = document.getElementById('btn-decrease');
const btnAllOn = document.getElementById('btn-all-on');
const btnAllOff = document.getElementById('btn-all-off');
// 更新LED显示
function updateLedDisplay() {
ledElements.forEach((led, index) => {
if (index < ledCount) {
led.classList.add('active');
} else {
led.classList.remove('active');
}
});
}
// 更新连接状态
function updateConnectionStatus(connected, ip) {
isConnected = connected;
if (connected) {
statusDot.classList.remove('disconnected');
statusDot.classList.add('connected');
statusText.textContent = '已连接';
ipAddress.textContent = `IP: ${ip}`;
} else {
statusDot.classList.remove('connected');
statusDot.classList.add('disconnected');
statusText.textContent = '未连接';
ipAddress.textContent = 'IP: -';
}
}
// 获取LED状态
async function getLedStatus() {
try {
const response = await fetch('/api/led/status');
const data = await response.json();
ledCount = data.count;
updateLedDisplay();
} catch (error) {
console.error('获取LED状态失败:', error);
}
}
// 控制LED
async function controlLed(params) {
try {
const formData = new URLSearchParams(params);
const response = await fetch('/api/led/control', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData
});
const data = await response.json();
if (data.success) {
await getLedStatus();
} else {
console.error('控制LED失败:', data.message);
}
} catch (error) {
console.error('发送控制请求失败:', error);
}
}
// 获取系统信息
async function getSystemInfo() {
try {
const response = await fetch('/api/system/info');
const data = await response.json();
updateConnectionStatus(data.connected, data.ip);
} catch (error) {
console.error('获取系统信息失败:', error);
updateConnectionStatus(false, '-');
}
}
// 初始化
async function initialize() {
await getSystemInfo();
await getLedStatus();
// 设置定时刷新
setInterval(getSystemInfo, 5000);
setInterval(getLedStatus, 2000);
}
// 事件监听器
btnIncrease.addEventListener('click', () => {
if (ledCount < 6) {
controlLed({ count: ledCount + 1 });
}
});
btnDecrease.addEventListener('click', () => {
if (ledCount > 0) {
controlLed({ count: ledCount - 1 });
}
});
btnAllOn.addEventListener('click', () => {
controlLed({ action: 'all_on' });
});
btnAllOff.addEventListener('click', () => {
controlLed({ action: 'all_off' });
});
// 启动应用
document.addEventListener('DOMContentLoaded', initialize);
</script>
</body>
</html>
上面的 HTML 文件,我在 Windsurf IDE 中进行预览,可以看到页面在浏览器中显示的实际的效果。
index.html 文件在 Windsurf IDE 中进行预览的效果,感觉还不错
项目实施步骤
准备工作
首先需要准备以下材料和工具:
- 硬件设备:
- XIAO ESP32C3 开发板(自制)
- APDS-9960手势传感器
- 连接线/杜邦线
- USB数据线
- 软件工具:
- Arduino IDE (2.0或更高版本)
- 必要的Arduino库:
- ESPAsyncWebServer: https://github.com/me-no-dev/ESPAsyncWebServer
- AsyncTCP: https://github.com/me-no-dev/AsyncTCP
- ArduinoJson: 通过Arduino库管理器安装
- XLOT_APDS9960AD库(第10周使用的库)
安装必要的库
在 Arduino IDE 中安装必要的库:
- Arduino 内置库管理器:
- 打开 Arduino IDE
- 选择"工具(Tools)" -> "管理库(Manage Libraries)"
- 搜索并安装 ArduinoJson 库
- 手动安装第三方库:
- ESPAsyncWebServer 和 AsyncTCP 库需要手动安装
- 从 GitHub 下载库文件 (ZIP格式)
- 在 Arduino IDE中,选择"项目" -> "加载库" -> "添加.ZIP库"
- 选择下载的 ZIP 文件
- 确认已安装 XLOT_APDS9960AD 库(第10周使用的库)
硬件连接
按照第10周作业的方式连接手势传感器:
APDS-9960引脚 | 连接到扩展板引脚 | XIAO ESP32C3 引脚 | 功能 |
---|---|---|---|
VCC(红线) | J1-2 (3.3V) | 3.3V | 电源正极 |
GND(黑线) | J1-1 (GND) | GND | 电源地线 |
SDA(黄线) | J1-3 (RX/D7) | GPIO20 | I2C数据线(软件实现) |
SCL(绿线) | J1-4 (TX/D6) | GPIO21 | I2C时钟线(软件实现) |
使用Y型分线器实现两个设备共享电源和地线的连接示意图
安装步骤
- 准备并安装必要的库(ESPAsyncWebServer、AsyncTCP、ArduinoJson)
- 创建项目目录结构,包括主程序和 data 文件夹
- 编译并上传主程序到 XIAO ESP32C3,打开串口监视器,可以看到一个提示的 IP 地址。
上传成功后,串口监视器会给出状态信息并提示一个 IP 地址
- 用电脑通过Web浏览器访问
http://[ESP32IP地址]/files
上传index.html
,注意电脑需要和 XIAO ESP32C3 连接相同的 Wi-Fi,才能成功访问。
XIAO ESP32C3 展示的文件管理界面,我们可以通过这个界面上传 index.html 文件
- 现在可以通过电脑或手机的浏览器访问
http://[ESP32IP地址]/
,来测试 Web 控制界面。
使用鼠标点击全部点亮按钮
开发板上的 LED 全部被点亮了。
现在可以全面测试手势与 Web 端的控制效果了。
遇到的挑战与解决方案
ESP32FS 工具不可用
问题:ESP32FS 工具无法在 Arduino IDE 中找到,无法直接上传 HTML 文件。
解决方案:添加了 Web 文件管理功能,通过/files
路径提供文件上传界面,使用户可以直接通过 Web 浏览器上传 HTML 文件,避免了对 ESP32FS 工具的依赖。
页面编码问题
问题:初次访问文件管理界面时出现中文乱码。
解决方案:虽然有乱码,但功能正常,通过文件管理界面成功上传了编码正确的 index.html 文件,解决了问题。然后告知 Claude 乱码问题,Claude 提供了修改后的 Arudino 程序后问题被解决。
跨设备访问
问题:电脑无法访问 ESP32C3 的 Web 界面。
解决方案:确认需要将所有设备连接到 ESP32C3 创建的 Wi-Fi 网络才能访问 Web 界面。
测试结果
系统测试结果表明项目功能完整:
- Wi-Fi 连接成功建立,IP地址正确显示
- 文件上传功能正常工作,成功上传index.html
- Web界面正确加载,显示LED状态和连接信息
- 按钮控制功能正常,可以增减LED亮灯数量和全开/全关
- 手势控制功能正常,与Web控制同步
结论与后续开发
本项目成功实现了智幻走马灯的Web控制界面,整合了手势控制功能并提供了直观的用户界面。系统采用轻量级设计,适合ESP32C3的资源限制,同时保持了良好的用户体验。
通过添加Web文件管理功能,解决了ESP32FS工具不可用的问题,提供了更灵活的部署方式。项目为"智幻走马灯"最终系统奠定了软件基础,证明了ESP32C3能够同时处理传感器输入和Web服务的能力。
后续可考虑添加LED亮度控制、电机控制功能、多设备同步等拓展,进一步提升系统功能和用户体验。