智幻走马灯
当千年传统遇见数字制造。
项目概览:传统美学与现代科技的完美融合
项目缘起
还记得小时候第一次见到走马灯时的惊艳吗?烛火摇曳中,剪纸人影在灯罩上翩翩起舞,仿佛有了生命。这个始于汉代的古老玩意儿,用最简单的物理原理——热气流上升——创造出了诗意的视觉体验。
但作为一个在 Fab Lab 摸爬滚打了18周的"数字工匠",我忍不住想:如果让这盏千年古灯插上现代科技的翅膀,会碰撞出怎样的火花?
于是,智幻走马灯诞生了。
左:传统走马灯的诗意;右:智幻走马灯的科技美学
它到底是什么?
简单说,这是一个会"读心术"的智能灯笼。
核心功能一览
🎯精准旋转控制:告别不可控的热气流,用步进电机实现精确到转速的控制。想要慢悠悠地冥想,还是快节奏地狂欢?一个手势搞定。
🌈可编程灯光魔法:28颗 WS2812B RGB LED 排成正反两列,360°无死角照明。从温暖的烛光黄到梦幻的彩虹渐变,千变万化只在弹指间。
👋手势识别:最多支持三个 APDS-9960 传感器呈120°分布,无论你从哪个角度"撩"它,它都能秒懂你的意图。上下左右,每个手势都有专属的灯光回应。
📱Web 远程控制:手机、电脑、平板,只要能上网就能控制。朋友聚会时,大家一起"指挥"灯笼表演,绝对是全场焦点。
🔗多设备同步:基于 MQTT 协议,多个智幻走马灯可以组成"灯阵",同步变化,场面相当震撼。
功能演示:手势控制、Web界面、同步效果
结构设计
物理结构设计
灯笼的结构占用了大量时间,因为灯笼要有外部框架,还有内部旋转结构,由电机驱动的齿轮机构等,把这些系统整合在一起花费了大量的时间。
我使用 Fusion 360 设计了整个灯笼结构体,包括复杂的齿轮系统和装配设计:
- 设计文件下载:https://a360.co/43zU9ao
根据设计文件渲染的效果图如下:
最终设计版本的效果图,由 Fusion 360 设计
然后我在 Fusion 里复制了一个文件,将所有结构零件展开如下图所示:
在 Fusion 360 里展开所有零件并渲染获得插图,然后我对需要制造的结构件文件都进行了编号
结构零件列表
结构件编号 | 零件名称 | 数量 | 功能说明 | 制造方式 | 材质 |
---|---|---|---|---|---|
手势传感器(Gesture Sensor) | 3 | 最多支撑安装 3 个手势传感器,用来感受各个方向的手势,直接控制灯效和电机转动 | 购买 | ||
C4 | 顶盖(Top Cover) | 1 | 圆筒形,位于灯笼最顶部,有 3 个 120 度分布的手势传感器支架,和 PCB 舱通过嵌套固定 | 3D 打印 | PLC/PETG |
XIAO ESP32C3 | 1 | 系统核心控制器 | 购买 | ||
半圆 PCB(Semicircle PCB) | 1 | 电子硬件开发板,通过定位固定孔置于 PCB 舱内 | 铣削/嘉立创生产 | 覆铜板/FR4 | |
C3 | PCB 舱(PCB Compartment) | 1 | 圆筒形承载 PCB 板的结构,和齿轮舱通过嵌套固定 | 3D 打印 | PLC/PETG |
C2A | 齿轮 A m1.1x16(Gear A m1.1x16) | 1 | 模数 1.1mm,16 齿,孔径 4.25mm,通过 4mm 钢轴固定;齿轮 B 通过齿轮 A 驱动转笼轴部齿轮 | 3D 打印 | PLC |
C2B | 齿轮 B m1.1x30(Gear B m1.1x30) | 1 | 模数 1.1mm,30 齿,孔径 4.25mm,通过 4mm 钢轴固定;齿轮 C 通过齿轮 B 驱动齿轮 A | 3D 打印 | PLC |
C2C | 齿轮 C m1.1x8(Gear C m1.1x8) | 1 | 模数 1.1mm,8 齿,连接电机轴,电机转动也带动齿轮 C 驱动 | 3D 打印 | PLC |
4mm直径钢轴(4mm Diameter Steel Shaft) | 2 | 支撑齿轮 | 购买 | ||
电机(Motor) | 1 | 驱动齿轮组,进而驱动灯笼转笼 | 购买 | ||
可充电电池(Rechargeable Battery) | 1 | 为系统供电 | 购买 | ||
C1 | 齿轮舱(Gear Compartment) | 1 | 支撑 3 个齿轮,内有电机盒和电池舱;中间有圆孔,可以让灯笼转动轴的齿轮穿过,和齿轮舱内齿轮连接;同外侧栏通过槽孔固定 | 3D 打印 | PLC/PETG |
B6 | 转笼顶盖与传动轴(Rotating Cage Top Cover and Drive Shaft) | 1 | 顶盖中轴带齿轮,通过齿轮舱齿轮 A 驱动此结构带动整改转笼转动 | 3D 打印 | PLC |
B5 | 转笼外框(Rotating Cage Outer Frame) | 2 | 转笼上下各有一个,固定转笼顶盖,底盖与 6 根转笼支撑柱 | 3D 打印 | PLC/PETG |
LED 灯带(LED Light Strip) | 2 | 每个灯带由 14 颗 RGB LED 组成 | 购买 | ||
B4 | 转笼支撑柱(Rotating Cage Support Column) | 6 | 起到支撑转笼和固定灯笼罩板的作用 | 3D 打印 | PLC |
B3 | 灯笼罩板(Lantern Cover Panel) | 6 | 1mm 厚半薄板,6 块板上可以贴乙烯基切割的贴纸图案,可以根据自己喜欢的主题 DIY | 3D 打印 | PLC/PETG |
B2 | 转笼底板(Rotating Cage Bottom Plate) | 1 | 下底部有一个圆锥凸起,可以放入下支撑底盘的圆柱约束内 | 3D 打印 | PLC |
B1 | 转笼底部支架(Bottom bracket of the cage) | 1 | 顶部有圆柱固定孔,同外侧栏通过槽孔固定 | 3D 打印 | PLC |
A1 | 灯笼外侧护栏(Lantern Outer Side Rail) | 6 | 激光切割获得 | 激光切割 | 3mm 木板 |
A2 | 灯笼外围栏(Lantern Outer Perimeter Rail) | 2 | 上下 2 个约束灯笼外侧护栏 | 激光切割 | 3mm 木板 |
结构件制造
制造文件导出
根据编号,我导出了制造结构部分所需的所有文件,目录结构如下。
结构部分所需的所有零件文件
下载结构文件压缩包:Magical Revolving Lantern Structure.zip
A1-A2 激光切割
在激光切割软件下导入 A1-Lantern Outer Side Rail.dxf (克隆为 6 个)和 A2-Lantern Outer Perimeter Rail.dxf(克隆为 2 个),对于 3mm 的椴木板,我设置 90%的激光强度,加工速度 15m/s。
在激光切割机软件中导入 A1 和 A2 文件,并克隆所需的数量,调整激光能量和速度
然后进行切割。
A1-A2 切割完成
B1-C4 3D 打印
压缩包里有个 Magical Revolving Lantern.3mf 文件,是拓竹打印机的项目存档,打开后可以看到所有 3D 打印件的分盘效果,方便逐盘切片打印。
拓竹打印机的项目多分盘存档
逐盘打印,用 Banbu LAB A1 打印机,逐盘打印了所有零件,B3 灯笼罩板我用了 PETE 的半透明材料,齿轮为了方便和齿轮舱区分,我用了金色的 PLA Slik 的料,其他都是白色 PLA 材料。
用 Banbu LAB A1 打印机,逐盘打印了所有零件
打印齿轮的时候,最好用最高品质打印(0.08mm High Quality)。因为我尝试过,默认的 0.2mm 会让齿轮中间的空洞变小,套在 4mm 钢轴上无法转动。
打印齿轮的时候建议选择最高品质
灯笼转笼罩的贴纸设计
灯笼的转笼罩,我使用了淡蓝色的半透明 PETG 材料打印,厚度 1mm,单片尺寸为 48×151 mm,总共有 6 片。我想使用乙烯基切割一些中国风格的剪纸图案贴在灯笼罩表面。
于是我尝试使用 https://www.lovart.ai/ 的在线工具,通过提示词和中国剪纸的参考图,提出了请求,在经过几次沟通后,得到了我需要的西游记剪纸风格图案,
https://www.lovart.ai/ 的在线工具可以通过我提供的剪纸参考图和提示词,生成系列高质量的西游记剪纸插图
然后我按照 6 片转笼灯罩的尺寸,规划了所需的图案,如下图所示。
根据 AI 生成的剪纸图案重新调整了大小,以适应转笼灯罩板的大小
获得了乙烯基切割机所需的图样,如下图所示。
乙烯基切割机所需的图样
电子硬件设计
智幻走马灯的 PCB 经过了 4 次大的迭代。
第 1 版:周课程电子项目验证
在我的 Fab Academy 课程的周项目中,我用 KiCad 设计了一个初级版本的 PCB,并分别用 CNC 切割和商业制造(嘉立创生产),初代版的这 2 个板子帮我完成了前 15 周的各种电子项目,验证了灯笼所需的各种电子功能。
为智幻走马灯设计的 1 代 PCB,为了方便测试灯效,我把 LED 阵列直径放在 PCB 上了,并分别尝试了 CNC 铣削和商业制造
第 2 版:铣削半圆形 PCB
智幻走马灯整个 PCB 开始规划为圆形,以正好放在齿轮舱上方,但如此电池缺少空间,所以根据结构采用了半圆环形 PCB 的集成设计,以为电池腾出空间。
第 2 版开始我尝试嘉立创 EDA,感觉因为它的库要丰富的多,也方便制造。先设计了一个铣削 PCB,目标是能够进行最基本的功能验证,但发现接线是个麻烦的事情。
第 2 版的 PCB 原理图
第 2 版 左边为嘉立创 EDA 设计的铣削 PCB 的设计图、中间为 CNC 输出的用于切削电路的 nc 文件转换的 PNG 图,右图为铣削出的 PCB
测试铣削的 PCB 的大小,能很好的和 PCB 舱契合。
测试第 2 版铣削的 PCB 的尺寸
我用这块 PCB 初步验证了 XIAO ESP32 C3、电池、开关、电机、LED 灯带和手势传感器,但主要的问题是接线比较复杂且不太稳定。
第 3 版:双面 PCB 尝试——各种失败教训
第 2 版尝试了更复杂的 PCB 设计(这个过程我写入了第 17 周的个人作业中),包括双面和增加线到板的插口支持。因为板子比较复杂,所以我通过嘉立创生产了 PCB,板子做出来很漂亮,我自信满满的焊上了所有的元件。
焊上的板子很好看,但现实是这个版本新增的设计出现了很多 bug,导致这个板子完全报废,主要的问题包括:
- XIAO 两排针脚插座的间距没有设置正确(大了约 3mm),导致 XIAO 无法插上插座;
- LED 灯带、手势传感器和电机的插座接口线序设置反了,导致插上后发现线序全部反了;
- AI 建议添加的肖特基二极管会产生电压降,导致无法通过电池给 XIAO 供电;
针对这些问题我又第 3 次修改了 PCB 设计。
第 4 版:双面 PCB 修订尝试——终于成功
针对第 17 周设计生产的 PCB 在实际焊接和测试中出现的问题,进行了一轮改进,最终完成的电路原理图如下所示。
第 4 版 PCB 设计电路原理图
在嘉立创 EDA 根据原理图重新调整了元件和线路,重新进行了制造,这次的元件焊接和测试都非常顺利。
使用嘉立创 EDA 进行布线并显示 3D 效果图
最终的模块连接图如下所示:
灯笼使用的电子硬件连接图
控制核心:XIAO ESP32C3 选择这块小家伙有三个理由:体积小巧、WiFi 内置、Arduino 兼容。在只有硬币大小的 PCB 上,它承担着整个系统的大脑职责。
感知系统:三眼神探 三个 APDS-9960 传感器呈 120° 分布,理论上能覆盖水平面的所有方向。每个传感器不仅能识别手势,还能检测距离和环境光强度,为后续的智能化留下了扩展空间。
执行机构:齿轮传动 + LED 阵列 机械部分使用了精密计算的齿轮系统,传动比经过优化,既保证了足够的扭矩,又控制了转速在合理范围。LED 部分由 2 各灯带组成(每个灯带有 14 颗 RGB LED),营造出层次丰富的光影效果。
对第 4 版的 PCB 进行测试,确保所有连接的设备都能够正常工作。
第 4 版 PCB 测试顺利
组装过程
有了结构件和 PCB 后,我们就可以进行组装了,下面是一个智幻走马灯所有的零件和元件。
智幻走马灯所有的零件和元件
组装外框结构 A1、A2 与 B1
在组装 A1 与 A2 的时候,可以借助热熔胶枪进行初步的固定(少量即可,放置 A1 在反转过程中掉落),一旦完成组装后,整个结构会非常紧实坚固。注意 B1 有一个圆柱状凸起要确保向上。
先组装使用激光切割木板的 A1 与 A2,然后将 B1 嵌入底部卡槽,注意 B1 的圆柱状槽要向上放置
组装灯笼转笼
这一步需要一些耐心,注意 B2 需要把有圆锥转轴的一面向下放置,以便转笼的底部圆锥轴能放入 B1 的圆柱槽内,组装好的转笼如下图所示。可以尝试将组装好的转笼放入第 1 步组装好的外框中,注意要让转笼底部的圆锥卡在下底座的圆柱槽内,这样可以用手轻松的转动转笼。
组装 B2、B4、B5 和 B6 得到一个结实的转笼,可以放入第 1 步组装的外框里试试
为转笼添加罩板和放置 LED 灯带
将转笼罩板(B3)小心的嵌入到转笼支撑柱侧面的槽中,当还剩 2 块板的时候,记得把 LED 灯条的较细的插头部分,从内部沿着转笼上方 B6 的轴孔穿出后,再封严最后两块罩板。
安装灯笼罩板(B3),记得还剩 2 块的时候,把 LED 灯带的接头从转笼上方 B6 轴孔穿出后,再安装剩下 2 块罩板封严灯笼
组装齿轮舱(C1)和 C2A 与 C2B 齿轮
将 B3 的 2 个轴孔插入 4mm 直径的钢轴,并分别放入 C2A 与 C2B 齿轮,然后将 B3 放入走马灯结构,让转笼带齿轮的轴从 B3 的中孔穿出,将 B3 向下推入木质灯笼框架的卡槽后,转笼真正变得稳定,此时可以用手转动转笼的中轴齿轮,应该可以看到转笼可以非常灵活的旋转,同时会带动 C2A 与 C2B 齿轮转动。
为齿轮舱(C1)的 2 个轴座插入 4mm 直径的钢轴,然后在钢轴上放置齿轮 C2B(最大的那个,位置靠近转笼中轴齿轮)和 C2A。将 C1 沿着木质框架推下,让转笼带齿轮的中轴从 C1 的中孔穿出,然后被木质侧栏的卡槽卡住
整合 PCB 舱与顶盖
首先将 PCB 沿着定位孔放置在 PCB 舱(C3),可以用热熔胶枪固定 PCB,然后将 Grove Mini FAN 的控制板也固定在 PCB 上,并用 Grove 线连接 PCB 和 控制板。控制板一端的 2P 电机线可以从 PCB 侧面的孔穿出,连接电机。然后将 1 只手势传感器的头部用热熔胶枪固定在 C4 预留的支架上,注意感应器探头面向外。
将 PCB 板置于 PCB 舱( C3),接好电机控制板,控制板一端的 2P 电机线可以从 PCB 侧面的孔穿出;将 1 只手势传感器的头部用热熔胶枪固定在 C4 预留的支架上
将电机转轴安装 C2C 齿轮,并放在电池舱。
将电机先放入电池仓
在安装 PCB 舱之前,记得把电机的 2P 电源线接上。
将电机 2P 线接上后,再安装 PCB 舱
小心的将 PCB 舱插在齿轮舱上方,然后连接 2 个 LED 灯条的接头。
安装 PCB 舱并插上 LED 灯条的接头
将电池插入,并将电池连接到 PCB 的插座上,注意红黑线不要反接。
连接电池
最后,将手势传感器的接头连接 PCB 上的插座,然后插上顶盖,打开开关,现在电机、灯带和手势传感器都工作了。
乙烯基贴纸切割与安装
打好的贴纸在妻子的帮助下,完成了 1 个图样的转印,最终切好的贴纸贴在了转笼的罩板上。
使用乙烯基切割机切出了剪纸风格的贴纸,然后在妻子帮助下转印到转笼的罩板上
交互设计
这部分在前面的课程已经经过验证和测试,对走马灯的控制通过 3 种途径:
- 灯笼的手势传感器
- 通过 Wi-Fi 进行 Web 控制
- 多个灯笼可以通过 Wi-Fi/MQTT 进行自动同步
灯笼可以 3 种方式进行控制
手势传感器控制
我集成了超迷你 APDS-9960 手势传感器,相关程序已经在第 9 周的作业进行了验证。在这里我调整了手势控制功能:
- 左右手势:在多个灯效模式间切换(传统走马灯、火焰效果、彩虹旋转、呼吸同步、流星追逐、节日庆典)
- 向上手势:开启走马灯(同时启动电机和灯效)
- 向下手势:关闭走马灯(停止电机和灯效)
通过 Wi-Fi 进行 Web 控制
相关程序已经在第 15 周个人作业进行了验证。 Final Project 中的 Web 控制,我打算就放 3 个按钮:
- 切换灯效(Switching lighting effects)
- 开启走马灯(Turn on the running light)
- 关闭走马灯(Turn off the running lights)
多个灯笼可以通过 Wi-Fi/MQTT 进行自动同步
相关程序已经在第 11 周个人作业进行了验证,多个登录会通过 Wi-Fi 借助 MQTT 协议自动进行同步。
软件架构
软件部分采用了多任务并发的架构设计,充分利用了 ESP32C3 的双核优势。
软件架构:多任务并发处理示意图
手势识别算法: 不是简单的方向判断,而是基于时序分析的复合手势识别。比如"画圆"手势会触发彩虹模式,"向上推"手势会逐渐增加亮度。
LED 控制策略: 实现了平滑的颜色过渡算法,避免了突兀的跳变。同时支持多种预设模式:呼吸灯、流水灯、频闪、渐变等。
通信协议设计: 结合了 WiFi 和 ESP-NOW 两种通信方式。日常控制用 WiFi(稳定、远距离),群组同步用 ESP-NOW(低延迟、无需路由器)。
分阶段测试程序
特别申明,本项目所有程序,均由 Claude Sonnet 4 编写。我通过向 AI 提供其他相关课程作业文档及需求后,由 AI 生成,然后测试改进获得。
电机开关测试程序
最开始只连接电机,并测试电机是否可以正常工作,测试程序如下:
#define MOTOR_PIN 10
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("Basic Motor Test - ON/OFF only");
pinMode(MOTOR_PIN, OUTPUT);
digitalWrite(MOTOR_PIN, LOW);
}
void loop() {
Serial.println("Motor ON");
digitalWrite(MOTOR_PIN, HIGH);
delay(3000);
Serial.println("Motor OFF");
digitalWrite(MOTOR_PIN, LOW);
delay(3000);
}
LED 灯效测试
然后只连接 1 个 LED 灯(GPIO9),测试灯效控制程序如下:
#include <Adafruit_NeoPixel.h>
#define PIN 9 // 改为GPIO 9
#define MAX_LED 14
Adafruit_NeoPixel strip = Adafruit_NeoPixel(MAX_LED, PIN, NEO_GRB + NEO_KHZ800);
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("Testing GPIO 9...");
strip.begin();
strip.setBrightness(20);
// 稳定初始化
strip.clear();
strip.show();
delay(100);
strip.clear();
strip.show();
delay(100);
}
void loop() {
// 简单的红绿蓝测试
Serial.println("RED");
for(int i = 0; i < MAX_LED; i++) {
strip.setPixelColor(i, strip.Color(50, 0, 0));
}
strip.show();
delay(1000);
Serial.println("GREEN");
for(int i = 0; i < MAX_LED; i++) {
strip.setPixelColor(i, strip.Color(0, 50, 0));
}
strip.show();
delay(1000);
Serial.println("BLUE");
for(int i = 0; i < MAX_LED; i++) {
strip.setPixelColor(i, strip.Color(0, 0, 50));
}
strip.show();
delay(1000);
Serial.println("OFF");
strip.clear();
strip.show();
delay(1000);
}
LED 灯效与电机程序
经过数次调试和修订,完成了电机和 2 个 LED 同步工作的测试程序:
#include <Adafruit_NeoPixel.h>
// 硬件定义
#define LED_PIN_1 9 // GPIO9 控制第一条LED灯带
#define LED_PIN_2 8 // GPIO8 控制第二条LED灯带
#define MOTOR_PIN 10 // GPIO10 控制电机
#define MAX_LED 14 // 14个LED
// 双LED灯带初始化
Adafruit_NeoPixel strip1 = Adafruit_NeoPixel(MAX_LED, LED_PIN_1, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel strip2 = Adafruit_NeoPixel(MAX_LED, LED_PIN_2, NEO_GRB + NEO_KHZ800);
// PWM参数
const int PWM_FREQ = 5000;
const int PWM_RESOLUTION = 8;
// 颜色定义(低亮度)
#define RED strip1.Color(50, 0, 0)
#define GREEN strip1.Color(0, 50, 0)
#define BLUE strip1.Color(0, 0, 50)
#define YELLOW strip1.Color(50, 50, 0)
#define PURPLE strip1.Color(50, 0, 50)
#define CYAN strip1.Color(0, 50, 50)
#define WHITE strip1.Color(30, 30, 30)
#define ORANGE strip1.Color(60, 20, 0)
#define OFF strip1.Color(0, 0, 0)
// 当前模式
int currentMode = 0;
const int TOTAL_MODES = 6;
// 辅助函数:同时设置两条LED灯带的像素颜色
void setDualPixelColor(int pixel, uint32_t color) {
strip1.setPixelColor(pixel, color);
strip2.setPixelColor(pixel, color);
}
// 辅助函数:同时显示两条LED灯带
void showDualStrips() {
strip1.show();
strip2.show();
}
// 辅助函数:同时清空两条LED灯带
void clearDualStrips() {
strip1.clear();
strip2.clear();
}
// 辅助函数:创建颜色(两条灯带兼容)
uint32_t createColor(int r, int g, int b) {
return strip1.Color(r, g, b);
}
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("=== 智幻走马灯 双LED版本 启动 ===");
Serial.println("Smart Lantern - Dual LED & Motor Integration");
// 初始化两条LED灯带
strip1.begin();
strip1.setBrightness(20);
strip1.clear();
strip1.show();
strip2.begin();
strip2.setBrightness(20);
strip2.clear();
strip2.show();
delay(100);
clearDualStrips();
showDualStrips();
// 初始化电机PWM
ledcAttach(MOTOR_PIN, PWM_FREQ, PWM_RESOLUTION);
ledcWrite(MOTOR_PIN, 0);
// 启动动画
startupSequence();
Serial.println("\n双LED系统就绪!开始运行效果...\n");
}
void loop() {
switch(currentMode) {
case 0:
Serial.println("模式1: 传统走马灯 (双LED)");
traditionalLanternMode();
break;
case 1:
Serial.println("模式2: 火焰效果 (双LED)");
fireMode();
break;
case 2:
Serial.println("模式3: 彩虹旋转 (双LED)");
rainbowSpinMode();
break;
case 3:
Serial.println("模式4: 呼吸同步 (双LED)");
breathingSyncMode();
break;
case 4:
Serial.println("模式5: 流星追逐 (双LED)");
meteorChaseMode();
break;
case 5:
Serial.println("模式6: 节日庆典 (双LED)");
festivalMode();
break;
}
// 切换到下一个模式
currentMode = (currentMode + 1) % TOTAL_MODES;
// 模式间过渡
transitionEffect();
}
// 启动动画
void startupSequence() {
Serial.println("启动序列 - 双LED同步...");
// 电机缓慢启动
for(int speed = 0; speed <= 100; speed += 5) {
ledcWrite(MOTOR_PIN, speed);
delay(30);
}
// 两条LED灯带逐个同步点亮
for(int i = 0; i < MAX_LED; i++) {
setDualPixelColor(i, WHITE);
showDualStrips();
delay(50);
}
delay(500);
// 全部熄灭
clearDualStrips();
showDualStrips();
ledcWrite(MOTOR_PIN, 0);
delay(500);
}
// 模式1: 传统走马灯效果
void traditionalLanternMode() {
uint32_t colors[] = {RED, ORANGE, YELLOW, GREEN, CYAN, BLUE};
int numColors = 6;
// 电机中速旋转
ledcWrite(MOTOR_PIN, 120);
// 运行30秒
unsigned long startTime = millis();
int offset = 0;
while(millis() - startTime < 30000) {
// 双LED色彩流动
for(int i = 0; i < MAX_LED; i++) {
int colorIndex = (i + offset) % numColors;
setDualPixelColor(i, colors[colorIndex]);
}
showDualStrips();
offset++;
delay(200); // 配合电机转速
}
// 缓慢停止
for(int speed = 120; speed >= 0; speed -= 5) {
ledcWrite(MOTOR_PIN, speed);
delay(50);
}
}
// 模式2: 火焰效果
void fireMode() {
// 电机慢速旋转,模拟火焰摇曳
ledcWrite(MOTOR_PIN, 80);
unsigned long startTime = millis();
while(millis() - startTime < 30000) {
// 火焰闪烁效果
for(int i = 0; i < MAX_LED; i++) {
int flicker = random(20, 80);
int r = flicker;
int g = flicker * 0.3;
int b = 0;
setDualPixelColor(i, createColor(r, g, b));
}
showDualStrips();
// 电机速度随机变化
int motorSpeed = 60 + random(-20, 20);
ledcWrite(MOTOR_PIN, motorSpeed);
delay(50);
}
ledcWrite(MOTOR_PIN, 0);
}
// 模式3: 彩虹旋转
void rainbowSpinMode() {
unsigned long startTime = millis();
int colorOffset = 0;
int loopCount = 0;
while(millis() - startTime < 30000) {
// 双LED显示彩虹 - 使用简单的RGB循环
for(int i = 0; i < MAX_LED; i++) {
// 创建彩虹效果
int phase = ((i * 255 / MAX_LED) + (colorOffset * 10)) % 255;
int r = 0, g = 0, b = 0;
if(phase < 85) {
// 红到绿
r = (255 - phase * 3) / 5; // 直接除以5降低亮度
g = (phase * 3) / 5;
b = 0;
} else if(phase < 170) {
// 绿到蓝
phase -= 85;
r = 0;
g = (255 - phase * 3) / 5;
b = (phase * 3) / 5;
} else {
// 蓝到红
phase -= 170;
r = (phase * 3) / 5;
g = 0;
b = (255 - phase * 3) / 5;
}
setDualPixelColor(i, createColor(r, g, b));
}
showDualStrips();
// 电机速度随彩虹变化
int motorSpeed = 120; // 固定速度
ledcWrite(MOTOR_PIN, motorSpeed);
colorOffset = (colorOffset + 1) % 26; // 循环范围
delay(100); // 延时
// 调试输出
loopCount++;
if(loopCount % 10 == 0) {
Serial.print(".");
}
}
ledcWrite(MOTOR_PIN, 0);
}
// 模式4: 呼吸同步
void breathingSyncMode() {
unsigned long startTime = millis();
while(millis() - startTime < 30000) {
// 呼吸循环
// 渐亮 + 加速
for(int level = 0; level <= 100; level += 2) {
// 双LED亮度
int brightness = level / 2;
for(int i = 0; i < MAX_LED; i++) {
setDualPixelColor(i, createColor(brightness, brightness/2, 0));
}
showDualStrips();
// 电机速度
ledcWrite(MOTOR_PIN, 50 + level);
delay(30);
}
// 渐暗 + 减速
for(int level = 100; level >= 0; level -= 2) {
// 双LED亮度
int brightness = level / 2;
for(int i = 0; i < MAX_LED; i++) {
setDualPixelColor(i, createColor(brightness, brightness/2, 0));
}
showDualStrips();
// 电机速度
ledcWrite(MOTOR_PIN, 50 + level);
delay(30);
}
}
ledcWrite(MOTOR_PIN, 0);
}
// 模式5: 流星追逐
void meteorChaseMode() {
// 电机快速旋转
ledcWrite(MOTOR_PIN, 180);
unsigned long startTime = millis();
int position = 0;
while(millis() - startTime < 30000) {
clearDualStrips();
// 主流星
setDualPixelColor(position, WHITE);
// 尾迹
if(position > 0) {
setDualPixelColor((position - 1) % MAX_LED, createColor(20, 20, 20));
}
if(position > 1) {
setDualPixelColor((position - 2) % MAX_LED, createColor(10, 10, 10));
}
if(position > 2) {
setDualPixelColor((position - 3) % MAX_LED, createColor(5, 5, 5));
}
showDualStrips();
position = (position + 1) % MAX_LED;
delay(50);
}
// 减速停止
for(int speed = 180; speed >= 0; speed -= 5) {
ledcWrite(MOTOR_PIN, speed);
delay(30);
}
}
// 模式6: 节日庆典
void festivalMode() {
uint32_t festiveColors[] = {RED, GREEN, YELLOW, BLUE, PURPLE};
// 电机中速旋转
ledcWrite(MOTOR_PIN, 130);
unsigned long startTime = millis();
while(millis() - startTime < 30000) {
// 随机闪烁
for(int i = 0; i < MAX_LED; i++) {
if(random(10) > 7) { // 30%概率闪烁
setDualPixelColor(i, festiveColors[random(5)]);
} else {
setDualPixelColor(i, OFF);
}
}
showDualStrips();
// 电机偶尔变速
if(random(100) > 90) {
ledcWrite(MOTOR_PIN, 100 + random(60));
}
delay(100);
}
ledcWrite(MOTOR_PIN, 0);
}
// 模式间过渡效果
void transitionEffect() {
Serial.println("切换模式 - 双LED同步...");
// 停止电机
ledcWrite(MOTOR_PIN, 0);
// 渐暗所有LED
for(int brightness = 20; brightness >= 0; brightness--) {
for(int i = 0; i < MAX_LED; i++) {
setDualPixelColor(i, createColor(brightness, brightness, brightness));
}
showDualStrips();
delay(50);
}
clearDualStrips();
showDualStrips();
delay(1000);
}
添加多手势传感器的测试程序
#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include "XLOT_APDS9960AD.h"
// 硬件定义
#define LED_PIN_1 9 // GPIO9 控制第一条LED灯带
#define LED_PIN_2 8 // GPIO8 控制第二条LED灯带
#define MOTOR_PIN 10 // GPIO10 控制电机
#define MAX_LED 14 // 14个LED
// I2C引脚定义(用于手势传感器)
#define SDA_PIN 6 // GPIO6 (D4) - I2C数据线
#define SCL_PIN 7 // GPIO7 (D5) - I2C时钟线
// 双LED灯带初始化
Adafruit_NeoPixel strip1 = Adafruit_NeoPixel(MAX_LED, LED_PIN_1, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel strip2 = Adafruit_NeoPixel(MAX_LED, LED_PIN_2, NEO_GRB + NEO_KHZ800);
// 手势传感器初始化
XLOT_APDS9960AD apds;
// PWM参数
const int PWM_FREQ = 5000;
const int PWM_RESOLUTION = 8;
// 颜色定义(低亮度)
#define RED strip1.Color(50, 0, 0)
#define GREEN strip1.Color(0, 50, 0)
#define BLUE strip1.Color(0, 0, 50)
#define YELLOW strip1.Color(50, 50, 0)
#define PURPLE strip1.Color(50, 0, 50)
#define CYAN strip1.Color(0, 50, 50)
#define WHITE strip1.Color(30, 30, 30)
#define ORANGE strip1.Color(60, 20, 0)
#define OFF strip1.Color(0, 0, 0)
// 系统状态控制
bool lanternOn = false; // 走马灯开关状态
int currentMode = 0; // 当前灯效模式
const int TOTAL_MODES = 6; // 总模式数量
int currentBrightness = 20; // 当前亮度
// 模式控制变量
unsigned long modeStartTime = 0; // 模式开始时间
int modeOffset = 0; // 模式内部偏移量
// 手势控制防抖
unsigned long lastGestureTime = 0;
const unsigned long GESTURE_DELAY = 800; // 手势间隔800ms
// 模式名称数组(用于调试输出)
const char* modeNames[TOTAL_MODES] = {
"传统走马灯",
"火焰效果",
"彩虹旋转",
"呼吸同步",
"流星追逐",
"节日庆典"
};
// 辅助函数:同时设置两条LED灯带的像素颜色
void setDualPixelColor(int pixel, uint32_t color) {
strip1.setPixelColor(pixel, color);
strip2.setPixelColor(pixel, color);
}
// 辅助函数:同时显示两条LED灯带
void showDualStrips() {
strip1.show();
strip2.show();
}
// 辅助函数:同时清空两条LED灯带
void clearDualStrips() {
strip1.clear();
strip2.clear();
}
// 辅助函数:创建颜色(两条灯带兼容)
uint32_t createColor(int r, int g, int b) {
return strip1.Color(r, g, b);
}
// 手势处理函数
void handleGestures() {
uint8_t gesture = apds.readGesture();
if (gesture != 0) {
// 手势防抖处理
if (millis() - lastGestureTime < GESTURE_DELAY) {
return;
}
lastGestureTime = millis();
switch (gesture) {
case APDS9960_LEFT:
Serial.println("🔄 手势: ← (切换到上一个模式)");
switchMode(-1);
break;
case APDS9960_RIGHT:
Serial.println("🔄 手势: → (切换到下一个模式)");
switchMode(1);
break;
case APDS9960_UP:
Serial.println("🔆 手势: ↑ (开启走马灯)");
turnOnLantern();
break;
case APDS9960_DOWN:
Serial.println("🔅 手势: ↓ (关闭走马灯)");
turnOffLantern();
break;
}
}
}
// 切换模式函数
void switchMode(int direction) {
// 只有在走马灯开启状态下才能切换模式
if (!lanternOn) {
Serial.println(" ⚠️ 走马灯未开启,请先向上滑动开启");
return;
}
// 计算新模式
currentMode += direction;
if (currentMode < 0) {
currentMode = TOTAL_MODES - 1;
} else if (currentMode >= TOTAL_MODES) {
currentMode = 0;
}
// 重置模式时间和偏移
modeStartTime = millis();
modeOffset = 0;
Serial.print(" ✅ 切换到模式 ");
Serial.print(currentMode + 1);
Serial.print(": ");
Serial.println(modeNames[currentMode]);
// 显示模式切换指示
showModeIndicator();
}
// 开启走马灯
void turnOnLantern() {
if (lanternOn) {
Serial.println(" ℹ️ 走马灯已经开启");
return;
}
lanternOn = true;
modeStartTime = millis();
modeOffset = 0;
Serial.println(" ✅ 走马灯已开启");
Serial.print(" 🎨 当前模式: ");
Serial.println(modeNames[currentMode]);
// 显示开启动画
showStartupAnimation();
}
// 关闭走马灯
void turnOffLantern() {
if (!lanternOn) {
Serial.println(" ℹ️ 走马灯已经关闭");
return;
}
lanternOn = false;
Serial.println(" ✅ 走马灯已关闭");
// 显示关闭动画
showShutdownAnimation();
// 停止电机和清空LED
ledcWrite(MOTOR_PIN, 0);
clearDualStrips();
showDualStrips();
}
// 显示模式切换指示
void showModeIndicator() {
clearDualStrips();
// 根据模式显示不同颜色的指示
uint32_t indicatorColor;
switch(currentMode) {
case 0: indicatorColor = ORANGE; break; // 传统走马灯 - 橙色
case 1: indicatorColor = RED; break; // 火焰效果 - 红色
case 2: indicatorColor = createColor(25, 25, 25); break; // 彩虹旋转 - 白色
case 3: indicatorColor = YELLOW; break; // 呼吸同步 - 黄色
case 4: indicatorColor = CYAN; break; // 流星追逐 - 青色
case 5: indicatorColor = PURPLE; break; // 节日庆典 - 紫色
}
// 显示模式编号对应的LED数量
for (int i = 0; i <= currentMode; i++) {
setDualPixelColor(i, indicatorColor);
}
showDualStrips();
delay(800);
clearDualStrips();
showDualStrips();
delay(200);
}
// 显示开启动画
void showStartupAnimation() {
Serial.println(" 🎬 播放开启动画...");
// LED从中心向外扩散
for (int i = 0; i < MAX_LED; i++) {
setDualPixelColor(i, GREEN);
showDualStrips();
delay(80);
}
delay(300);
// 电机缓慢启动
for (int speed = 0; speed <= 120; speed += 10) {
ledcWrite(MOTOR_PIN, speed);
delay(50);
}
clearDualStrips();
showDualStrips();
}
// 显示关闭动画
void showShutdownAnimation() {
Serial.println(" 🎬 播放关闭动画...");
// 电机缓慢停止
int currentSpeed = 120;
while (currentSpeed > 0) {
ledcWrite(MOTOR_PIN, currentSpeed);
currentSpeed -= 10;
delay(100);
}
ledcWrite(MOTOR_PIN, 0);
// LED从外向内熄灭
for (int i = MAX_LED - 1; i >= 0; i--) {
setDualPixelColor(i, OFF);
showDualStrips();
delay(80);
}
}
// 运行当前模式的灯效
void runCurrentMode() {
if (!lanternOn) {
return; // 走马灯关闭状态下不运行任何效果
}
switch(currentMode) {
case 0:
traditionalLanternMode();
break;
case 1:
fireMode();
break;
case 2:
rainbowSpinMode();
break;
case 3:
breathingSyncMode();
break;
case 4:
meteorChaseMode();
break;
case 5:
festivalMode();
break;
}
}
// 模式1: 传统走马灯效果
void traditionalLanternMode() {
uint32_t colors[] = {RED, ORANGE, YELLOW, GREEN, CYAN, BLUE};
int numColors = 6;
// 电机中速旋转
ledcWrite(MOTOR_PIN, 120);
// 双LED色彩流动
for(int i = 0; i < MAX_LED; i++) {
int colorIndex = (i + modeOffset) % numColors;
setDualPixelColor(i, colors[colorIndex]);
}
showDualStrips();
// 每200ms更新一次偏移
if (millis() - modeStartTime > (modeOffset + 1) * 200) {
modeOffset++;
}
}
// 模式2: 火焰效果
void fireMode() {
// 电机慢速旋转,模拟火焰摇曳
int motorSpeed = 80 + random(-15, 15);
ledcWrite(MOTOR_PIN, motorSpeed);
// 火焰闪烁效果
for(int i = 0; i < MAX_LED; i++) {
int flicker = random(20, 80);
int r = flicker;
int g = flicker * 0.3;
int b = 0;
setDualPixelColor(i, createColor(r, g, b));
}
showDualStrips();
}
// 模式3: 彩虹旋转
void rainbowSpinMode() {
// 电机固定速度
ledcWrite(MOTOR_PIN, 120);
// 双LED显示彩虹
for(int i = 0; i < MAX_LED; i++) {
int phase = ((i * 255 / MAX_LED) + (modeOffset * 10)) % 255;
int r = 0, g = 0, b = 0;
if(phase < 85) {
r = (255 - phase * 3) / 5;
g = (phase * 3) / 5;
b = 0;
} else if(phase < 170) {
phase -= 85;
r = 0;
g = (255 - phase * 3) / 5;
b = (phase * 3) / 5;
} else {
phase -= 170;
r = (phase * 3) / 5;
g = 0;
b = (255 - phase * 3) / 5;
}
setDualPixelColor(i, createColor(r, g, b));
}
showDualStrips();
// 每100ms更新一次偏移
if (millis() - modeStartTime > (modeOffset + 1) * 100) {
modeOffset++;
if (modeOffset > 25) modeOffset = 0;
}
}
// 模式4: 呼吸同步
void breathingSyncMode() {
// 计算呼吸周期
unsigned long cycleTime = (millis() - modeStartTime) % 4000; // 4秒一个周期
int breathLevel = 0;
if (cycleTime < 2000) {
// 渐亮过程
breathLevel = map(cycleTime, 0, 2000, 0, 100);
} else {
// 渐暗过程
breathLevel = map(cycleTime, 2000, 4000, 100, 0);
}
// 双LED亮度
int brightness = breathLevel / 2;
for(int i = 0; i < MAX_LED; i++) {
setDualPixelColor(i, createColor(brightness, brightness/2, 0));
}
showDualStrips();
// 电机速度随呼吸变化
ledcWrite(MOTOR_PIN, 50 + breathLevel);
}
// 模式5: 流星追逐
void meteorChaseMode() {
// 电机快速旋转
ledcWrite(MOTOR_PIN, 180);
// 计算流星位置
int position = (modeOffset) % MAX_LED;
clearDualStrips();
// 主流星
setDualPixelColor(position, WHITE);
// 尾迹
for (int i = 1; i <= 3; i++) {
int tailPos = (position - i + MAX_LED) % MAX_LED;
int brightness = 30 - (i * 8);
if (brightness > 0) {
setDualPixelColor(tailPos, createColor(brightness, brightness, brightness));
}
}
showDualStrips();
// 每50ms更新一次位置
if (millis() - modeStartTime > (modeOffset + 1) * 50) {
modeOffset++;
}
}
// 模式6: 节日庆典
void festivalMode() {
uint32_t festiveColors[] = {RED, GREEN, YELLOW, BLUE, PURPLE};
// 电机中速旋转
ledcWrite(MOTOR_PIN, 130);
// 随机闪烁
for(int i = 0; i < MAX_LED; i++) {
if(random(10) > 7) { // 30%概率闪烁
setDualPixelColor(i, festiveColors[random(5)]);
} else {
setDualPixelColor(i, OFF);
}
}
showDualStrips();
// 电机偶尔变速
if(random(100) > 95) {
ledcWrite(MOTOR_PIN, 100 + random(60));
}
}
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("=== 智幻走马灯 手势控制系统 启动 ===");
Serial.println("Smart Lantern - Gesture Control System");
// 初始化I2C总线(用于手势传感器)
Wire.begin(SDA_PIN, SCL_PIN);
// 初始化手势传感器
if(!apds.begin()){
Serial.println("❌ 手势传感器初始化失败! 请检查接线.");
Serial.println(" 将以无手势控制模式运行...");
} 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灯带
strip1.begin();
strip1.setBrightness(currentBrightness);
strip1.clear();
strip1.show();
strip2.begin();
strip2.setBrightness(currentBrightness);
strip2.clear();
strip2.show();
// 初始化电机PWM
ledcAttach(MOTOR_PIN, PWM_FREQ, PWM_RESOLUTION);
ledcWrite(MOTOR_PIN, 0);
Serial.println("\n🎮 手势控制说明:");
Serial.println(" ↑ : 开启走马灯");
Serial.println(" ↓ : 关闭走马灯");
Serial.println(" ← : 切换到上一个灯效模式");
Serial.println(" → : 切换到下一个灯效模式");
Serial.println("\n💡 系统就绪,等待手势控制...");
// 显示就绪指示
for(int i = 0; i < 3; i++) {
setDualPixelColor(0, GREEN);
showDualStrips();
delay(200);
clearDualStrips();
showDualStrips();
delay(200);
}
}
void loop() {
// 检测手势
handleGestures();
// 运行当前模式(只有在开启状态下)
runCurrentMode();
// 短暂延迟
delay(50);
}
现在手势、灯带和电机都有反应了。
添加 Web 模块的测试程序
- Web界面特点
现代化设计:渐变背景、毛玻璃效果、阴影动画。
响应式布局:手机、平板、电脑都能完美显示。
实时状态显示:当前开关状态和模式名称。
三个核心按钮:
🔆 开启走马灯
🔅 关闭走马灯
🎨 切换灯效
- API接口
GET /api/status:获取当前状态(开关状态、模式信息)。
POST /api/control:发送控制命令(on/off/switch)。
- 智能交互
状态同步:Web界面每3秒自动更新状态。
操作反馈:每个操作都有成功/失败提示。
防误操作:关闭状态下不能切换模式(Web和手势一致)。
🔧 使用方法
修改WiFi信息:
cppconst char* ssid = "YourWiFiName"; // 改为您的WiFi名称
const char* password = "YourWiFiPassword"; // 改为您的WiFi密码
上传程序后:
串口监视器会显示IP地址
用浏览器访问该IP地址即可看到控制界面
双重控制:
手势控制:向上开启,向下关闭,左右切换模式
Web控制:点击按钮实现相同功能
#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
#include "XLOT_APDS9960AD.h"
// WiFi设置 - 请修改为您的WiFi信息
const char* ssid = "YourWiFiName";
const char* password = "YourWiFiPassword";
// Web服务器
AsyncWebServer server(80);
// 硬件定义
#define LED_PIN_1 9 // GPIO9 控制第一条LED灯带
#define LED_PIN_2 8 // GPIO8 控制第二条LED灯带
#define MOTOR_PIN 10 // GPIO10 控制电机
#define MAX_LED 14 // 14个LED
// I2C引脚定义
#define SDA_PIN 6 // GPIO6 - I2C数据线
#define SCL_PIN 7 // GPIO7 - I2C时钟线
// LED灯带
Adafruit_NeoPixel strip1 = Adafruit_NeoPixel(MAX_LED, LED_PIN_1, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel strip2 = Adafruit_NeoPixel(MAX_LED, LED_PIN_2, NEO_GRB + NEO_KHZ800);
// 手势传感器
XLOT_APDS9960AD apds;
// PWM参数
const int PWM_FREQ = 5000;
const int PWM_RESOLUTION = 8;
// 系统状态
bool lanternOn = false;
int currentMode = 0;
const int TOTAL_MODES = 6;
int currentBrightness = 20;
// 手势控制防抖
unsigned long lastGestureTime = 0;
const unsigned long GESTURE_DELAY = 800;
// 模式名称
const char* modeNames[TOTAL_MODES] = {
"传统走马灯", "火焰效果", "彩虹旋转",
"呼吸同步", "流星追逐", "节日庆典"
};
// 模式控制变量
unsigned long modeStartTime = 0;
int modeOffset = 0;
// 辅助函数
void setDualPixelColor(int pixel, uint32_t color) {
strip1.setPixelColor(pixel, color);
strip2.setPixelColor(pixel, color);
}
void showDualStrips() {
strip1.show();
strip2.show();
}
void clearDualStrips() {
strip1.clear();
strip2.clear();
}
uint32_t createColor(int r, int g, int b) {
return strip1.Color(r, g, b);
}
// 这个函数在当前程序中不需要,因为我们使用LED灯带而不是单独的LED
// void updateLEDs() {
// // 此函数用于单独LED控制,当前程序使用LED灯带
// }
// WiFi连接
void connectToWiFi() {
Serial.println("连接WiFi...");
WiFi.begin(ssid, password);
int retries = 0;
while (WiFi.status() != WL_CONNECTED && retries < 20) {
delay(500);
Serial.print(".");
retries++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("");
Serial.println("WiFi连接成功!");
Serial.print("IP地址: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("");
Serial.println("WiFi连接失败!");
}
}
// Web服务器设置
void setupWebServer() {
// 主页
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'>";
html += "<title>智幻走马灯控制</title>";
html += "<style>body{font-family:Arial;text-align:center;background:#667eea;color:white;padding:20px}";
html += ".container{background:rgba(255,255,255,0.1);padding:30px;border-radius:15px;max-width:400px;margin:0 auto}";
html += "button{width:100%;padding:15px;margin:10px 0;border:none;border-radius:10px;font-size:16px;cursor:pointer}";
html += ".btn-on{background:#4CAF50;color:white}";
html += ".btn-off{background:#f44336;color:white}";
html += ".btn-switch{background:#2196F3;color:white}";
html += ".status{background:rgba(255,255,255,0.2);padding:10px;border-radius:8px;margin:20px 0}";
html += "</style></head><body>";
html += "<div class='container'>";
html += "<h1>智幻走马灯</h1>";
html += "<div class='status'>";
html += "<p>状态: <span id='status'>获取中</span></p>";
html += "<p>模式: <span id='mode'>获取中</span></p>";
html += "</div>";
html += "<button class='btn-on' onclick='sendCmd(\"on\")'>开启走马灯</button>";
html += "<button class='btn-off' onclick='sendCmd(\"off\")'>关闭走马灯</button>";
html += "<button class='btn-switch' onclick='sendCmd(\"switch\")'>切换灯效</button>";
html += "</div>";
html += "<script>";
html += "var modes=['传统走马灯','火焰效果','彩虹旋转','呼吸同步','流星追逐','节日庆典'];";
html += "function updateStatus(){";
html += "fetch('/api/status').then(r=>r.json()).then(d=>{";
html += "document.getElementById('status').innerText=d.on?'已开启':'已关闭';";
html += "document.getElementById('mode').innerText=modes[d.mode]||'未知';";
html += "}).catch(e=>document.getElementById('status').innerText='连接失败');}";
html += "function sendCmd(cmd){";
html += "fetch('/api/control',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},";
html += "body:'command='+cmd}).then(r=>r.json()).then(d=>{";
html += "if(d.success)setTimeout(updateStatus,200);else alert('操作失败:'+d.message);";
html += "}).catch(e=>alert('网络错误'));}";
html += "updateStatus();setInterval(updateStatus,3000);";
html += "</script></body></html>";
request->send(200, "text/html", html);
});
// API: 获取状态
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request) {
DynamicJsonDocument doc(128);
doc["on"] = lanternOn;
doc["mode"] = currentMode;
String response;
serializeJson(doc, response);
request->send(200, "application/json", response);
});
// API: 控制命令
server.on("/api/control", HTTP_POST, [](AsyncWebServerRequest *request) {
DynamicJsonDocument responseDoc(128);
responseDoc["success"] = false;
responseDoc["message"] = "无效请求";
if (request->hasParam("command", true)) {
String command = request->getParam("command", true)->value();
if (command == "on") {
if (!lanternOn) {
turnOnLantern();
responseDoc["success"] = true;
responseDoc["message"] = "走马灯已开启";
} else {
responseDoc["message"] = "走马灯已经开启";
}
}
else if (command == "off") {
if (lanternOn) {
turnOffLantern();
responseDoc["success"] = true;
responseDoc["message"] = "走马灯已关闭";
} else {
responseDoc["message"] = "走马灯已经关闭";
}
}
else if (command == "switch") {
if (lanternOn) {
switchMode(1);
responseDoc["success"] = true;
responseDoc["message"] = String("已切换到: ") + modeNames[currentMode];
} else {
responseDoc["message"] = "请先开启走马灯";
}
}
}
String response;
serializeJson(responseDoc, response);
request->send(200, "application/json", response);
});
server.begin();
Serial.println("Web服务器已启动");
}
// 手势处理
void handleGestures() {
uint8_t gesture = apds.readGesture();
if (gesture != 0) {
if (millis() - lastGestureTime < GESTURE_DELAY) {
return;
}
lastGestureTime = millis();
switch (gesture) {
case APDS9960_LEFT:
Serial.println("手势: ← (上一个模式)");
switchMode(-1);
break;
case APDS9960_RIGHT:
Serial.println("手势: → (下一个模式)");
switchMode(1);
break;
case APDS9960_UP:
Serial.println("手势: ↑ (开启走马灯)");
turnOnLantern();
break;
case APDS9960_DOWN:
Serial.println("手势: ↓ (关闭走马灯)");
turnOffLantern();
break;
}
}
}
// 切换模式
void switchMode(int direction) {
if (!lanternOn) {
Serial.println("走马灯未开启,请先向上滑动开启");
return;
}
currentMode += direction;
if (currentMode < 0) {
currentMode = TOTAL_MODES - 1;
} else if (currentMode >= TOTAL_MODES) {
currentMode = 0;
}
modeStartTime = millis();
modeOffset = 0;
Serial.print("切换到模式: ");
Serial.println(modeNames[currentMode]);
showModeIndicator();
}
// 开启走马灯
void turnOnLantern() {
if (lanternOn) {
Serial.println("走马灯已经开启");
return;
}
lanternOn = true;
modeStartTime = millis();
modeOffset = 0;
Serial.println("走马灯已开启");
Serial.print("当前模式: ");
Serial.println(modeNames[currentMode]);
showStartupAnimation();
}
// 关闭走马灯
void turnOffLantern() {
if (!lanternOn) {
Serial.println("走马灯已经关闭");
return;
}
lanternOn = false;
Serial.println("走马灯已关闭");
showShutdownAnimation();
ledcWrite(MOTOR_PIN, 0);
clearDualStrips();
showDualStrips();
}
// 显示模式指示
void showModeIndicator() {
clearDualStrips();
uint32_t indicatorColor;
switch(currentMode) {
case 0: indicatorColor = createColor(60, 20, 0); break; // 橙色
case 1: indicatorColor = createColor(50, 0, 0); break; // 红色
case 2: indicatorColor = createColor(25, 25, 25); break; // 白色
case 3: indicatorColor = createColor(50, 50, 0); break; // 黄色
case 4: indicatorColor = createColor(0, 50, 50); break; // 青色
case 5: indicatorColor = createColor(50, 0, 50); break; // 紫色
}
for (int i = 0; i <= currentMode; i++) {
setDualPixelColor(i, indicatorColor);
}
showDualStrips();
delay(800);
clearDualStrips();
showDualStrips();
delay(200);
}
// 开启动画
void showStartupAnimation() {
Serial.println("播放开启动画...");
for (int i = 0; i < MAX_LED; i++) {
setDualPixelColor(i, createColor(0, 50, 0));
showDualStrips();
delay(80);
}
delay(300);
for (int speed = 0; speed <= 120; speed += 10) {
ledcWrite(MOTOR_PIN, speed);
delay(50);
}
clearDualStrips();
showDualStrips();
}
// 关闭动画
void showShutdownAnimation() {
Serial.println("播放关闭动画...");
int currentSpeed = 120;
while (currentSpeed > 0) {
ledcWrite(MOTOR_PIN, currentSpeed);
currentSpeed -= 10;
delay(100);
}
ledcWrite(MOTOR_PIN, 0);
for (int i = MAX_LED - 1; i >= 0; i--) {
setDualPixelColor(i, createColor(0, 0, 0));
showDualStrips();
delay(80);
}
}
// 运行当前模式
void runCurrentMode() {
if (!lanternOn) {
return;
}
switch(currentMode) {
case 0:
traditionalLanternMode();
break;
case 1:
fireMode();
break;
case 2:
rainbowSpinMode();
break;
case 3:
breathingSyncMode();
break;
case 4:
meteorChaseMode();
break;
case 5:
festivalMode();
break;
}
}
// 模式1: 传统走马灯
void traditionalLanternMode() {
uint32_t colors[] = {
createColor(50, 0, 0), // 红
createColor(60, 20, 0), // 橙
createColor(50, 50, 0), // 黄
createColor(0, 50, 0), // 绿
createColor(0, 50, 50), // 青
createColor(0, 0, 50) // 蓝
};
int numColors = 6;
ledcWrite(MOTOR_PIN, 120);
for(int i = 0; i < MAX_LED; i++) {
int colorIndex = (i + modeOffset) % numColors;
setDualPixelColor(i, colors[colorIndex]);
}
showDualStrips();
if (millis() - modeStartTime > (modeOffset + 1) * 200) {
modeOffset++;
}
}
// 模式2: 火焰效果
void fireMode() {
int motorSpeed = 80 + random(-15, 15);
ledcWrite(MOTOR_PIN, motorSpeed);
for(int i = 0; i < MAX_LED; i++) {
int flicker = random(20, 80);
int r = flicker;
int g = flicker * 0.3;
int b = 0;
setDualPixelColor(i, createColor(r, g, b));
}
showDualStrips();
}
// 模式3: 彩虹旋转
void rainbowSpinMode() {
ledcWrite(MOTOR_PIN, 120);
for(int i = 0; i < MAX_LED; i++) {
int phase = ((i * 255 / MAX_LED) + (modeOffset * 10)) % 255;
int r = 0, g = 0, b = 0;
if(phase < 85) {
r = (255 - phase * 3) / 5;
g = (phase * 3) / 5;
b = 0;
} else if(phase < 170) {
phase -= 85;
r = 0;
g = (255 - phase * 3) / 5;
b = (phase * 3) / 5;
} else {
phase -= 170;
r = (phase * 3) / 5;
g = 0;
b = (255 - phase * 3) / 5;
}
setDualPixelColor(i, createColor(r, g, b));
}
showDualStrips();
if (millis() - modeStartTime > (modeOffset + 1) * 100) {
modeOffset++;
if (modeOffset > 25) modeOffset = 0;
}
}
// 模式4: 呼吸同步
void breathingSyncMode() {
unsigned long cycleTime = (millis() - modeStartTime) % 4000;
int breathLevel = 0;
if (cycleTime < 2000) {
breathLevel = map(cycleTime, 0, 2000, 0, 100);
} else {
breathLevel = map(cycleTime, 2000, 4000, 100, 0);
}
int brightness = breathLevel / 2;
for(int i = 0; i < MAX_LED; i++) {
setDualPixelColor(i, createColor(brightness, brightness/2, 0));
}
showDualStrips();
ledcWrite(MOTOR_PIN, 50 + breathLevel);
}
// 模式5: 流星追逐
void meteorChaseMode() {
ledcWrite(MOTOR_PIN, 180);
int position = (modeOffset) % MAX_LED;
clearDualStrips();
setDualPixelColor(position, createColor(30, 30, 30));
for (int i = 1; i <= 3; i++) {
int tailPos = (position - i + MAX_LED) % MAX_LED;
int brightness = 30 - (i * 8);
if (brightness > 0) {
setDualPixelColor(tailPos, createColor(brightness, brightness, brightness));
}
}
showDualStrips();
if (millis() - modeStartTime > (modeOffset + 1) * 50) {
modeOffset++;
}
}
// 模式6: 节日庆典
void festivalMode() {
uint32_t festiveColors[] = {
createColor(50, 0, 0), // 红
createColor(0, 50, 0), // 绿
createColor(50, 50, 0), // 黄
createColor(0, 0, 50), // 蓝
createColor(50, 0, 50) // 紫
};
ledcWrite(MOTOR_PIN, 130);
for(int i = 0; i < MAX_LED; i++) {
if(random(10) > 7) {
setDualPixelColor(i, festiveColors[random(5)]);
} else {
setDualPixelColor(i, createColor(0, 0, 0));
}
}
showDualStrips();
if(random(100) > 95) {
ledcWrite(MOTOR_PIN, 100 + random(60));
}
}
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("=== 智幻走马灯 手势+Web控制系统 启动 ===");
// 初始化I2C
Wire.begin(SDA_PIN, SCL_PIN);
// 初始化手势传感器
if(!apds.begin()){
Serial.println("手势传感器初始化失败!");
} 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
strip1.begin();
strip1.setBrightness(currentBrightness);
strip1.clear();
strip1.show();
strip2.begin();
strip2.setBrightness(currentBrightness);
strip2.clear();
strip2.show();
// 初始化电机
ledcAttach(MOTOR_PIN, PWM_FREQ, PWM_RESOLUTION);
ledcWrite(MOTOR_PIN, 0);
// 连接WiFi
connectToWiFi();
// 设置Web服务器
if (WiFi.status() == WL_CONNECTED) {
setupWebServer();
}
Serial.println("系统就绪!");
Serial.println("手势控制:");
Serial.println(" ↑ : 开启走马灯");
Serial.println(" ↓ : 关闭走马灯");
Serial.println(" ← → : 切换灯效模式");
if (WiFi.status() == WL_CONNECTED) {
Serial.println("Web控制:");
Serial.print(" 访问地址: http://");
Serial.println(WiFi.localIP());
}
}
void loop() {
// 检测手势
handleGestures();
// 运行当前模式
runCurrentMode();
// 检查WiFi连接
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi连接断开,尝试重连...");
WiFi.reconnect();
}
delay(50);
}
成功编译上传,在串口监视器可以看到 XIAO ESP32C3 提供的 IP 地址如下图所示,如果需要进行 Web 控制, 访问地址: http://192.168.91.124
。注意要访问此 IP 地址,需要手机或电脑和 XIAO ESP32C3 使用相同的 Wi-Fi。
程序成功编译上传后,会提示 Web 模式下可用的 IP 地址
我用 PC 连接了和 XIAO 一样的 Wi-Fi,然后用浏览器访问 http://192.168.91.124
最终版程序:手势+Web+MQTT 同步
完整功能清单
✅ 手势控制
- 上下左右四个方向的手势识别
- 多次重试初始化机制
- 分时检测避免资源冲突
✅ Web控制界面
- 现代化响应式设计
- 实时状态显示
- 三种控制按钮 + 同步开关
✅ MQTT多设备同步
- 自动设备发现
- 实时状态同步
- 防循环消息机制
✅ 六种灯效模式
- 传统走马灯
- 火焰效果
- 彩虹旋转
- 呼吸同步
- 流星追逐
- 节日庆典
#include <Adafruit_NeoPixel.h>
#include <Wire.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ArduinoJson.h>
#include <PubSubClient.h>
#include "XLOT_APDS9960AD.h"
// WiFi设置 - 请修改为您的WiFi信息
const char* ssid = "xiao";
const char* password = "12345678";
// MQTT设置
const char* mqtt_broker = "broker.hivemq.com";
const int mqtt_port = 1883;
const char* mqtt_topic_command = "smart_lantern/command";
const char* mqtt_topic_status = "smart_lantern/status";
const char* mqtt_topic_heartbeat = "smart_lantern/heartbeat";
// Web服务器和MQTT客户端
AsyncWebServer server(80);
WiFiClient espClient;
PubSubClient mqttClient(espClient);
// 硬件定义
#define LED_PIN_1 9 // GPIO9 控制第一条LED灯带
#define LED_PIN_2 8 // GPIO8 控制第二条LED灯带
#define MOTOR_PIN 10 // GPIO10 控制电机
#define MAX_LED 14 // 14个LED
// I2C引脚定义
#define SDA_PIN 6 // GPIO6 - I2C数据线
#define SCL_PIN 7 // GPIO7 - I2C时钟线
// LED灯带
Adafruit_NeoPixel strip1 = Adafruit_NeoPixel(MAX_LED, LED_PIN_1, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel strip2 = Adafruit_NeoPixel(MAX_LED, LED_PIN_2, NEO_GRB + NEO_KHZ800);
// 手势传感器
XLOT_APDS9960AD apds;
// PWM参数
const int PWM_FREQ = 5000;
const int PWM_RESOLUTION = 8;
// 系统状态
bool lanternOn = false;
int currentMode = 0;
const int TOTAL_MODES = 6;
int currentBrightness = 20;
bool gestureEnabled = false;
// 设备唯一标识
String deviceId;
bool syncEnabled = true;
// 任务时间管理
unsigned long lastGestureCheck = 0;
unsigned long lastMqttCheck = 0;
unsigned long lastHeartbeat = 0;
unsigned long lastMqttAttempt = 0;
// 任务间隔控制
const unsigned long GESTURE_CHECK_INTERVAL = 50; // 50ms检查一次手势
const unsigned long MQTT_CHECK_INTERVAL = 100; // 100ms检查一次MQTT
const unsigned long HEARTBEAT_INTERVAL = 30000; // 30秒心跳
const unsigned long MQTT_RETRY_INTERVAL = 5000; // 5秒重连间隔
// 手势控制防抖
unsigned long lastGestureTime = 0;
const unsigned long GESTURE_DELAY = 800;
// 模式名称
const char* modeNames[TOTAL_MODES] = {
"Traditional Lantern", "Fire Effect", "Rainbow Spin",
"Breathing Sync", "Meteor Chase", "Festival Mode"
};
// 模式控制变量
unsigned long modeStartTime = 0;
int modeOffset = 0;
// 初始化设备ID
void initDeviceId() {
uint64_t chipId = ESP.getEfuseMac();
char deviceIdBuffer[20];
snprintf(deviceIdBuffer, sizeof(deviceIdBuffer), "Lantern%llX", chipId & 0xFFFFFF);
deviceId = String(deviceIdBuffer);
Serial.print("Device ID: ");
Serial.println(deviceId);
}
// 辅助函数
void setDualPixelColor(int pixel, uint32_t color) {
strip1.setPixelColor(pixel, color);
strip2.setPixelColor(pixel, color);
}
void showDualStrips() {
strip1.show();
strip2.show();
}
void clearDualStrips() {
strip1.clear();
strip2.clear();
}
uint32_t createColor(int r, int g, int b) {
return strip1.Color(r, g, b);
}
// I2C设备扫描
void scanI2CDevices() {
Serial.println("\n=== I2C Device Scanner ===");
byte error, address;
int deviceCount = 0;
for(address = 1; address < 127; address++) {
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0) {
Serial.print("Device found at 0x");
if (address < 16) {
Serial.print("0");
}
Serial.print(address, HEX);
if (address == 0x39) {
Serial.print(" <- APDS-9960 Gesture Sensor!");
}
Serial.println();
deviceCount++;
}
}
if (deviceCount == 0) {
Serial.println("No I2C devices found");
} else {
Serial.print("Found ");
Serial.print(deviceCount);
Serial.println(" I2C device(s)");
}
Serial.println("==========================");
}
// 增强的手势传感器初始化
bool initGestureSensor() {
Serial.println("\n=== Enhanced Gesture Sensor Init ===");
// 首先扫描I2C设备
scanI2CDevices();
// 重置I2C总线
Serial.println("Resetting I2C bus...");
Wire.end();
delay(100);
Wire.begin(SDA_PIN, SCL_PIN);
delay(500);
// 多次尝试初始化,每次尝试前都重置
for(int attempt = 1; attempt <= 5; attempt++) {
Serial.print("Gesture sensor init attempt ");
Serial.print(attempt);
Serial.print("/5: ");
// 每次尝试前稍微延迟
delay(attempt * 200);
if(apds.begin()) {
Serial.println("SUCCESS!");
// 验证传感器是否真的可用
delay(100);
// 配置传感器
Serial.println("Configuring sensor...");
apds.enableProximity(true);
delay(50);
apds.enableGesture(true);
delay(50);
apds.setProxGain(APDS9960_PGAIN_8X);
delay(50);
apds.setGestureGain(APDS9960_PGAIN_8X);
delay(50);
apds.setGestureGain(APDS9960_AGAIN_64X);
delay(50);
apds.setGestureGain(APDS9960_GGAIN_8);
delay(50);
Serial.println("Sensor configured successfully!");
// 测试读取功能
Serial.println("Testing sensor read...");
for(int test = 0; test < 5; test++) {
uint8_t testGesture = apds.readGesture();
delay(100);
}
Serial.println("Sensor test completed!");
return true;
} else {
Serial.println("FAILED");
// 失败后重新扫描I2C
if(attempt == 3) {
Serial.println("Re-scanning I2C after failures...");
scanI2CDevices();
}
}
}
Serial.println("All gesture sensor init attempts failed!");
return false;
}
// WiFi连接
void connectToWiFi() {
Serial.println("Connecting to WiFi...");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
int retries = 0;
while (WiFi.status() != WL_CONNECTED && retries < 20) {
delay(500);
Serial.print(".");
retries++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("");
Serial.println("WiFi connected successfully!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("");
Serial.println("WiFi connection failed!");
}
}
// MQTT连接
void connectToMQTT() {
if (!mqttClient.connected() && WiFi.status() == WL_CONNECTED) {
if (millis() - lastMqttAttempt > MQTT_RETRY_INTERVAL) {
lastMqttAttempt = millis();
if (mqttClient.connect(deviceId.c_str())) {
Serial.println("MQTT connected successfully!");
// 订阅主题
mqttClient.subscribe(mqtt_topic_command);
mqttClient.subscribe(mqtt_topic_status);
mqttClient.subscribe(mqtt_topic_heartbeat);
// 发送上线通知
publishHeartbeat();
publishStatus();
} else {
Serial.print("MQTT connection failed, error code: ");
Serial.println(mqttClient.state());
}
}
}
}
// 发布心跳消息
void publishHeartbeat() {
if (mqttClient.connected()) {
DynamicJsonDocument doc(256);
doc["deviceId"] = deviceId;
doc["timestamp"] = millis();
doc["ip"] = WiFi.localIP().toString();
doc["status"] = "online";
String message;
serializeJson(doc, message);
mqttClient.publish(mqtt_topic_heartbeat, message.c_str());
}
}
// 发布状态消息
void publishStatus() {
if (mqttClient.connected()) {
DynamicJsonDocument doc(256);
doc["deviceId"] = deviceId;
doc["lanternOn"] = lanternOn;
doc["currentMode"] = currentMode;
doc["modeName"] = modeNames[currentMode];
doc["timestamp"] = millis();
String message;
serializeJson(doc, message);
mqttClient.publish(mqtt_topic_status, message.c_str());
}
}
// 发布控制命令
void publishCommand(const String& command, const String& source = "gesture") {
if (mqttClient.connected() && syncEnabled) {
DynamicJsonDocument doc(256);
doc["deviceId"] = deviceId;
doc["command"] = command;
doc["source"] = source;
doc["lanternOn"] = lanternOn;
doc["currentMode"] = currentMode;
doc["timestamp"] = millis();
String message;
serializeJson(doc, message);
bool published = mqttClient.publish(mqtt_topic_command, message.c_str());
if (published) {
Serial.print("Published command: ");
Serial.println(command);
}
}
}
// MQTT消息回调
void onMqttMessage(char* topic, byte* payload, unsigned int length) {
String message;
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
// 解析JSON消息
DynamicJsonDocument doc(512);
DeserializationError error = deserializeJson(doc, message);
if (error) {
return; // 静默忽略解析错误
}
String senderId = doc["deviceId"];
// 忽略自己发送的消息
if (senderId == deviceId) {
return;
}
// 处理不同类型的消息
String topicStr = String(topic);
if (topicStr == mqtt_topic_command) {
String command = doc["command"];
Serial.print("Received sync command: ");
Serial.print(command);
Serial.print(" (from: ");
Serial.print(senderId);
Serial.println(")");
// 执行同步命令
if (command == "turn_on") {
if (!lanternOn) {
lanternOn = true;
modeStartTime = millis();
modeOffset = 0;
showStartupAnimation();
}
}
else if (command == "turn_off") {
if (lanternOn) {
lanternOn = false;
showShutdownAnimation();
ledcWrite(MOTOR_PIN, 0);
clearDualStrips();
showDualStrips();
}
}
else if (command == "switch_mode") {
if (lanternOn) {
int newMode = doc["currentMode"];
if (newMode >= 0 && newMode < TOTAL_MODES) {
currentMode = newMode;
modeStartTime = millis();
modeOffset = 0;
showModeIndicator();
}
}
}
}
else if (topicStr == mqtt_topic_heartbeat) {
String status = doc["status"];
if (status == "online") {
Serial.print("Device online: ");
Serial.println(senderId);
}
}
}
// Web服务器设置
void setupWebServer() {
// 主页
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'>";
html += "<title>Smart Lantern Control</title>";
html += "<style>body{font-family:Arial;text-align:center;background:#667eea;color:white;padding:20px}";
html += ".container{background:rgba(255,255,255,0.1);padding:30px;border-radius:15px;max-width:400px;margin:0 auto}";
html += "button{width:100%;padding:15px;margin:10px 0;border:none;border-radius:10px;font-size:16px;cursor:pointer}";
html += ".btn-on{background:#4CAF50;color:white}";
html += ".btn-off{background:#f44336;color:white}";
html += ".btn-switch{background:#2196F3;color:white}";
html += ".btn-sync{background:#FF9800;color:white}";
html += ".status{background:rgba(255,255,255,0.2);padding:10px;border-radius:8px;margin:20px 0}";
html += "</style></head><body>";
html += "<div class='container'>";
html += "<h1>🏮 Smart Lantern</h1>";
html += "<div class='status'>";
html += "<p>Status: <span id='status'>Loading...</span></p>";
html += "<p>Mode: <span id='mode'>Loading...</span></p>";
html += "<p>Device: <span id='deviceId'>Loading...</span></p>";
html += "<p>MQTT: <span id='mqttStatus'>Loading...</span></p>";
html += "<p>Gesture: <span id='gestureStatus'>Loading...</span></p>";
html += "</div>";
html += "<button class='btn-on' onclick='sendCmd(\"on\")'>🔆 Turn On</button>";
html += "<button class='btn-off' onclick='sendCmd(\"off\")'>🔅 Turn Off</button>";
html += "<button class='btn-switch' onclick='sendCmd(\"switch\")'>🎨 Switch Mode</button>";
html += "<button class='btn-sync' onclick='sendCmd(\"toggle_sync\")'>🔄 Sync: <span id='syncStatus'>Loading...</span></button>";
html += "</div>";
html += "<script>";
html += "var modes=['Traditional Lantern','Fire Effect','Rainbow Spin','Breathing Sync','Meteor Chase','Festival Mode'];";
html += "function updateStatus(){";
html += "fetch('/api/status').then(r=>r.json()).then(d=>{";
html += "document.getElementById('status').innerText=d.on?'On':'Off';";
html += "document.getElementById('mode').innerText=modes[d.mode]||'Unknown';";
html += "document.getElementById('deviceId').innerText=d.deviceId||'Unknown';";
html += "document.getElementById('mqttStatus').innerText=d.mqttConnected?'Connected':'Disconnected';";
html += "document.getElementById('gestureStatus').innerText=d.gestureEnabled?'Available':'Unavailable';";
html += "document.getElementById('syncStatus').innerText=d.syncEnabled?'On':'Off';";
html += "}).catch(e=>console.error(e));}";
html += "function sendCmd(cmd){";
html += "fetch('/api/control',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},";
html += "body:'command='+cmd}).then(r=>r.json()).then(d=>{";
html += "if(d.success)setTimeout(updateStatus,200);";
html += "}).catch(e=>console.error(e));}";
html += "updateStatus();setInterval(updateStatus,3000);";
html += "</script></body></html>";
request->send(200, "text/html", html);
});
// API: 获取状态
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request) {
DynamicJsonDocument doc(256);
doc["on"] = lanternOn;
doc["mode"] = currentMode;
doc["deviceId"] = deviceId;
doc["mqttConnected"] = mqttClient.connected();
doc["syncEnabled"] = syncEnabled;
doc["gestureEnabled"] = gestureEnabled;
String response;
serializeJson(doc, response);
request->send(200, "application/json", response);
});
// API: 控制命令
server.on("/api/control", HTTP_POST, [](AsyncWebServerRequest *request) {
DynamicJsonDocument responseDoc(128);
responseDoc["success"] = false;
responseDoc["message"] = "Invalid request";
if (request->hasParam("command", true)) {
String command = request->getParam("command", true)->value();
if (command == "on") {
if (!lanternOn) {
turnOnLantern();
publishCommand("turn_on", "web");
responseDoc["success"] = true;
responseDoc["message"] = "Lantern turned on";
}
}
else if (command == "off") {
if (lanternOn) {
turnOffLantern();
publishCommand("turn_off", "web");
responseDoc["success"] = true;
responseDoc["message"] = "Lantern turned off";
}
}
else if (command == "switch") {
if (lanternOn) {
switchMode(1);
publishCommand("switch_mode", "web");
responseDoc["success"] = true;
responseDoc["message"] = String("Switched to: ") + modeNames[currentMode];
}
}
else if (command == "toggle_sync") {
syncEnabled = !syncEnabled;
responseDoc["success"] = true;
responseDoc["message"] = String("Sync ") + (syncEnabled ? "enabled" : "disabled");
}
}
String response;
serializeJson(responseDoc, response);
request->send(200, "application/json", response);
});
server.begin();
Serial.println("Web server started");
}
// 增强的手势处理函数
void handleGestures() {
// 分时检查,避免与MQTT冲突
if (millis() - lastGestureCheck < GESTURE_CHECK_INTERVAL) {
return;
}
lastGestureCheck = millis();
if (!gestureEnabled) {
return;
}
// 添加传感器健康检查
static unsigned long lastHealthCheck = 0;
if (millis() - lastHealthCheck > 30000) { // 每30秒检查一次
lastHealthCheck = millis();
Serial.println("Gesture sensor health check...");
// 尝试简单的I2C通信
Wire.beginTransmission(0x39);
byte error = Wire.endTransmission();
if (error != 0) {
Serial.println("Gesture sensor I2C communication failed!");
Serial.print("Error code: ");
Serial.println(error);
// 尝试重新初始化
Serial.println("Attempting to reinitialize sensor...");
gestureEnabled = initGestureSensor();
} else {
Serial.println("Gesture sensor I2C OK");
}
}
uint8_t gesture = apds.readGesture();
if (gesture != 0) {
if (millis() - lastGestureTime < GESTURE_DELAY) {
return;
}
lastGestureTime = millis();
Serial.println("=== GESTURE DETECTED ===");
Serial.print("Timestamp: ");
Serial.println(millis());
Serial.print("Gesture code: ");
Serial.println(gesture);
switch (gesture) {
case APDS9960_LEFT:
Serial.println("Action: LEFT (previous mode)");
switchMode(-1);
publishCommand("switch_mode", "gesture");
break;
case APDS9960_RIGHT:
Serial.println("Action: RIGHT (next mode)");
switchMode(1);
publishCommand("switch_mode", "gesture");
break;
case APDS9960_UP:
Serial.println("Action: UP (turn on lantern)");
turnOnLantern();
publishCommand("turn_on", "gesture");
break;
case APDS9960_DOWN:
Serial.println("Action: DOWN (turn off lantern)");
turnOffLantern();
publishCommand("turn_off", "gesture");
break;
default:
Serial.print("Unknown gesture code: ");
Serial.println(gesture);
break;
}
Serial.println("========================");
}
// 调试信息:定期显示手势检测状态
static unsigned long lastStatusReport = 0;
if (millis() - lastStatusReport > 60000) { // 每分钟报告一次
lastStatusReport = millis();
Serial.print("Gesture system status - Enabled: ");
Serial.print(gestureEnabled ? "YES" : "NO");
Serial.print(", Check interval: ");
Serial.print(GESTURE_CHECK_INTERVAL);
Serial.println("ms");
}
}
// 切换模式
void switchMode(int direction) {
if (!lanternOn) {
return;
}
currentMode += direction;
if (currentMode < 0) {
currentMode = TOTAL_MODES - 1;
} else if (currentMode >= TOTAL_MODES) {
currentMode = 0;
}
modeStartTime = millis();
modeOffset = 0;
showModeIndicator();
publishStatus();
}
// 开启走马灯
void turnOnLantern() {
if (lanternOn) {
return;
}
lanternOn = true;
modeStartTime = millis();
modeOffset = 0;
showStartupAnimation();
publishStatus();
}
// 关闭走马灯
void turnOffLantern() {
if (!lanternOn) {
return;
}
lanternOn = false;
showShutdownAnimation();
ledcWrite(MOTOR_PIN, 0);
clearDualStrips();
showDualStrips();
publishStatus();
}
// 显示模式指示
void showModeIndicator() {
clearDualStrips();
uint32_t indicatorColor;
switch(currentMode) {
case 0: indicatorColor = createColor(60, 20, 0); break; // 橙色
case 1: indicatorColor = createColor(50, 0, 0); break; // 红色
case 2: indicatorColor = createColor(25, 25, 25); break; // 白色
case 3: indicatorColor = createColor(50, 50, 0); break; // 黄色
case 4: indicatorColor = createColor(0, 50, 50); break; // 青色
case 5: indicatorColor = createColor(50, 0, 50); break; // 紫色
}
for (int i = 0; i <= currentMode; i++) {
setDualPixelColor(i, indicatorColor);
}
showDualStrips();
delay(500);
clearDualStrips();
showDualStrips();
}
// 开启动画
void showStartupAnimation() {
for (int i = 0; i < MAX_LED; i++) {
setDualPixelColor(i, createColor(0, 50, 0));
showDualStrips();
delay(50);
}
for (int speed = 0; speed <= 120; speed += 10) {
ledcWrite(MOTOR_PIN, speed);
delay(30);
}
clearDualStrips();
showDualStrips();
}
// 关闭动画
void showShutdownAnimation() {
int currentSpeed = 120;
while (currentSpeed > 0) {
ledcWrite(MOTOR_PIN, currentSpeed);
currentSpeed -= 10;
delay(50);
}
ledcWrite(MOTOR_PIN, 0);
for (int i = MAX_LED - 1; i >= 0; i--) {
setDualPixelColor(i, createColor(0, 0, 0));
showDualStrips();
delay(50);
}
}
// 运行当前模式
void runCurrentMode() {
if (!lanternOn) {
return;
}
switch(currentMode) {
case 0: traditionalLanternMode(); break;
case 1: fireMode(); break;
case 2: rainbowSpinMode(); break;
case 3: breathingSyncMode(); break;
case 4: meteorChaseMode(); break;
case 5: festivalMode(); break;
}
}
// 模式1: 传统走马灯
void traditionalLanternMode() {
uint32_t colors[] = {
createColor(50, 0, 0), // 红
createColor(60, 20, 0), // 橙
createColor(50, 50, 0), // 黄
createColor(0, 50, 0), // 绿
createColor(0, 50, 50), // 青
createColor(0, 0, 50) // 蓝
};
int numColors = 6;
ledcWrite(MOTOR_PIN, 120);
for(int i = 0; i < MAX_LED; i++) {
int colorIndex = (i + modeOffset) % numColors;
setDualPixelColor(i, colors[colorIndex]);
}
showDualStrips();
if (millis() - modeStartTime > (modeOffset + 1) * 200) {
modeOffset++;
}
}
// 模式2: 火焰效果
void fireMode() {
int motorSpeed = 80 + random(-15, 15);
ledcWrite(MOTOR_PIN, motorSpeed);
for(int i = 0; i < MAX_LED; i++) {
int flicker = random(20, 80);
int r = flicker;
int g = flicker * 0.3;
int b = 0;
setDualPixelColor(i, createColor(r, g, b));
}
showDualStrips();
}
// 模式3: 彩虹旋转
void rainbowSpinMode() {
ledcWrite(MOTOR_PIN, 120);
for(int i = 0; i < MAX_LED; i++) {
int phase = ((i * 255 / MAX_LED) + (modeOffset * 10)) % 255;
int r = 0, g = 0, b = 0;
if(phase < 85) {
r = (255 - phase * 3) / 5;
g = (phase * 3) / 5;
b = 0;
} else if(phase < 170) {
phase -= 85;
r = 0;
g = (255 - phase * 3) / 5;
b = (phase * 3) / 5;
} else {
phase -= 170;
r = (phase * 3) / 5;
g = 0;
b = (255 - phase * 3) / 5;
}
setDualPixelColor(i, createColor(r, g, b));
}
showDualStrips();
if (millis() - modeStartTime > (modeOffset + 1) * 100) {
modeOffset++;
if (modeOffset > 25) modeOffset = 0;
}
}
// 模式4: 呼吸同步 - 简化版
void breathingSyncMode() {
unsigned long cycleTime = (millis() - modeStartTime) % 4000;
int breathLevel = (cycleTime < 2000) ?
map(cycleTime, 0, 2000, 0, 50) : map(cycleTime, 2000, 4000, 50, 0);
for(int i = 0; i < MAX_LED; i++) {
setDualPixelColor(i, createColor(breathLevel, breathLevel/2, 0));
}
showDualStrips();
ledcWrite(MOTOR_PIN, 50 + breathLevel * 2);
}
// 模式5: 流星追逐
void meteorChaseMode() {
ledcWrite(MOTOR_PIN, 180);
int position = (modeOffset) % MAX_LED;
clearDualStrips();
setDualPixelColor(position, createColor(30, 30, 30));
for (int i = 1; i <= 3; i++) {
int tailPos = (position - i + MAX_LED) % MAX_LED;
int brightness = 30 - (i * 8);
if (brightness > 0) {
setDualPixelColor(tailPos, createColor(brightness, brightness, brightness));
}
}
showDualStrips();
if (millis() - modeStartTime > (modeOffset + 1) * 50) {
modeOffset++;
}
}
// 模式6: 节日庆典
void festivalMode() {
uint32_t festiveColors[] = {
createColor(50, 0, 0), // 红
createColor(0, 50, 0), // 绿
createColor(50, 50, 0), // 黄
createColor(0, 0, 50), // 蓝
createColor(50, 0, 50) // 紫
};
ledcWrite(MOTOR_PIN, 130);
for(int i = 0; i < MAX_LED; i++) {
if(random(10) > 7) {
setDualPixelColor(i, festiveColors[random(5)]);
} else {
setDualPixelColor(i, createColor(0, 0, 0));
}
}
showDualStrips();
if(random(100) > 95) {
ledcWrite(MOTOR_PIN, 100 + random(60));
}
}
// MQTT维护任务
void maintainMQTT() {
if (millis() - lastMqttCheck < MQTT_CHECK_INTERVAL) {
return;
}
lastMqttCheck = millis();
if (!mqttClient.connected()) {
connectToMQTT();
} else {
mqttClient.loop();
// 发送心跳
if (millis() - lastHeartbeat > HEARTBEAT_INTERVAL) {
publishHeartbeat();
lastHeartbeat = millis();
}
}
}
void setup() {
Serial.begin(115200);
delay(3000);
Serial.println("=== Smart Lantern Debug Enhanced Version ===");
Serial.println("Version: Complete Debug v1.0");
// 初始化设备ID
initDeviceId();
// 先初始化LED和电机(在WiFi之前)
Serial.println("Initializing LED strips...");
strip1.begin();
strip1.setBrightness(currentBrightness);
strip1.clear();
strip1.show();
strip2.begin();
strip2.setBrightness(currentBrightness);
strip2.clear();
strip2.show();
Serial.println("Initializing motor...");
ledcAttach(MOTOR_PIN, PWM_FREQ, PWM_RESOLUTION);
ledcWrite(MOTOR_PIN, 0);
// 早期手势传感器初始化(在WiFi之前)
Serial.println("Early gesture sensor initialization...");
Wire.begin(SDA_PIN, SCL_PIN);
delay(1000); // 给传感器更多启动时间
gestureEnabled = initGestureSensor();
// 连接WiFi
connectToWiFi();
// 设置MQTT和Web服务器(在手势传感器之后)
if (WiFi.status() == WL_CONNECTED) {
Serial.println("Setting up MQTT...");
mqttClient.setServer(mqtt_broker, mqtt_port);
mqttClient.setCallback(onMqttMessage);
// 延迟启动MQTT,避免与手势传感器冲突
delay(1000);
connectToMQTT();
Serial.println("Setting up Web server...");
setupWebServer();
}
Serial.println("\n=== System Status ===");
Serial.println("Control Methods:");
if (gestureEnabled) {
Serial.println(" ✅ Gesture Control: Available");
Serial.println(" UP: Turn on lantern");
Serial.println(" DOWN: Turn off lantern");
Serial.println(" LEFT/RIGHT: Switch light modes");
} else {
Serial.println(" ❌ Gesture Control: Unavailable");
Serial.println(" Check I2C connections and sensor wiring");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println(" ✅ Web Control: Available");
Serial.print(" Access: http://");
Serial.println(WiFi.localIP());
Serial.println(" ✅ MQTT Sync: Available");
Serial.print(" Device ID: ");
Serial.println(deviceId);
Serial.print(" MQTT Status: ");
Serial.println(mqttClient.connected() ? "Connected" : "Connecting...");
} else {
Serial.println(" ❌ WiFi: Not connected");
}
Serial.println("\nDebug Features:");
Serial.println(" • Enhanced I2C scanning");
Serial.println(" • Multi-retry gesture sensor init");
Serial.println(" • Periodic health checks");
Serial.println(" • Detailed gesture detection logs");
Serial.println("\n🚀 System Ready!");
Serial.println("Watch serial monitor for gesture detection and health reports");
// 启动指示动画
for(int i = 0; i < 3; i++) {
setDualPixelColor(0, createColor(0, 50, 0));
showDualStrips();
delay(200);
clearDualStrips();
showDualStrips();
delay(200);
}
// 最后一次健康检查报告
Serial.println("\n=== Initial Health Check ===");
Serial.print("Gesture sensor: ");
Serial.println(gestureEnabled ? "READY" : "FAILED");
Serial.print("WiFi: ");
Serial.println(WiFi.status() == WL_CONNECTED ? "CONNECTED" : "FAILED");
Serial.print("MQTT: ");
Serial.println(mqttClient.connected() ? "CONNECTED" : "PENDING");
Serial.println("=============================");
}
void loop() {
// 分时任务调度,避免资源冲突
// 任务1: 手势检测(50ms间隔)- 高优先级
handleGestures();
// 任务2: 灯效运行(实时)
runCurrentMode();
// 任务3: MQTT维护(100ms间隔)
maintainMQTT();
// 任务4: WiFi监控(降低频率)
static unsigned long lastWifiCheck = 0;
if (millis() - lastWifiCheck > 10000) { // 10秒检查一次WiFi
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi reconnecting...");
WiFi.reconnect();
}
lastWifiCheck = millis();
}
// 主循环延迟(减少CPU占用)
delay(10);
}
对最终程序进行测试,完全实现了 3 种控制方式,下面是最终版本的 Web 访问截图:
通过 web 方式直接访问 XIAO ESP32C3 进行控制
最终制造了 2 个走马灯,进行多灯笼同步测试。
成功完成了对最终程序进行测试,完全实现了 3 种控制方式
下面的视频展示了 3 种成功控制灯笼的演示。
制作历程:18周的成长之路
阶段一:构思与设计(第1-6周)
项目最初的想法很朴素:做个会发光的东西。但随着对 Fab Academy 课程的深入,这个"会发光的东西"逐渐有了明确的方向和复杂的功能。
周次 | 里程碑 | 状态 | 成果展示 |
---|---|---|---|
第1周 | 项目构思与需求分析 | ✅ | ![]() |
第2周 | 3D建模设计 | ✅ | ![]() |
第3周 | 激光切割外壳 | ✅ | ![]() |
第6周 | PCB电路设计 | ✅ | ![]() |
这个阶段最大的收获是学会了从模糊想法到具体方案的转化。最初只是想做个"智能灯",经过需求分析、技术调研、可行性评估,最终锁定了走马灯这个极具文化内涵的载体。
阶段二:核心技术突破(第7-11周)
进入技术实现阶段,每一周都是一个新的挑战。PCB 制作、传感器调试、电机控制,每个环节都需要反复试错和优化。
周次 | 里程碑 | 状态 | 成果展示 |
---|---|---|---|
第8周 | PCB制作与焊接 | ✅ | ![]() |
第9周 | 手势传感器集成 | ✅ | ![]() |
第10周 | 电机与LED控制 | ✅ | ![]() |
第11周 | 网络通信实现 | ✅ | ![]() |
这个阶段的关键突破是解决了多传感器协同工作的问题。三个手势传感器如何避免相互干扰?如何在有限的算力下实现实时的手势识别?这些都需要在实践中摸索出最优解。
阶段三:系统集成与优化(第12-17周)
技术验证完成后,就是最考验功力的系统集成阶段。如何把散落的功能模块整合成一个和谐的整体?
周次 | 里程碑 | 状态 | 成果展示 |
---|---|---|---|
第15周 | Web控制界面 | ✅ | ![]() |
第16周 | 机械结构集成 | ✅ | ![]() |
第17周 | 圆环PCB设计 | ✅ | ![]() |
第17周的 PCB 设计第 1 板焊接测试发现了很多问题,进行排查修订后,完成了最终的 PCB 的设计和生产。
当前状态:主体全部完成
已完成的部分 ✅
硬件制造完成度:100%
- 激光切割的木质外壳:完美 ✨
- 3D打印的齿轮系统:运转顺畅 ✨
- 圆环PCB:制作完成,焊接到位 ✨
- 电机控制:测试通过 ✨
- LED灯效:绚丽多彩 ✨
软件开发完成度:100%
- 基础固件:稳定运行 ✨
- 手势识别:算法验证 ✨
- Web界面:功能完整 ✨
- WiFi通信:连接稳定 ✨
当前状态:主要功能已实现,正在进行最终整合调试
遇到的小插曲 ⚠️
PCB 设计错误
项目进行到最后关头,竟然发现了一个低级错误:第 3 版 PCB上的手势传感器、LED 灯带和电机控制板接口引脚顺序都设计反了!这就像给汽车装了个反向的方向盘,方向全都颠倒了。
问题分析: 设计PCB时参考了错误的引脚图,导致线序反接。虽然没有烧毁元件(还好有保护电路),但传感器、灯带和电机都无法正常工作。
解决方案:设计了第 4 版的 PCB
齿轮连续工作问题
智幻走马灯全部安装好后,测试运行很快发现灯笼频繁会出现卡顿现象,我移开 PCB 舱观察,发现 C2A 运行一会被推高导致无法吃力,如下面视频所示。
于是我尝试修改了 C2B 和 C2A 齿轮,如下图所示。将齿轮高度从 4mm 增加到 5mm,然后在轴部又增加了 4mm 的包裹,让齿轮能更紧密的贴合钢轴。
重新设计并打印了 C2B 和 C2A 齿轮
替换后灯笼转动的稳定性好了很多。
这个小插曲让我深刻体会到了项目管理中"墨菲定律"的威力:能出错的地方一定会出错。好在预留了足够的时间缓冲,这点小问题不会影响最终演示。
技术规格详解
材料与成本分析
整个项目的总成本控制在250元以内,这在同类智能硬件产品中算是相当经济的。
成本构成:电子元件占主要部分,机械材料成本较低
BOM
No. | Name | Quantity | Comment | Designator | Footprint | Pin Count | Manufacturer | LCSC Price | Total price(RMB) |
---|---|---|---|---|---|---|---|---|---|
1 | 10uF | 1 | 10uF | C1 | CAP-TH_BD4.0-P1.50-D0.8-FD | 2 | HUAWEI(华威集团) | 0.1292 | 0.13 |
2 | 100nF | 1 | 100nF | C2 | CAP-TH_L5.0-W2.5-P5.00-D1.0 | 2 | Dersonic(德尔创) | 0.1216 | 0.12 |
3 | WAFER-PH2.0-4PZZ | 4 | WAFER-PH2.0-4PZZ | FAN1,GROVE1,GROVE2,GROVE3 | CONN-TH_4P-P2.00_FWF20001-S04S22W5B | 4 | XUNPU(讯普) | 0.1045 | 0.42 |
4 | 1.0T-4P立贴 | 3 | 1.0T-4P立贴 | GESTURE1,GESTURE2,GESTURE3 | CONN-SMD_1.0T-4P-LT | 6 | BOOMELE(博穆精密) | 0.3802 | 1.14 |
5 | PZ254V-11-02P | 1 | PZ254V-11-02P | JBATT1 | HDR-TH_2P-P2.54-V-M | 2 | XFCN(兴飞) | 0.0851 | 0.09 |
6 | WAFER-PH2.0-3PZZ | 2 | WAFER-PH2.0-3PZZ | LEDSTRIP1,LEDSTRIP2 | CONN-TH_PH2.0D-L-3P | 3 | XUNPU(讯普) | 0.0791 | 0.16 |
7 | SS-12D10-G090 | 1 | SS-12D10-G090 | SW1 | SW-TH_3P-L12.7-W6.6-P4.70_C9900034369 | 3 | G-Switch(品赞) | 1.801 | 1.80 |
8 | XIAOESP32C3 | 1 | XIAOESP32C3 | XIAOESP32C3 | CONN-SMD_113991054 | 23 | Seeed Studio(矽递科技) | 35 | 35.00 |
9 | 2541FV-7P-B | 2 | 2541FV-7P-B | XIAOL1,XIAOR1 | HDR-TH_7P-P2.54-V-F | 7 | HanElectricity(瀚源) | 0.2922 | 0.58 |
10 | XLOT APDS-9960 手势传感器 | 3 | APDS-9960 | XLOT | JST PH2.0mm | 4 | XLOT | 30 | 90.00 |
11 | 拓竹 N20-10D侧出轴电机-25rpm (1个)- LA008 bambulab(Toy motor) | 1 | N20-10D LA008 bambulab | N20-10D | SH1.0mm-2P | 2 | Bambu lab | 29 | 29.00 |
12 | 劲爽5V伏大电流锂电池(Powerful 5V high current lithium battery) | 1 | JST2.54 | 2 | 劲爽电池 | 30 | 30.00 | ||
13 | 可编程全彩RGB灯条 | 2 | 14灯珠 RGB灯条(14 LED beads RGB light strip) | PH2.0 | 3 | 亚博智能 Yahboom | 13 | 26.00 | |
14 | 椴木板(Limestone board) | 1 | 60cm80cm3mm | 30 | 30.00 | ||||
15 | 3D打印 PLA/PETG 材料 3D Printing PLA/PETG Materials | 1 | Bambu | 20 | 20.00 | ||||
16 | Grove - Universal 4 Pin 20cm Unbuckled Cable | 1 | Seeed Studio(矽递科技) | 4 | 4.00 | ||||
17 | 4mm直径钢轴(4mm Diameter Steel Shaft) | 2 | 直径4mm,长度20mm Diameter 4mm, length 20mm | 劲功 | 0.04 | 0.08 | |||
SUM | 268.52 |
性能指标
响应性能:
- 手势识别延迟:< 200ms
- LED颜色切换:< 50ms
- WiFi连接时间:< 3s
- 电机启动时间:< 500ms
- MQTT 同步:<2s
续航能力:
- 正常使用模式:约4小时
交互范围:
- 手势识别距离:5-30cm
- WiFi控制距离:约50米
- 多设备同步:理论上对于任何订阅相同 MQTT 频道的智幻走马灯都可以实现同步
数字制造技能的全面运用
这个项目几乎用到了 Fab Academy 教授的所有制造技术:
📐** 2D/3D 设计**
- Fusion 360:整个灯笼结构体,包括复杂的齿轮系统和装配设计
- 嘉立创EDA:电路原理图和PCB布局
⚙️** 减材制造**
- 激光切割:精密的木质外壳切割
- CNC铣削:PCB电路板制造
- 乙烯基切割:灯笼罩板上的中国传统剪纸风格图案
🖨️** 增材制造**
- FDM 3D打印:复杂的齿轮和支撑结构
- 多色打印:增强视觉效果
🔌** 电子制造**
- 双面 PCB设计:在单面 PCB 设计基础上,挑战了双层 PCB 设计,从原理图到成品的完整流程
- PCB 商业生产:通过嘉立创下单制造双层 PCB
- 电子元件焊接:各种接头与元件焊接流程
- 调试测试:示波器和逻辑分析仪的运用
从设计到成品
质量控制与测试
每个制造环节都有对应的质量控制标准:
机械部分:
- 尺寸精度:±0.1mm
- 表面质量:无明显层纹
- 装配间隙:0.2-0.5mm
电子部分:
- 焊接质量:目视检查 + 导通测试
- 功能测试:分模块逐步验证
- 整机测试:长时间稳定性验证
项目评估标准
技术实现度评估
我对项目整体做了一个评估
核心功能验证(满分40分):
- ✅ 电机与齿轮传动控制:7/10分(改进齿轮后转笼转动比较稳定)
- ⚠️ 手势识别系统:7/10分(有时会失灵)
- ✅ LED灯效控制:8/10分
- ✅ 无线连接功能:8/10分
用户体验(满分25分):
- 🔄 交互响应速度:8/10分(持续优化中)
- ✅ 界面美观易用:6/10分
- ✅ 整体完成质量:4/5分
技术创新(满分25分):
- ✅ 系统复杂程度:6/10分
- ✅ 制造工艺难度:7/10分
- ✅ 创意独特程度:3/5分
文档完整性(满分10分):
- ✅ 过程记录详实:4/5分
- ✅ 开源资料完整:4/5分
项目总分:72/100分
项目的意义与价值
文化传承的数字化表达
这个项目不只是技术的展示,更是文化传承的新尝试。走马灯作为中华传统文化的载体,在数字化时代如何保持其文化内核而又焕发新的生命力?智幻走马灯给出了一个可能的答案。
传统元素的保留:
- 六边形的经典造型
- 旋转产生动态美感的核心理念
- 光影变化营造诗意氛围
现代科技的赋能:
- 可控的旋转和灯效
- 智能的交互方式
- 网络化的连接能力
开源精神的践行
项目的所有设计文件、代码、制作教程都将开源发布,希望能够:
- 为其他Maker提供完整的参考案例
- 推动传统文化的数字化创新
- 促进Fab Lab社区的知识共享
开源内容清单:
- 完整的3D模型文件
- PCB设计文件(原理图+Layout)
- 固件源代码
- 制作教程和BOM清单
- 视频演示和技术解析
反思与展望
18周的成长轨迹
回望这18周的项目历程,最大的收获不是掌握了多少新技术,而是学会了如何将复杂的想法转化为可实现的方案,如何在时间和资源的约束下做出合理的取舍。
技术层面的收获:
- 掌握了完整的数字制造工艺链
- 学会了系统性的工程思维
- 提升了问题解决能力
非技术层面的成长:
- 项目管理和时间规划能力
- 团队协作和沟通技巧
- 开源分享的意识和习惯
未来的改进方向
这个项目还有很多可以完善的地方:
功能扩展:
- 增加声音感应,实现音乐律动效果
- 加入温湿度传感器,打造智能环境灯
- 开发手机APP,提供更丰富的控制选项
性能优化:
- 更高效的电源管理系统
- 更快的手势识别算法
- 更稳定的无线通信协议
产品化探索:
- 模块化设计,便于用户自定义
- 成本进一步优化,提高性价比
- 建立用户社区,促进创意分享
致谢
这个项目的完成离不开很多人的帮助:
- 导师团队:Pradnya Shindekar,于剑锋给我们提供了专业的技术指导和建议
- Seeed Studio:提供费用支撑,另外特别感谢徐国群工程师提供了专业的 CNC 使用指导,硬件工程师瞿翔楠提供了焊接的专业指导
- 柴火创客空间:提供了完善的制造设备和环境
- Fab Lab社区:丰富的开源资源和经验分享
- 家人朋友:感谢妻子在整个学习过程中的照顾
- Claude AI:在PCB设计和技术方案选择上提供了宝贵建议
结语
智幻走马灯,不只是我的 Fab Academy 最终项目,更是我对传统文化与现代科技融合的一次探索。在这个快速变化的时代,如何让传统文化焕发新的生命力?如何让科技更有温度和人文关怀?
或许答案就在我们每一个小小的创造里,在每一次大胆的尝试里,在每一份开源的分享里。
希望这盏小小的智幻走马灯,能够点亮更多人心中对创造的热情,对文化的热爱,对未来的憧憬。