,
项目 | 内容 |
课程名称 | 可变速呼吸灯 + 手机控制 |
课时安排 | 2课时(90分钟) |
授课对象 | 初中/高中信息技术、创客社团 |
前置知识 | 已完成第5课(基础WiFi控制灯条) |
核心素养 | 计算思维、数字化学习与创新、信息意识 |
setBrightness()millis() 替代 delay())类型 | 内容 | 突破策略 |
重点 | 呼吸效果的正弦波算法实现 | 可视化波形图辅助理解 |
难点 |
| 生活类比(看手表 vs 闭眼数秒) |
难点 | 前后端数据交互(URL参数传递) | 浏览器开发者工具抓包演示 |
老师们在此环节达成共识,统一教学策略
BUG现象 | 原因分析 | 教学应对 |
呼吸灯不亮 |
| 强调状态变量的作用 |
呼吸卡顿 |
| 引出 |
颜色切换后呼吸失效 | 只在 | 讲解分支逻辑的完整性 |
手机连不上 | WiFi密码错误或防火墙 | 提前准备热点备用方案 |
【教师操作】 打开第5课代码,快速回顾:
// ============================================// 第5课回顾:基础WiFi灯条控制框架// ============================================#include <WiFi.h>#include <WebServer.h>#include <FastLED.h>#define LED_PIN 2 // LED数据引脚接GPIO2#define NUM_LEDS 10 // 灯条共10颗灯珠const char* ssid = "你的WiFi名"; // ⚠️ 改成你的热点名称const char* password = "你的密码"; // ⚠️ 改成你的密码CRGB leds[NUM_LEDS]; // 定义LED数组,存储每颗灯的颜色WebServer server(80); // 创建Web服务器,端口80bool lightOn = true; // 灯的总开关状态CRGB currentColor = CRGB(255, 255, 255); // 当前颜色:白色int brightness = 128; // 当前亮度:128/255 ≈ 50%// ...(此处保留第5课的HTML页面和handle函数)【即时测试】 编译上传,手机访问IP地址,确认开关灯、调色、调亮度正常。
【教师提问】 "如果我想让灯像人呼吸一样慢慢变亮再慢慢变暗,怎么实现?"
【教师边写边讲】
// ============================================// 新增:呼吸模式相关变量// ============================================bool breathing = false; // 呼吸模式开关:默认关闭int breatheSpeed = 3000; // 呼吸周期:3000毫秒 = 3秒完成一次呼吸 // 数值越小呼吸越快,越大越慢【教师讲解】
"这里我们用两个变量控制呼吸。breathing是开关,就像电风扇的摇头按钮;breatheSpeed是速度,就像风扇的档位。"
【即时测试】 编译上传,确认无报错(此时呼吸功能尚未启用,只是变量声明)。
【教师边写边讲】
// ============================================// 函数:updateBreathing()// 作用:计算当前时刻的呼吸亮度并更新LED// 注意:此函数每20毫秒调用一次,实现平滑动画// ============================================void updateBreathing() { // 【保护条件】如果呼吸关闭或灯总开关关闭,直接返回,不做任何操作 if(!breathing || !lightOn) return; // 1. 获取当前时间(毫秒),从程序启动开始累计 unsigned long currentTime = millis(); // 2. 计算当前处于呼吸周期的哪个位置(0.0 ~ 1.0) // 例如:breatheSpeed=3000,当前时间=4500ms // 4500 % 3000 = 1500,1500/3000 = 0.5,正好在周期中点 float position = (currentTime % (unsigned long)breatheSpeed) / (float)breatheSpeed; // 3. 用正弦函数计算亮度系数(0.0 ~ 1.0) // sin(0) = 0, sin(PI/2) = 1, sin(PI) = 0 // 这样实现:暗→亮→暗 的一个完整呼吸波形 float sineValue = sin(position * PI); // 4. 将0.0~1.0映射到0~255的亮度值 int breatheBrightness = (int)(sineValue * 255); // 5. 应用亮度并刷新LED FastLED.setBrightness(breatheBrightness); setAllLeds(currentColor); // 保持当前颜色,只改变亮度}【关键板书/投屏】
时间(ms) 0 750 1500 2250 3000 │ │ │ │ │position 0 0.25 0.5 0.75 1.0 │ │ │ │ │sin(π*pos) 0 0.71 1 0.71 0 │ │ │ │ │亮度 0 180 255 180 0 ▼ ▼ ▼ ▼ ▼ 灭 较亮 最亮 较亮 灭【教师提问】 "为什么用millis()而不用delay()?"
【学生思考后教师解答】
"如果用delay(3000),这3秒内服务器完全卡死,手机点任何按钮都没反应。millis()就像看手表——瞄一眼就知道过了多久,不耽误干别的事。"
【即时测试】 在loop()中临时调用updateBreathing(),强制breathing = true,观察灯效。
【教师边写边讲】
// ============================================// loop()主循环:处理客户端 + 定时更新呼吸// ============================================void loop() { // 1. 必须持续调用,否则Web服务器无法响应手机请求 server.handleClient(); // 2. 使用静态变量记录上次更新时间 // static:变量只初始化一次,值会保持到下次进入函数 static unsigned long lastUpdate = 0; // 3. 每20毫秒更新一次呼吸效果 // 1000ms/20ms = 50帧/秒,人眼看起来就是流畅动画 if(millis() - lastUpdate > 20) { updateBreathing(); // 执行呼吸亮度计算 lastUpdate = millis(); // 记录本次更新时间 } // 4. 短暂释放CPU,避免看门狗复位 delay(1);}【即时测试】 编译上传,确认Web控制仍然响应迅速,同时LED呼吸流畅。
【教师边写边讲】 在HTML的<body>中,"开关控制"卡片下方插入:
<!-- ========================================== --><!-- 呼吸模式控制卡片 --><!-- ========================================== --><div class="card"> <div class="card-title">🫁 呼吸模式</div> <!-- 呼吸开关按钮 --> <button class="btn breathing" onclick="toggleBreathe()">开关呼吸</button> <!-- 呼吸状态文字显示 --> <div class="breathe-status" id="breatheStatus">呼吸模式: 关闭</div> <!-- 呼吸速度调节 --> <div class="card-title" style="margin-top:15px">⏱️ 呼吸速度</div> <!-- range滑块:min=500ms(很快), max=5000ms(很慢), 默认3000ms --> <input type="range" class="slider" min="500" max="5000" value="3000" onchange="setBreatheSpeed(this.value)"> <!-- 显示当前周期,单位转换为秒 --> <div class="status">周期: <span id="speedVal">3.0</span>秒</div></div>【即时测试】 刷新手机页面,确认新卡片显示正常,按钮和滑块可交互。
【教师边写边讲】 在<script>标签内添加:
// ============================================// 呼吸模式前端控制// ============================================// 记录当前呼吸状态(前端本地状态,用于切换显示)let isBreathing = false;/** * 切换呼吸开关 * 点击按钮时:切换本地状态 → 发送请求给ESP32 → 更新页面文字 */function toggleBreathe() { // 1. 翻转状态:开→关,关→开 isBreathing = !isBreathing; // 2. 发送请求到ESP32,URL格式:/breathe?state=1 或 /breathe?state=0 fetch('/breathe?state=' + (isBreathing ? '1' : '0')); // 3. 更新页面显示文字 document.getElementById('breatheStatus').textContent = '呼吸模式: ' + (isBreathing ? '开启 🫁' : '关闭');}/** * 设置呼吸速度 * @param {number} val - 周期毫秒数,范围500~5000 */function setBreatheSpeed(val) { // 1. 发送速度值到ESP32,URL格式:/breathespeed?s=3000 fetch('/breathespeed?s=' + val); // 2. 将毫秒转换为秒显示,保留1位小数 document.getElementById('speedVal').textContent = (val / 1000).toFixed(1);}【教师讲解】
"这里有两个关键点:一是fetch()发送请求,二是URL参数的格式。/breathe?state=1中,?后面是参数,state是参数名,1是值。"
【即时测试】 点击呼吸按钮,观察浏览器Network面板(F12)是否发出请求。
【教师边写边讲】 在Arduino代码中添加:
// ============================================// 处理:/breathe?state=1 或 /breathe?state=0// 作用:开启或关闭呼吸模式// ============================================void handleBreathe() { // 检查URL中是否有"state"参数 if(server.hasArg("state")) { // 读取参数值并转换为整数,等于1则开启呼吸 breathing = server.arg("state").toInt() == 1; // 【重要】如果关闭呼吸,且灯是开着的,恢复为正常常亮状态 if(!breathing && lightOn) { FastLED.setBrightness(brightness); // 恢复用户设定的亮度 setAllLeds(currentColor); // 恢复当前颜色 } } server.send(200, "text/plain", "OK");}// ============================================// 处理:/breathespeed?s=3000// 作用:修改呼吸周期// ============================================void handleBreatheSpeed() { if(server.hasArg("s")) { // 将字符串参数转换为整数 breatheSpeed = server.arg("s").toInt(); } server.send(200, "text/plain", "OK");}【即时测试】 完整编译上传,手机端全流程测试:
【教师带领学生梳理】
📁 项目结构├── 🔌 硬件层:FastLED控制WS2812B灯条├── 🌐 网络层:WiFi + WebServer(端口80)├── 🎨 表现层:HTML/CSS/JS手机控制页面└── ⚙️ 业务层:状态变量 + 处理函数 + 呼吸算法状态变量联动关系:lightOn ──┬── true ──→ 允许呼吸/允许常亮 └── false ──→ 强制熄灭(无论breathing状态)breathing ──┬── true ──→ updateBreathing()接管亮度控制 └── false ──→ 恢复用户设置的brightness难度 | 任务 | 提示 |
⭐ | 修改呼吸波形为"先快后慢" | 使用 |
⭐⭐ | 添加"彩虹呼吸"模式 | 在 |
⭐⭐⭐ | 多设备同步呼吸 | 引入NTP时间同步,所有设备用同一 |
┌─────────────────────────────────────────┐│ 呼吸灯核心公式 ││ ││ 亮度 = sin( (时间 % 周期) / 周期 × π ) × 255 ││ ││ ↓ 暗 → ↑ 亮 → ↓ 暗 ││ │├─────────────────────────────────────────┤│ 非阻塞定时器模板 ││ ││ static unsigned long last = 0; ││ if(millis() - last > 间隔) { ││ 执行任务(); ││ last = millis(); ││ } ││ │└─────────────────────────────────────────┘map()函数实现呼吸速度与滑块非线性映射(低速区更精细)班级 | 出现的问题 | 解决策略 | 效果评估 | 记录人 |
略。。
本文标签:#少儿编程 #科创 #电子爱好者 #物联网 #arduino #ESP32
![]() | ![]() | ![]() |
关注我们,方便学习和答疑