项目 | 内容 |
课时 | 2课时(90分钟) |
前置课程 | 第1-4课(已掌握:点灯、WiFi联网、基础网页控制) |
教学目标 | ① 美化手机控制界面 ② 实现颜色选择功能 ③ 实现亮度调节功能 |
教学形式 | 教师演示 + 学生同步实操(边写代码边讲解,每写完一段立即测试) |
所需材料 | ESP32开发板、WS2812B灯条(10灯)、杜邦线、USB线、电脑、手机 |
提问环节:
教师话术:
"今天我们要给我们的灯控系统来个大装修!我们要做三件事:
最终效果:拿出手机,打开一个像APP一样漂亮的页面,点颜色、拖滑块,灯条实时响应!"
展示最终效果视频/图片(教师课前准备好)
教师操作: 打开Arduino IDE,加载第4课的最终代码。
讲解要点:
// 第4课我们已经有这些基础:
#include <WiFi.h> // WiFi功能
#include <WebServer.h> // 网页服务器
#include <FastLED.h> // LED控制
#define LED_PIN 2 // 灯条接GPIO 2
#define NUM_LEDS 10 // 10颗灯珠
const char* ssid = "你的WiFi名"; // WiFi名称
const char* password = "你的WiFi密码"; // WiFi密码
CRGB leds[NUM_LEDS]; // LED数组,像10个抽屉,每个放一种颜色
WebServer server(80); // 服务器开在80端口(网页默认端口)
bool lightOn = true; // 灯的状态:开着提问: "这些代码是什么意思?谁来解释一下CRGB leds[NUM_LEDS]?"
预期回答: "这是一个数组,有10个元素,每个元素是一个颜色对象,对应一颗灯珠。"
教师操作: 在第4课代码基础上,添加两个新变量。
新增代码(在全局变量区域):
// ========== 【新增】状态变量 ==========
// 为什么要加这两个变量?
// 因为我们要记住:用户选了什么颜色?亮度调到多少?
// 就像你调台灯,调完后关掉,下次打开应该还是这个亮度
CRGB currentColor = CRGB(255, 255, 255);
// currentColor = 当前颜色,默认白色(255,255,255)
// CRGB是FastLED的颜色类型,三个数字分别代表红、绿、蓝的强度
// 每个颜色范围0-255,0是最暗,255是最亮
// 所以(255,255,255)就是红满+绿满+蓝满 = 白色
int brightness = 128;
// brightness = 亮度,范围0-255
// 128刚好是一半,作为默认值比较温和,不会太刺眼教师讲解:
"大家看,我加了两个'记忆变量'。
currentColor像是一个调色盘,记住你现在选的颜色。默认白色。
brightness像是音量旋钮,记住亮度大小。默认128,一半亮度。
为什么要记住?因为当你关灯后再开灯,应该恢复之前的状态,而不是每次都回到默认。"
学生操作: 在自己的代码中添加这两个变量。
测试验证: 编译上传,串口监视器观察是否正常启动。没有功能变化,只是加了变量,确保不报错。
教师讲解:
"第4课的网页代码是怎么写的?是不是有很多转义符号,看起来很乱?
今天教大家一个C++秘技:R"rawliteral(...)rawliteral"
它的作用:把一大段HTML原封不动地塞进程序里,不用转义任何符号!"
教师板书/投影对比:
旧写法(痛苦):
const char* html = "<html><head><title>\"灯控\"</title></head><body>...</body></html>";
// 每个引号前都要加反斜杠,写一行就眼花了新写法(清爽):
const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>灯控</title>
</head>
<body>
...
</body>
</html>
)rawliteral";
// 从R"rawliteral( 开始,到 )rawliteral" 结束
// 中间所有内容原样保留,双引号、换行、缩进全部保留关键规则强调:
R 表示Raw(原始),后面的 "标记( 和 )标记" 必须成对出现rawliteral 或 EOF学生操作: 把第4课的HTML部分替换成这种新写法。
测试验证: 编译上传,手机访问,页面应该和第4课一样能正常显示。
教师操作: 在<head>标签内添加<style>块。
讲解策略: 不要一次性贴出全部CSS,而是分块讲解,每块立即看效果。
【第4.1块】基础重置 + 页面背景
<style>
/* ========== CSS基础重置 ========== */
* {
margin: 0; /* 去掉所有元素默认外边距 */
padding: 0; /* 去掉所有元素默认内边距 */
box-sizing: border-box; /* 盒子模型:宽高包含边框和内边距 */
/* 什么意思?比如一个div宽300px,加了padding和border后还是300px,不会撑大 */
}
/* ========== 页面整体背景 ========== */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* 字体栈:优先用苹果系统字体,其次Windows的Segoe UI,最后通用无衬线体 */
/* 这样在不同手机上都能显示好看的字体 */
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
/* linear-gradient = 线性渐变 */
/* 135deg = 渐变方向:从左上到右下 */
/* #667eea 是起点颜色(蓝紫色),#764ba2 是终点颜色(深紫色) */
/* 0%和100%表示颜色在渐变中的位置 */
/* 效果:整个页面背景是漂亮的紫蓝渐变,像极光一样 */
min-height: 100vh; /* 最小高度 = 视口高度的100%,确保背景铺满全屏 */
padding: 20px; /* 四周留白20像素,内容不贴边 */
}
</style>
教师讲解要点:
"box-sizing: border-box是CSS中最重要的一句话。没有它,你设置宽度300px,加了padding和border后会变成300+padding+border,布局会乱。有了它,300px就是最终宽度,padding和border从内部扣减。"
学生操作: 添加这部分CSS,编译上传,手机刷新看效果。
预期效果: 页面背景变成漂亮的紫蓝渐变,文字可能看不清(因为默认黑色),没关系,下一步解决。
【第4.2块】容器和标题
<style>
/* ... 接上面的CSS ... */
/* ========== 内容容器 ========== */
.container {
max-width: 400px; /* 最宽400像素 */
/* 为什么?手机屏幕宽度各异,400px在大多数手机上显示舒适 */
/* 在大屏幕(如平板)上,内容不会无限撑宽,保持美观 */
margin: 0 auto; /* 上下边距0,左右自动(居中) */
}
h1 {
color: white; /* 标题白色,在深色背景上清晰 */
text-align: center; /* 文字居中 */
margin-bottom: 20px; /* 标题下方留白 */
font-size: 24px; /* 字号 */
}
</style>
教师讲解:
".container是一个'盒子',把所有内容装进去。max-width: 400px就像给水流加一个400px宽的渠道,水(内容)在里面流,不会泛滥。"
学生操作: 在HTML的<body>里添加容器:
<body>
<div class="container">
<h1>🏠 灯条控制中心</h1>
<!-- 后续内容都放在这个container里面 -->
</div>
</body>
测试验证: 手机刷新,看到标题居中、白色、背景渐变。
【第4.3块】卡片样式——让内容分区清晰
<style>
/* ... 接上面的CSS ... */
/* ========== 卡片样式 ========== */
.card {
background: white; /* 卡片背景白色,与深色页面形成对比 */
border-radius: 16px; /* 圆角16像素,像iOS的卡片 */
padding: 20px; /* 内边距20px,内容与边框保持距离 */
margin-bottom: 15px; /* 卡片之间间距15px */
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
/* box-shadow = 盒子阴影,四个值:
0 = 水平偏移(不左右偏)
10px = 垂直偏移(向下偏10像素)
30px = 模糊半径(阴影边缘虚化30像素)
rgba(0,0,0,0.2) = 颜色:黑色,20%透明度
效果:卡片像浮在背景之上,有立体感 */
}
.card-title {
font-size: 18px;
font-weight: bold; /* 加粗 */
margin-bottom: 15px;
color: #333; /* 深灰色,比纯黑柔和 */
}
</style>
教师讲解:
"border-radius: 16px让方角变圆角,这是现代UI设计的标志。iPhone的按钮、微信的对话框都是圆角。
box-shadow是'魔法'——没有阴影的卡片像贴在墙上,有了阴影像悬浮在空中,层次立刻出来。"
学生操作: 在HTML中添加第一个卡片(开关控制):
<div class="card">
<div class="card-title">开关控制</div>
<button onclick="fetch('/on')">开灯</button>
<button onclick="fetch('/off')">关灯</button>
</div>
测试验证: 手机刷新,看到白色圆角卡片,按钮在卡片内。对比第4课的简陋界面,学生应该有"哇"的感觉。
【第4.4块】按钮美化
<style>
/* ... 接上面的CSS ... */
/* ========== 按钮样式 ========== */
.btn {
padding: 15px 30px; /* 上下15px,左右30px,胖胖的按钮 */
margin: 5px; /* 按钮之间间距 */
border: none; /* 去掉默认边框 */
border-radius: 10px; /* 圆角 */
font-size: 16px; /* 字号 */
cursor: pointer; /* 鼠标悬停时变成手型 */
color: white; /* 文字白色 */
font-weight: bold; /* 文字加粗 */
}
.on { background: #4CAF50; } /* 绿色 = 开灯,像交通灯 */
.off { background: #f44336; } /* 红色 = 关灯,像停止按钮 */
/* 按钮悬停效果:鼠标放上去稍微变暗 */
.btn:hover {
opacity: 0.9; /* 透明度90%,变暗一点点 */
transform: scale(1.02); /* 微微放大2%,有按下去的感觉 */
}
</style>
教师讲解:
"#4CAF50是Material Design(谷歌设计规范)中的标准绿色,#f44336是标准红色。用这些'标准色',界面看起来就专业。
:hover是CSS伪类,表示'鼠标悬停时'。在手机上,点击瞬间也会触发hover效果,给用户反馈。"
学生操作: 给按钮加上class:
<button class="btn on" onclick="fetch('/on')">开灯</button>
<button class="btn off" onclick="fetch('/off')">关灯</button>
测试验证: 手机刷新,看到绿色"开灯"按钮、红色"关灯"按钮,点击有颜色反馈。
【第4.5块】颜色选择按钮
<style>
/* ... 接上面的CSS ... */
/* ========== 颜色选择按钮 ========== */
.color-btn {
width: 50px; /* 宽50像素 */
height: 50px; /* 高50像素 */
border-radius: 50%; /* 圆角50% = 正圆形! */
border: 3px solid transparent; /* 默认边框透明(占位置但看不见) */
margin: 5px; /* 间距 */
cursor: pointer;
display: inline-block; /* 行内块,可以设置宽高又能排在一行 */
}
/* 被选中的颜色按钮 */
.color-btn.selected {
border-color: #333; /* 边框变深灰色 */
transform: scale(1.1); /* 放大10%,突出显示 */
}
</style>
教师讲解:
"border-radius: 50%是CSS画圆的秘密。只要宽高相等,圆角50%就是正圆。
border: 3px solid transparent先放一个透明的边框占位。当选中时,把颜色改成#333,圆就突然有了边框,而且因为本来就有占位,页面不会跳动。"
学生操作: 在HTML中添加颜色选择区域:
<div class="card">
<div class="card-title">🎨 选择颜色</div>
<div class="color-picker">
<!-- 白色按钮,默认选中(有selected类) -->
<div class="color-btn selected" style="background:#fff" onclick="setColor(255,255,255)"></div>
<div class="color-btn" style="background:#f00" onclick="setColor(255,0,0)"></div>
<div class="color-btn" style="background:#0f0" onclick="setColor(0,255,0)"></div>
<div class="color-btn" style="background:#00f" onclick="setColor(0,0,255)"></div>
<div class="color-btn" style="background:#ff0" onclick="setColor(255,255,0)"></div>
<div class="color-btn" style="background:#f0f" onclick="setColor(255,0,255)"></div>
<div class="color-btn" style="background:#0ff" onclick="setColor(0,255,255)"></div>
<div class="color-btn" style="background:#ffa500" onclick="setColor(255,165,0)"></div>
</div>
</div>
教师讲解颜色值:
"这些颜色代码是十六进制表示法:
#fff = 白色 = (255,255,255)#f00 = 红色 = (255,0,0) #0f0 = 绿色 = (0,255,0)#00f = 蓝色 = (0,0,255)#ff0 = 黄色 = 红+绿#f0f = 紫色 = 红+蓝#0ff = 青色 = 绿+蓝#ffa500 = 橙色 = 标准橙色onclick里的setColor(255,0,0)是JavaScript函数调用,我们下一步写这个函数。"
测试验证: 手机刷新,看到一排彩色圆点。点击暂时没反应(因为JS函数还没写),但界面已经漂亮。
【第4.6块】亮度滑块
<style>
/* ... 接上面的CSS ... */
/* ========== 滑块样式 ========== */
.slider {
width: 100%; /* 宽度填满父容器 */
height: 8px; /* 滑轨道高度 */
border-radius: 4px; /* 圆角 */
background: #ddd; /* 浅灰色轨道 */
outline: none; /* 去掉点击时的蓝色轮廓 */
-webkit-appearance: none; /* 去掉浏览器默认样式(关键!) */
}
/* Webkit浏览器(Chrome、Safari)的滑块 thumb */
.slider::-webkit-slider-thumb {
-webkit-appearance: none; /* 去掉默认thumb样式 */
width: 24px; /* thumb宽度 */
height: 24px; /* thumb高度 */
border-radius: 50%; /* 圆形thumb */
background: #667eea; /* 蓝紫色,和背景呼应 */
cursor: pointer;
box-shadow: 0 2px 6px rgba(0,0,0,0.3); /* thumb阴影 */
}
/* Firefox浏览器的滑块 thumb */
.slider::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
border: none;
}
</style>
教师讲解:
"input type="range"是HTML5的新控件,浏览器自动渲染成滑块。
但不同浏览器渲染的样子不一样!Chrome用-webkit-slider-thumb,Firefox用-moz-range-thumb。我们要分别写,才能在所有手机上好看。
-webkit-appearance: none是'大杀器'——去掉浏览器默认样式,让我们完全自定义。"
学生操作: 在HTML中添加亮度区域:
<div class="card">
<div class="card-title">🔆 亮度调节</div>
<input type="range" class="slider" min="0" max="255" value="128"
onchange="setBrightness(this.value)">
<div class="status">亮度: <span id="bval">50%</span></div>
</div>
教师讲解属性:
min="0":滑到最左 = 0(最暗)max="255":滑到最右 = 255(最亮)value="128":初始位置在中间(128/255 ≈ 50%)onchange:值改变时触发(松开滑块时)this.value:当前滑块的值测试验证: 手机刷新,看到滑块,拖动有视觉反馈。
【第4.7块】状态文字
<style>
/* ... 接上面的CSS ... */
.status {
text-align: center; /* 居中 */
color: #666; /* 中灰色 */
margin-top: 10px; /* 上方间距 */
font-size: 14px; /* 小字 */
}
</style>
学生操作: 在HTML底部添加IP显示:
<div class="status">IP: <span id="ip">--</span></div>
教师操作: 在</body>前添加<script>标签。
【第5.1块】获取IP地址
<script>
// ========== 页面加载时自动获取IP ==========
// fetch('/ip') 发送请求到ESP32的/ip路径
// .then(r => r.text()) 把响应转成文本
// .then(ip => { ... }) 拿到IP字符串,更新页面
fetch('/ip')
.then(r => r.text())
.then(ip => {
document.getElementById('ip').textContent = ip;
})
.catch(err => {
console.log('获取IP失败:', err);
});
/* 逐行解释:
fetch('/ip') → 向服务器发送GET请求,路径是/ip
.then(r => r.text()) → 服务器返回的是纯文本,转成字符串
.then(ip => ...) → 拿到IP字符串(如"192.168.1.100")
document.getElementById('ip') → 找到id="ip"的span元素
.textContent = ip → 把元素里的文字改成IP地址
*/
</script>
教师讲解Promise:
"fetch返回一个Promise(承诺)。就像你点外卖,店家承诺'做好了给你送'。.then()就是'做好了之后做什么'。第一个.then处理响应,第二个.then处理数据。
这种写法叫'链式调用',避免了嵌套回调的地狱。"
测试验证: 手机刷新,看到IP地址从"--"变成实际IP(如192.168.1.100)。
【第5.2块】颜色设置函数
<script>
// ... 接上面的代码 ...
// ========== 设置颜色函数 ==========
function setColor(r, g, b) {
// 第1步:发送颜色指令到ESP32
// 使用模板字符串(反引号),${变量}自动插入值
fetch(`/color?r=${r}&g=${g}&b=${b}`);
/* 生成的URL示例:/color?r=255&g=0&b=0
? 后面是查询参数
r=255 表示红色通道值
g=0 表示绿色通道值
b=0 表示蓝色通道值
合起来就是:给我纯红色!
*/
// 第2步:更新按钮选中状态(视觉反馈)
// document.querySelectorAll('.color-btn')
// → 选中所有class含color-btn的元素,返回数组
// .forEach(b => b.classList.remove('selected'))
// → 对每个元素,移除selected类(去掉边框)
document.querySelectorAll('.color-btn')
.forEach(b => b.classList.remove('selected'));
// event.target 是触发点击事件的那个元素(被点的颜色圆点)
// .classList.add('selected') 给它加上selected类(显示边框+放大)
event.target.classList.add('selected');
}
</script>
教师讲解事件对象:
"event是浏览器自动传入的参数,包含这次点击的所有信息。event.target就是'被点的是谁'。点白色圆点,target就是白色圆点;点红色圆点,target就是红色圆点。
classList是元素的'类名管理器',比直接改className安全,不会误删其他类。"
测试验证:
如果灯条没变红: 说明后端handleColor还没写,进入下一步。
【第5.3块】亮度设置函数
<script>
// ... 接上面的代码 ...
// ========== 设置亮度函数 ==========
function setBrightness(val) {
// 第1步:发送亮度值到ESP32
fetch('/brightness?b=' + val);
/* URL示例:/brightness?b=200
b=200 表示亮度设为200(接近最亮)
*/
// 第2步:更新页面显示的百分比
// Math.round(val/255*100) → 把0-255转成0-100%,四舍五入
// + '%' → 加上百分号
document.getElementById('bval').textContent =
Math.round(val/255*100) + '%';
/* 计算示例:
val=0 → 0/255*100 = 0% → "0%"
val=128 → 128/255*100 = 50% → "50%"
val=255 → 255/255*100 = 100% → "100%"
*/
}
</script>
教师讲解计算:
"滑块给的是0-255,但用户想看百分比。怎么转换?
百分比 = (当前值 / 最大值) × 100%
所以 val/255*100,再用Math.round四舍五入成整数。"
测试验证: 拖动滑块,页面上的百分比数字实时变化。
教师操作: 在Arduino代码的loop()之前,添加处理函数。
【第6.1块】设置所有灯珠的辅助函数
// ========== 辅助函数:设置所有灯珠为同一种颜色 ==========
void setAllLeds(CRGB color) {
// for循环遍历所有灯珠
// int i = 0 从第0颗开始
// i < NUM_LEDS 到第9颗结束(共10颗,索引0-9)
// i++ 每次加1
for(int i = 0; i < NUM_LEDS; i++) {
leds[i] = color; // 把第i颗灯珠设为指定颜色
// leds是FastLED管理的数组,每个元素是CRGB类型
}
FastLED.show(); // ★★★ 关键!把内存数据发送到硬件 ★★★
// 在此之前,所有操作只是改内存
// 只有调用show(),GPIO 2才会输出精确的时序信号,真正点亮灯条
}教师强调:
"FastLED.show()是唯一真正控制硬件的语句。很多同学改了颜色但灯没变,就是忘了这行!"
【第6.2块】处理根路径——返回网页
// ========== 处理函数:返回主页面 ==========
void handleRoot() {
// 当浏览器访问 http://IP地址/ 时,执行这个函数
// server.send(状态码, 内容类型, 内容)
server.send(200, "text/html", htmlPage);
/* 参数解释:
200 → HTTP状态码:OK,请求成功
"text/html" → 内容类型:这是HTML网页
htmlPage → 实际发送的内容(我们定义的大段HTML字符串)
*/
}教师讲解HTTP状态码:
"200表示'一切正常'。常见的还有:
【第6.3块】开灯处理
// ========== 处理函数:开灯 ==========
void handleOn() {
lightOn = true; // 记住状态:灯现在是开着的
setAllLeds(currentColor); // 把所有灯珠设为当前记忆的颜色
server.send(200, "text/plain", "OK"); // 回复手机:成功
// text/plain = 纯文本,不是网页
// "OK" = 简单的成功确认
Serial.println("开灯"); // 串口打印日志,调试用
// Serial.println = 打印并换行
// 在串口监视器里能看到这行输出,确认代码执行到了这里
}教师讲解状态管理:
"为什么要lightOn = true?因为后面颜色控制和亮度控制都要判断:
'如果灯是关着的,只更新记忆,不点亮灯条'。
这样关灯状态下选颜色,下次开灯自动显示新颜色。"
【第6.4块】关灯处理
// ========== 处理函数:关灯 ==========
void handleOff() {
lightOn = false; // 记住状态:灯现在是关着的
setAllLeds(CRGB(0, 0, 0)); // 所有灯珠设为黑色(0,0,0) = 熄灭
server.send(200, "text/plain", "OK");
Serial.println("关灯");
}教师提问: "为什么关灯是CRGB(0,0,0)而不是关闭电源?"
预期回答: "WS2812B需要持续供电维持状态,设为黑色就是不发光,但芯片还在工作。"
【第6.5块】颜色处理——重点!
// ========== 处理函数:设置颜色 ==========
void handleColor() {
// 第1步:检查请求中是否包含r、g、b三个参数
// server.hasArg("参数名") 检查URL中有没有这个参数
// 例如:/color?r=255&g=0&b=0 → hasArg("r")返回true
if(server.hasArg("r") && server.hasArg("g") && server.hasArg("b")) {
// 第2步:取出参数值并转成整数
// server.arg("r") 获取参数r的值(字符串"255")
// .toInt() 把字符串转成整数 255
int r = server.arg("r").toInt();
int g = server.arg("g").toInt();
int b = server.arg("b").toInt();
// 第3步:保存到全局变量
currentColor = CRGB(r, g, b);
// CRGB是FastLED的颜色构造器,三个整数生成颜色对象
// 第4步:如果灯是开着的,立即更新显示
if(lightOn) {
setAllLeds(currentColor);
// 只有灯开着才点亮,关着状态只记忆颜色
}
// 第5步:串口打印调试信息
// Serial.printf 是格式化打印,类似C语言的printf
// %d 表示整数,会按顺序替换为r、g、b的值
Serial.printf("颜色: R=%d G=%d B=%d\n", r, g, b);
// 输出示例:颜色: R=255 G=0 B=0
}
// 无论参数是否有效,都回复OK(前端不需要知道具体错误)
server.send(200, "text/plain", "OK");
}教师重点讲解:
"这段代码有防御性编程思想:
hasArg检查参数是否存在,防止程序崩溃if(lightOn)判断状态,避免关灯时意外点亮send回复,否则前端会卡住等响应Serial.printf是调试利器。 %d是占位符,后面的变量按顺序填入。\n是换行符。"
测试验证:
【第6.6块】亮度处理
// ========== 处理函数:设置亮度 ==========
void handleBrightness() {
// 检查是否有b参数(brightness的缩写)
if(server.hasArg("b")) {
// 取出亮度值并转成整数
brightness = server.arg("b").toInt();
// 设置FastLED的全局亮度
FastLED.setBrightness(brightness);
/* 注意:setBrightness不是改变颜色值!
它改变的是LED驱动芯片的PWM占空比。
颜色还是(255,255,255),但LED实际亮度按比例降低。
这是硬件级调光,比软件改颜色值更平滑。
*/
// 如果灯开着,更新显示
if(lightOn) {
setAllLeds(currentColor);
}
Serial.printf("亮度: %d\n", brightness);
}
server.send(200, "text/plain", "OK");
}教师讲解PWM:
"PWM = 脉冲宽度调制。LED不是真的变暗,而是快速开关(每秒几千次)。
占空比50% = 亮一半时间,人眼看起来就是一半亮度。FastLED.setBrightness()就是调这个占空比,所以很平滑。"
测试验证: 拖动滑块,观察灯条亮度变化。串口显示亮度值。
【第6.7块】IP地址处理
// ========== 处理函数:返回IP地址 ==========
void handleIP() {
// WiFi.localIP() 获取ESP32在当前WiFi中的IP地址
// .toString() 把IP地址对象转成字符串(如"192.168.1.100")
server.send(200, "text/plain", WiFi.localIP().toString());
// 纯文本发送IP字符串,前端JS收到后显示在页面上
}教师操作: 在setup()函数中,添加路由注册。
void setup() {
Serial.begin(115200); // 串口通信,波特率115200
// ========== 初始化LED ==========
FastLED.addLeds<<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
/* 参数解释:
WS2812B → LED芯片型号
LED_PIN → 数据引脚(GPIO 2)
GRB → 颜色顺序:绿-红-蓝(WS2812B的标准顺序)
leds → LED数组
NUM_LEDS → 灯珠数量(10)
*/
FastLED.setBrightness(brightness); // 设置初始亮度(128)
setAllLeds(currentColor); // 点亮所有灯(默认白色)
FastLED.show(); // 发送数据到硬件
Serial.println("\n=== 第5次课:美化界面 ===");
// ========== 连接WiFi ==========
WiFi.begin(ssid, password); // 开始连接WiFi
while(WiFi.status() != WL_CONNECTED) {
// WiFi.status() 返回连接状态
// WL_CONNECTED = 已连接(WiFi库定义的常量)
delay(500); // 等500毫秒
Serial.print("."); // 打印一个点,像进度条
}
// 循环直到连接成功,点会越来越多:..........
Serial.println("\nWiFi已连接");
Serial.print("IP: ");
Serial.println(WiFi.localIP()); // 打印IP地址
// ========== 【关键】注册HTTP路由 ==========
// server.on("路径", 处理函数)
// 当收到对应路径的请求时,自动调用处理函数
server.on("/", handleRoot); // 根路径 → 返回网页
server.on("/on", handleOn); // /on → 开灯
server.on("/off", handleOff); // /off → 关灯
server.on("/color", handleColor); // /color → 设置颜色
server.on("/brightness", handleBrightness); // /brightness → 设置亮度
server.on("/ip", handleIP); // /ip → 返回IP
server.begin(); // 启动服务器
Serial.println("服务器启动");
}教师强调路由匹配:
"这6行server.on就是'指令分发大厅'。
手机访问/on,ESP32知道去执行handleOn;
访问/color?r=255,去执行handleColor。
顺序不重要,但路径必须写对。少一个字母就404!"
void loop() {
server.handleClient(); // 检查是否有客户端请求,有就处理
delay(1); // 延时1毫秒,让系统喘口气
/* 为什么加delay?
ESP32是双核,但WiFi任务需要CPU时间。
不加delay可能导致WiFi不稳定、响应变慢。
1毫秒很短,人感知不到,但系统更稳定。
*/
}教师带领学生按顺序测试:
步骤 | 操作 | 预期结果 | 如果不成功 |
1 | 编译上传 | 无报错,串口显示连接信息 | 检查库是否安装、板子是否选对 |
2 | 手机访问IP | 看到漂亮界面 | 检查同WiFi、IP是否正确 |
3 | 点击"开灯" | 灯亮(白色) | 检查GPIO接线、灯条方向 |
4 | 点击"关灯" | 灯灭 | 检查handleOff代码 |
5 | 点击红色 | 灯变红,红按钮有边框 | 检查handleColor、串口输出 |
6 | 点击绿色 | 灯变绿,绿按钮有边框 | 检查RGB值是否正确 |
7 | 拖动滑块 | 亮度变化,百分比更新 | 检查handleBrightness |
8 | 关灯→选蓝→开灯 | 直接显示蓝色 | 检查状态记忆逻辑 |
教师提前准备,学生遇到问题时引导自查:
错误1:编译报错 R"rawliteral 相关
R"MYHTML(... )MYHTML"错误2:网页显示乱码
<meta charset="UTF-8"><head>第一行添加字符集声明错误3:颜色按钮点击后灯不变色
错误4:亮度调节无效
FastLED.setBrightness()必须在addLeds之后调用错误5:GPIO 2导致ESP32无法启动
#define LED_PINloop()中用millis()实现亮度正弦变化Preferences库保存颜色和亮度,重启后恢复课前检查:
课中注意:
课后跟进:
主板书(保留到下课):
第5课:美化界面 + 颜色 + 亮度
核心概念:
├─ R"rawliteral(...)rawliteral" → 原样嵌入HTML
├─ CSS三大杀招:渐变背景 / 圆角卡片 / 阴影悬浮
├─ HTTP路由:server.on("路径", 函数)
├─ URL参数:?r=255&g=0&b=0
└─ FastLED.show() → 唯一真正控制硬件
状态管理:
lightOn(开关) → currentColor(颜色) → brightness(亮度)
↓ ↓ ↓
记忆状态 记忆颜色 记忆亮度副板书(随写随擦):
本文标签:#少儿编程 #科创 #电子爱好者 #物联网 #arduino #ESP32
![]() | ![]() | ![]() |
关注我们,方便学习和答疑
如果还需完整代码,请联系我