Skip to content

智幻走马灯

当千年传统遇见数字制造。

项目概览:传统美学与现代科技的完美融合

项目缘起

还记得小时候第一次见到走马灯时的惊艳吗?烛火摇曳中,剪纸人影在灯罩上翩翩起舞,仿佛有了生命。这个始于汉代的古老玩意儿,用最简单的物理原理——热气流上升——创造出了诗意的视觉体验。

但作为一个在 Fab Lab 摸爬滚打了18周的"数字工匠",我忍不住想:如果让这盏千年古灯插上现代科技的翅膀,会碰撞出怎样的火花?

于是,智幻走马灯诞生了。

画板

左:传统走马灯的诗意;右:智幻走马灯的科技美学

它到底是什么?

简单说,这是一个会"读心术"的智能灯笼。

核心功能一览

🎯精准旋转控制:告别不可控的热气流,用步进电机实现精确到转速的控制。想要慢悠悠地冥想,还是快节奏地狂欢?一个手势搞定。

🌈可编程灯光魔法:28颗 WS2812B RGB LED 排成正反两列,360°无死角照明。从温暖的烛光黄到梦幻的彩虹渐变,千变万化只在弹指间。

👋手势识别:最多支持三个 APDS-9960 传感器呈120°分布,无论你从哪个角度"撩"它,它都能秒懂你的意图。上下左右,每个手势都有专属的灯光回应。

📱Web 远程控制:手机、电脑、平板,只要能上网就能控制。朋友聚会时,大家一起"指挥"灯笼表演,绝对是全场焦点。

🔗多设备同步:基于 MQTT 协议,多个智幻走马灯可以组成"灯阵",同步变化,场面相当震撼。

功能演示:手势控制、Web界面、同步效果

结构设计

物理结构设计

灯笼的结构占用了大量时间,因为灯笼要有外部框架,还有内部旋转结构,由电机驱动的齿轮机构等,把这些系统整合在一起花费了大量的时间。

我使用 Fusion 360 设计了整个灯笼结构体,包括复杂的齿轮系统和装配设计:

根据设计文件渲染的效果图如下:

画板

最终设计版本的效果图,由 Fusion 360 设计

然后我在 Fusion 里复制了一个文件,将所有结构零件展开如下图所示:

画板

在 Fusion 360 里展开所有零件并渲染获得插图,然后我对需要制造的结构件文件都进行了编号

结构零件列表

结构件编号零件名称数量功能说明制造方式材质
手势传感器(Gesture Sensor)3最多支撑安装 3 个手势传感器,用来感受各个方向的手势,直接控制灯效和电机转动购买
C4顶盖(Top Cover)1圆筒形,位于灯笼最顶部,有 3 个 120 度分布的手势传感器支架,和 PCB 舱通过嵌套固定3D 打印PLC/PETG
XIAO ESP32C31系统核心控制器购买
半圆 PCB(Semicircle PCB)1电子硬件开发板,通过定位固定孔置于 PCB 舱内铣削/嘉立创生产覆铜板/FR4
C3PCB 舱(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 驱动齿轮 A3D 打印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)61mm 厚半薄板,6 块板上可以贴乙烯基切割的贴纸图案,可以根据自己喜欢的主题 DIY3D 打印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 生成,然后测试改进获得。

电机开关测试程序

最开始只连接电机,并测试电机是否可以正常工作,测试程序如下:

cpp
#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),测试灯效控制程序如下:

cpp
#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 同步工作的测试程序:

cpp
#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);
}

添加多手势传感器的测试程序

cpp
#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 模块的测试程序

  1. Web界面特点

现代化设计:渐变背景、毛玻璃效果、阴影动画。
响应式布局:手机、平板、电脑都能完美显示。
实时状态显示:当前开关状态和模式名称。
三个核心按钮:

🔆 开启走马灯
🔅 关闭走马灯
🎨 切换灯效

  1. API接口

GET /api/status:获取当前状态(开关状态、模式信息)。
POST /api/control:发送控制命令(on/off/switch)。

  1. 智能交互

状态同步:Web界面每3秒自动更新状态。
操作反馈:每个操作都有成功/失败提示。
防误操作:关闭状态下不能切换模式(Web和手势一致)。

🔧 使用方法

修改WiFi信息:

cpp
cppconst char* ssid = "YourWiFiName";        // 改为您的WiFi名称
const char* password = "YourWiFiPassword"; // 改为您的WiFi密码

上传程序后:

串口监视器会显示IP地址
用浏览器访问该IP地址即可看到控制界面

双重控制:

手势控制:向上开启,向下关闭,左右切换模式
Web控制:点击按钮实现相同功能

cpp
#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多设备同步

  • 自动设备发现
  • 实时状态同步
  • 防循环消息机制

六种灯效模式

  • 传统走马灯
  • 火焰效果
  • 彩虹旋转
  • 呼吸同步
  • 流星追逐
  • 节日庆典
cpp
#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.NameQuantityCommentDesignatorFootprintPin CountManufacturerLCSC PriceTotal price(RMB)
110uF110uFC1CAP-TH_BD4.0-P1.50-D0.8-FD2HUAWEI(华威集团)0.12920.13
2100nF1100nFC2CAP-TH_L5.0-W2.5-P5.00-D1.02Dersonic(德尔创)0.12160.12
3WAFER-PH2.0-4PZZ4WAFER-PH2.0-4PZZFAN1,GROVE1,GROVE2,GROVE3CONN-TH_4P-P2.00_FWF20001-S04S22W5B4XUNPU(讯普)0.10450.42
41.0T-4P立贴31.0T-4P立贴GESTURE1,GESTURE2,GESTURE3CONN-SMD_1.0T-4P-LT6BOOMELE(博穆精密)0.38021.14
5PZ254V-11-02P1PZ254V-11-02PJBATT1HDR-TH_2P-P2.54-V-M2XFCN(兴飞)0.08510.09
6WAFER-PH2.0-3PZZ2WAFER-PH2.0-3PZZLEDSTRIP1,LEDSTRIP2CONN-TH_PH2.0D-L-3P3XUNPU(讯普)0.07910.16
7SS-12D10-G0901SS-12D10-G090SW1SW-TH_3P-L12.7-W6.6-P4.70_C99000343693G-Switch(品赞)1.8011.80
8XIAOESP32C31XIAOESP32C3XIAOESP32C3CONN-SMD_11399105423Seeed Studio(矽递科技)3535.00
92541FV-7P-B22541FV-7P-BXIAOL1,XIAOR1HDR-TH_7P-P2.54-V-F7HanElectricity(瀚源)0.29220.58
10XLOT APDS-9960 手势传感器3APDS-9960XLOTJST PH2.0mm4XLOT3090.00
11拓竹 N20-10D侧出轴电机-25rpm (1个)- LA008 bambulab(Toy motor)1N20-10D LA008 bambulabN20-10DSH1.0mm-2P2Bambu lab2929.00
12劲爽5V伏大电流锂电池(Powerful 5V high current lithium battery)1JST2.542劲爽电池3030.00
13可编程全彩RGB灯条214灯珠 RGB灯条(14 LED beads RGB light strip)PH2.03亚博智能 Yahboom1326.00
14椴木板(Limestone board)160cm80cm3mm3030.00
153D打印 PLA/PETG 材料 3D Printing PLA/PETG Materials1Bambu2020.00
16Grove - Universal 4 Pin 20cm Unbuckled Cable1Seeed Studio(矽递科技)44.00
174mm直径钢轴(4mm Diameter Steel Shaft)2直径4mm,长度20mm Diameter 4mm, length 20mm劲功0.040.08
SUM268.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 最终项目,更是我对传统文化与现代科技融合的一次探索。在这个快速变化的时代,如何让传统文化焕发新的生命力?如何让科技更有温度和人文关怀?

或许答案就在我们每一个小小的创造里,在每一次大胆的尝试里,在每一份开源的分享里。

希望这盏小小的智幻走马灯,能够点亮更多人心中对创造的热情,对文化的热爱,对未来的憧憬。