基于ESP32-C6与WebSocket的自制智能彩灯控制系统开发实践
1. 项目概述从一块开发板到智能彩灯控制中枢最近在捣鼓智能家居的小玩意儿手头正好有一块DFRobot的FireBeetle 2 ESP32 C6开发板。这块板子挺有意思集成了Wi-Fi 6和蓝牙5.0性能比经典的ESP32要强不少关键是功耗控制得挺好很适合做需要长时间联网的物联网终端。我琢磨着用它来驱动一串WS2812B的可编程RGB彩灯再自己写个手机APP来控制应该是个不错的练手项目。这个想法听起来简单但实际做下来从硬件连接到网络协议再到APP界面设计每一步都有不少门道。今天就把我整个从零搭建“自制APP控制彩灯”系统的过程、踩过的坑以及一些优化心得详细地分享出来。无论你是刚接触物联网的新手还是想找点新思路的老玩家相信都能从中找到有用的东西。这个项目的核心目标很明确摆脱对成品智能灯带APP的依赖完全自主掌控。这意味着我们需要自己搭建服务端在ESP32 C6上、定义通信协议、并开发一个专属的控制客户端手机APP。最终实现的效果是手机APP通过Wi-Fi连接到ESP32 C6可以随心所欲地控制灯带的颜色、亮度、动态效果如彩虹渐变、呼吸灯等并且所有逻辑都由我们自己编写的代码决定。下面我们就从硬件选型与连接开始一步步拆解。2. 硬件选型、连接与供电方案解析2.1 核心控制器为什么是FireBeetle 2 ESP32 C6市面上ESP32的开发板很多我选择FireBeetle 2 ESP32 C6主要有几个考量。首先ESP32-C6是乐鑫较新的芯片它最大的亮点是原生支持Wi-Fi 6802.11ax和蓝牙5.0。对于这个彩灯项目Wi-Fi 6的高效和低延迟在理论上有助于更流畅的控制响应尤其是在网络环境复杂、设备多的情况下。虽然控制几个彩灯数据量不大但“战未来”的考虑总是好的。其次这块板子的设计比较紧凑引脚布局清晰自带Type-C接口调试和供电都很方便。最后DFRobot的生态和文档支持一向不错遇到问题比较容易找到社区解答。当然如果你手头只有经典的ESP32开发板如ESP32-DevKitC也完全没问题。本项目涉及的核心功能——Wi-Fi连接、TCP/UDP或HTTP服务器、PWM/数字信号控制——经典ESP32都能完美胜任。选择C6更多是个人对新硬件尝鲜的兴趣。在代码层面两者在Arduino框架下的兼容性很高大部分库可以通用但需要注意一些引脚定义和低功耗函数可能略有差异。2.2 彩灯模块WS2812B的优势与注意事项彩灯我选用的是最常见的WS2812B智能RGB灯带。它内部集成了控制芯片和RGB LED只需要一根数据线Din就能实现全彩控制极大地简化了布线。每个灯珠都可以独立寻址这意味着我们可以轻松实现流水、渐变、图案等复杂效果这是传统并联RGB灯带无法比拟的。使用WS2812B有几个关键点必须注意。第一是供电。每个灯珠在全白最亮时电流可能高达60mA。如果你计划驱动几十个甚至上百个灯珠那么总电流会非常惊人。一个常见的5V 10A电源适配器理论上也只能驱动不到170个灯珠在最极端情况下。因此务必根据灯珠数量计算好总电流并选择功率余量充足的电源。第二是信号电平。WS2812B的数据输入要求是5V逻辑电平而ESP32 C6的GPIO输出是3.3V。虽然很多情况下3.3V信号也能勉强驱动不推荐但为了长期稳定和远距离传输最好使用一个逻辑电平转换器如74HCT125或者选择那些标称支持3.3V控制的“3.3V兼容版”WS2812B。第三是数据线连接。数据线应尽量短并在靠近ESP32引脚处加一个300-500欧姆的电阻以抑制信号反射。如果灯带较长还需要在末端并联一个约100uF的电解电容和0.1uF的瓷片电容进行电源滤波。2.3 电路连接与供电设计实战连接非常简单但细节决定成败。以下是接线示意图电源部分这是重中之重。我采用独立供电方案。一个5V/5A的开关电源直接给WS2812B灯带供电。电源的“正极5V”接到灯带的VCC“负极GND”同时接到灯带的GND和ESP32 C6的GND引脚。务必确保ESP32和灯带共地这是信号正常传输的基础。信号部分ESP32 C6的一个GPIO引脚我选择的是GPIO4因为它通常没有特殊复用功能通过一个330欧姆的电阻连接到WS2812B灯带的数据输入Din引脚。ESP32供电ESP32 C6可以通过其Type-C口由电脑或手机充电器供电5V也可以从上述5V开关电源经过一个低压差稳压器LDO降压到3.3V来供电。为了简化我直接用了另一个5V/1A的适配器通过Type-C口给ESP32供电。切忌直接拿驱动灯带的5V大电流电源接到ESP32的5V引脚除非你确认该电源非常干净且电压精准否则容易损坏开发板。注意上电顺序。理想情况下应先给控制板ESP32上电让其程序初始化完成再给灯带上电。或者同时上电。避免先给灯带上电而ESP32未启动导致数据引脚状态不确定可能引起灯珠乱闪。3. 固件开发在ESP32 C6上构建WebSocket服务器硬件连好后我们就要给ESP32 C6编写固件Firmware。它的角色是一个网络服务器接收来自手机APP的指令并解析指令来控制WS2812B。为了实现实时、双向的低延迟控制我放弃了传统的HTTP请求如REST API而选择了WebSocket协议。3.1 为什么选择WebSocket而非HTTPHTTP协议是无状态的每次控制如改颜色都需要手机APP发起一个完整的请求-响应过程。频繁操作时建立连接、传输头信息的开销较大感觉不够“跟手”。WebSocket则不同它在初次HTTP握手升级后就建立了一个全双工的持久化连接。在这个连接上服务器和客户端可以随时主动发送数据非常适合实时控制场景。例如APP发送一个{cmd:color, r:255, g:0, b:0}的JSON字符串ESP32收到后立刻解析并设置灯带为红色整个过程在毫秒级完成体验非常流畅。3.2 核心库与开发环境搭建我使用Arduino IDE进行开发因为它对ESP系列支持友好库生态丰富。需要安装的库主要有FastLED这是驱动WS2812B等智能彩灯的事实标准库。它优化了时序提供了丰富的颜色和效果函数效率极高。ESPAsyncWebServer和ESPAsyncTCP这是一个异步Web服务器库性能比标准WiFiServer好更重要的是它内置了WebSocket支持让我们能轻松创建WebSocket端点。在Arduino IDE中通过“工具”-“开发板”-“开发板管理器”安装“ESP32 Arduino”支持包确保版本支持ESP32-C6。然后在“项目”-“加载库”-“管理库”中搜索并安装上述库。3.3 固件代码结构详解以下是固件程序的核心逻辑模块#include WiFi.h #include ESPAsyncWebServer.h #include FastLED.h #define LED_PIN 4 #define NUM_LEDS 30 #define BRIGHTNESS 100 CRGB leds[NUM_LEDS]; const char* ssid 你的Wi-Fi名称; const char* password 你的Wi-Fi密码; AsyncWebServer server(80); AsyncWebSocket ws(/ws); // WebSocket端点路径 void onWebSocketEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { // 处理WebSocket事件连接、断开、收到数据 if(type WS_EVT_DATA){ // 这里解析客户端发来的JSON指令 data[len] \0; // 确保字符串结束 String message String((char*)data); parseCommand(message); } } void parseCommand(String jsonStr) { // 简易JSON解析实际项目建议用ArduinoJson库 // 示例解析 {cmd:solid, r:255, g:100, b:50} if(jsonStr.indexOf(\cmd\:\solid\) ! -1){ // 提取r,g,b值 int r extractValue(jsonStr, r); int g extractValue(jsonStr, g); int b extractValue(jsonStr, b); fill_solid(leds, NUM_LEDS, CRGB(r, g, b)); FastLED.show(); } else if(jsonStr.indexOf(\cmd\:\effect\) ! -1){ String effect extractStringValue(jsonStr, name); if(effect rainbow) startRainbowEffect(); else if(effect breath) startBreathEffect(); } else if(jsonStr.indexOf(\cmd\:\brightness\) ! -1){ int val extractValue(jsonStr, value); FastLED.setBrightness(constrain(val, 0, 255)); FastLED.show(); } } void setup() { Serial.begin(115200); FastLED.addLedsWS2812B, LED_PIN, GRB(leds, NUM_LEDS); FastLED.setBrightness(BRIGHTNESS); fill_solid(leds, NUM_LEDS, CRGB::Black); FastLED.show(); WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(1000); Serial.println(Connecting to WiFi...); } Serial.println(WiFi Connected!); Serial.print(IP Address: ); Serial.println(WiFi.localIP()); ws.onEvent(onWebSocketEvent); server.addHandler(ws); // 可选提供一个简单的网页用于测试 server.on(/, HTTP_GET, [](AsyncWebServerRequest *request){ request-send(200, text/html, htmlbodyh1FireBeetle LED Controller/h1/body/html); }); server.begin(); } void loop() { ws.cleanupClients(); // 清理断开连接的客户端 // 如果有动态效果正在运行在这里更新 updateEffects(); }代码关键点解析WebSocket事件处理onWebSocketEvent函数是通信中枢。当手机APP客户端通过WebSocket发送数据时会触发WS_EVT_DATA事件。我们在这里拿到原始数据data将其转换为字符串后进行解析。命令解析parseCommand函数负责解析APP发来的JSON指令。我实现了一个简单的字符串查找和值提取逻辑。对于更复杂的交互强烈建议使用ArduinoJson库它能安全、高效地处理JSON序列化和反序列化。异步服务器使用AsyncWebServer意味着网络服务不会阻塞主循环。我们可以在loop()中平滑地更新灯光动画如彩虹循环同时随时响应网络控制命令两者互不干扰。效果处理动态效果如彩虹、呼吸需要状态保持和逐帧更新。我通常定义一些全局变量如效果类型、当前步数、颜色相位等在独立的updateEffects()函数中根据当前激活的效果类型计算每一帧的颜色并刷新灯带。3.4 固件烧录与网络配置将代码编译上传到FireBeetle 2 ESP32 C6。上传成功后打开串口监视器波特率115200你将看到ESP32连接Wi-Fi的过程并打印出它的IP地址例如192.168.1.105。记下这个IP手机APP将连接到这个地址。实操心得硬编码Wi-Fi密码在代码里不利于维护。一个更专业的做法是在首次启动时如果无法连接预设网络则ESP32进入“配网模式”如启动一个AP热点并提供一个配置页面。可以使用WiFiManager库来优雅地实现这个功能极大提升用户体验。4. 手机APP开发使用Flutter构建跨平台控制端为了让控制端能在Android和iOS上通用我选择了Flutter框架。它用Dart语言编写一套代码可以编译到两个平台开发效率很高。即使你没有Flutter基础跟着下面的步骤也能快速搭建起来。4.1 开发环境与项目创建首先确保你的电脑安装了Flutter SDK和Android Studio或Xcode用于iOS。在终端中创建一个新项目flutter create led_controller_app cd led_controller_app然后用你喜欢的IDE如Android Studio或VS Code打开项目。4.2 核心依赖与UI布局我们需要一个WebSocket客户端库来连接ESP32。在项目的pubspec.yaml文件中添加依赖dependencies: flutter: sdk: flutter web_socket_channel: ^2.4.0 # WebSocket客户端库 provider: ^6.1.1 # 状态管理可选但推荐然后运行flutter pub get安装依赖。APP的UI主要包含以下几部分连接面板输入ESP32的IP地址建立/断开WebSocket连接。颜色选择器一个直观的拾色器用于选择静态颜色。效果选择区按钮列表用于切换不同的动态效果。亮度滑块一个Slider控件用于实时调节全局亮度。4.3 核心代码实现连接与通信我们创建一个主要的控制页面ControlPage。其核心是管理一个WebSocket连接。import package:flutter/material.dart; import package:web_socket_channel/web_socket_channel.dart; import package:web_socket_channel/io.dart; class ControlPage extends StatefulWidget { override _ControlPageState createState() _ControlPageState(); } class _ControlPageState extends StateControlPage { WebSocketChannel? _channel; String _ipAddress 192.168.1.105; // 默认IP实际应由用户输入 bool _connected false; Color _currentColor Colors.blue; double _brightness 0.5; // 范围0-1对应0-255 void _connect() { try { final url ws://$_ipAddress/ws; // WebSocket地址 _channel IOWebSocketChannel.connect(url); _channel!.stream.listen( (message) { // 处理从ESP32发来的消息如状态确认 print(Received: $message); }, onDone: () { // 连接关闭 setState(() { _connected false; }); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(Disconnected))); }, onError: (error) { print(Error: $error); }, ); setState(() { _connected true; }); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(Connected!))); } catch (e) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(Connection failed: $e))); } } void _disconnect() { _channel?.sink.close(); setState(() { _connected false; }); } // 发送JSON格式的命令 void _sendCommand(MapString, dynamic command) { if (_channel ! null _connected) { final jsonString jsonEncode(command); _channel!.sink.add(jsonString); print(Sent: $jsonString); } } // 发送纯色命令 void _sendColor(Color color) { _sendCommand({ cmd: solid, r: color.red, g: color.green, b: color.blue, }); } // 发送效果命令 void _sendEffect(String effectName) { _sendCommand({ cmd: effect, name: effectName, }); } // 发送亮度命令 void _sendBrightness(double value) { int brightnessValue (value * 255).toInt(); _sendCommand({ cmd: brightness, value: brightnessValue, }); } override void dispose() { _channel?.sink.close(); super.dispose(); } override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(LED Controller)), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ // 连接控制区 Row( children: [ Expanded( child: TextField( controller: TextEditingController(text: _ipAddress), onChanged: (value) _ipAddress value, decoration: InputDecoration(labelText: ESP32 IP), ), ), SizedBox(width: 10), ElevatedButton( onPressed: _connected ? _disconnect : _connect, child: Text(_connected ? Disconnect : Connect), style: ElevatedButton.styleFrom( backgroundColor: _connected ? Colors.red : Colors.green, ), ), ], ), Divider(height: 30), // 颜色选择器 Text(Static Color, style: Theme.of(context).textTheme.titleMedium), SizedBox(height: 10), ColorPickerWidget( onColorChanged: (color) { _currentColor color; if (_connected) _sendColor(color); }, ), Divider(height: 30), // 效果选择区 Text(Effects, style: Theme.of(context).textTheme.titleMedium), Wrap( spacing: 8.0, children: [Rainbow, Breath, Fire, Theater Chase].map((effect) { return ElevatedButton( onPressed: _connected ? () _sendEffect(effect.toLowerCase()) : null, child: Text(effect), ); }).toList(), ), Divider(height: 30), // 亮度控制 Text(Brightness, style: Theme.of(context).textTheme.titleMedium), Slider( value: _brightness, min: 0.0, max: 1.0, divisions: 100, label: (_brightness * 100).round().toString(), onChanged: _connected ? (value) { setState(() _brightness value); _sendBrightness(value); } : null, ), ], ), ), ); } }代码关键点解析连接管理_connect方法使用IOWebSocketChannel.connect建立连接并监听stream以接收服务器消息。_disconnect方法关闭连接。良好的连接状态管理_connected变量用于控制UI元素的可用性。命令封装_sendCommand方法将Dart的Map对象通过jsonEncode转换为JSON字符串发送。这样在ESP32端就能用ArduinoJson轻松解析。将颜色、效果、亮度等操作封装成独立方法使代码更清晰。UI交互颜色选择器可以使用第三方库如flutter_colorpicker。为了简化示例中用一个占位ColorPickerWidget表示。效果按钮和亮度滑块在操作时会立即发送对应的JSON命令。错误处理网络操作必须包裹在try-catch中并用SnackBar给用户友好的提示。监听onDone和onError来及时更新连接状态。4.4 APP打包与测试在项目根目录运行flutter run可以在连接的手机或模拟器上直接运行调试。确认功能正常后可以分别打包Android:flutter build apk --releaseiOS: 需要在macOS上连接苹果开发者账号使用Xcode进行归档和发布。将生成的APK文件安装到安卓手机确保手机和ESP32连接在同一个Wi-Fi网络下在APP中输入正确的ESP32 IP地址点击连接你就可以完全掌控你的彩灯了。5. 高级功能扩展与优化实践基础的控制实现后我们可以让这个系统变得更智能、更强大。这里分享几个我实践过的扩展方向。5.1 动态效果引擎的设计在固件端简单的fill_solid显示纯色很容易。但如何流畅地运行彩虹、呼吸、流星雨等效果呢关键在于状态机和非阻塞更新。以彩虹效果为例我们不应该使用delay来制造渐变这会阻塞整个程序导致网络无响应。正确做法是利用millis()函数进行时间管理。// 全局效果状态变量 enum EffectType { EFFECT_NONE, EFFECT_RAINBOW, EFFECT_BREATH }; EffectType currentEffect EFFECT_NONE; unsigned long prevEffectUpdate 0; uint8_t hueOffset 0; // 用于彩虹效果的色相偏移 uint8_t breathBrightness 0; bool breathDirection true; // 呼吸方向 void setEffect(EffectType effect) { currentEffect effect; prevEffectUpdate millis(); hueOffset 0; breathBrightness 0; breathDirection true; } void updateEffects() { unsigned long now millis(); switch(currentEffect) { case EFFECT_RAINBOW: if (now - prevEffectUpdate 30) { // 每30ms更新一帧 prevEffectUpdate now; // 使用FastLED内置的彩虹渐变函数 fill_rainbow(leds, NUM_LEDS, hueOffset, 7); // 每次色相偏移1 FastLED.show(); } break; case EFFECT_BREATH: if (now - prevEffectUpdate 20) { // 每20ms更新一帧 prevEffectUpdate now; // 计算呼吸亮度 (0-255) if(breathDirection) { breathBrightness 5; if(breathBrightness 250) breathDirection false; } else { breathBrightness - 5; if(breathBrightness 5) breathDirection true; } // 假设呼吸基色为白色可根据需要修改 fill_solid(leds, NUM_LEDS, CRGB(breathBrightness, breathBrightness, breathBrightness)); FastLED.show(); } break; case EFFECT_NONE: default: // 静态模式无需更新 break; } }在loop()函数中持续调用updateEffects()。当APP发送{cmd:effect,name:rainbow}时固件调用setEffect(EFFECT_RAINBOW)激活该效果updateEffects()就会开始自动循环更新。发送{cmd:effect,name:none}或一个新的纯色命令则切换回EFFECT_NONE。5.2 场景与定时任务你可以进一步实现“场景”功能。例如APP发送一个场景数组{ cmd: scene, steps: [ {duration: 5000, effect: solid, color: {r:255, g:0, b:0}}, {duration: 3000, effect: rainbow}, {duration: 0, effect: breath} // 0表示无限时长 ] }固件端解析后按顺序执行每个步骤并在指定时间后自动切换到下一步。这需要维护一个场景队列和计时器。更进阶的可以利用ESP32的RTC实时时钟和NTP网络时间协议同步时间实现定时开关或切换场景真正迈向自动化。5.3 OTA空中升级功能集成当你想更新ESP32的固件时难道每次都要插上数据线吗太麻烦了。集成OTAOver-The-Air功能后你可以通过Wi-Fi直接上传新的固件文件。在Arduino代码中包含ArduinoOTA库并在setup()中添加初始化代码#include ArduinoOTA.h void setup() { // ... 其他初始化代码 ... ArduinoOTA.setHostname(firebeetle-led-controller); ArduinoOTA.setPassword(your_ota_password); // 设置密码更安全 ArduinoOTA.begin(); } void loop() { ArduinoOTA.handle(); // 必须定期调用以处理OTA请求 // ... 你的主循环代码 ... }编译时在Arduino IDE的“工具”菜单中选择“端口”-“网络端口”找到你的ESP32。上传时代码就会通过网络传输过去。这对于将设备安装在高处或封闭环境后进行调整简直是神器。5.4 功耗优化考量FireBeetle 2 ESP32 C6本身功耗不高但加上几十个WS2812B整体功耗就上去了。如果希望做成电池供电的装饰灯就需要优化深度睡眠在不需要控制时让ESP32进入深度睡眠模式仅由定时器或外部中断唤醒。这需要硬件上连接一个唤醒引脚如GPIO0。灯光亮度与数量降低FastLED.setBrightness()的值能显著省电。非必要情况不要点亮所有灯珠。Wi-Fi休眠在连接保持的情况下可以调用WiFi.setSleep(true)允许Wi-Fi模块在空闲时休眠但可能会轻微增加控制延迟。6. 常见问题、排查技巧与调试心得在开发过程中我遇到了不少问题这里总结一下希望能帮你少走弯路。6.1 连接与通信问题问题现象可能原因排查步骤APP无法连接提示连接失败/超时1. IP地址错误2. ESP32未成功连接Wi-Fi3. 防火墙/路由器设置阻止了WebSocket端口通常为801. 检查串口监视器确认ESP32获取的IP是否正确。2. 确认手机和ESP32在同一局域网连接同一个路由器。3. 在电脑浏览器输入http://[ESP32_IP]看是否能打开ESP32上的测试网页如果有。4. 尝试关闭手机和电脑的防火墙临时测试。连接成功但发送指令灯无反应1. WebSocket路径不匹配2. JSON格式错误3. 固件命令解析逻辑有误1. 确认APP中WebSocket URL是ws://IP/ws与固件中AsyncWebSocket ws(/ws)匹配。2. 在APP和固件端都打印发送/接收的原始字符串对比是否一致。确保JSON键名如cmd,r完全匹配。3. 检查固件端parseCommand函数逻辑特别是字符串比较是否准确。控制响应延迟高或卡顿1. Wi-Fi信号差2. ESP32主循环被阻塞3. 网络内有广播风暴1. 将设备靠近路由器。2. 检查固件loop()中是否有delay()长延时用millis()非阻塞方式重构。3. 尝试更换路由器信道减少干扰。6.2 灯光显示问题问题现象可能原因排查步骤灯带完全不亮1. 供电问题2. 数据线接错3. 代码中未调用FastLED.show()1. 用万用表测量灯带VCC和GND之间是否有5V电压。2. 确认数据线接到了正确的Din引脚且ESP32已接地。3. 在setup()中先写一个简单的测试代码如leds[0] CRGB::Red; FastLED.show();看第一个灯是否亮起。部分灯珠颜色异常或乱闪1. 电源功率不足2. 数据信号受到干扰3. 时序问题1. 这是最常见的原因增加电源功率或减少点亮的灯珠数量测试。2. 在数据线靠近ESP32端加一个330欧姆电阻并在灯带电源输入端并联一个大电容如470uF。3. 确认FastLED.addLeds中的芯片类型和颜色顺序如WS2812B, GRB设置正确。动态效果闪烁或不平滑1. 刷新率太低2. 效果更新逻辑有误1. 确保updateEffects()被频繁调用在loop()中无阻塞。2. 检查效果更新间隔如if (now - prevEffectUpdate 30)是否合理太短可能处理不过来太长则卡顿。6.3 开发与调试技巧分步调试不要试图一次性写完所有功能。先让一个灯珠亮起来再让所有灯珠显示纯色然后加入Wi-Fi连接最后实现WebSocket和控制逻辑。每步都验证通过。善用串口打印在固件代码的关键位置如连接Wi-Fi成功、收到WebSocket数据、解析命令后使用Serial.println()输出信息这是追踪程序流和变量值最直接的方法。使用桌面客户端测试在开发APP之前可以先使用Chrome浏览器的“WebSocket测试客户端”扩展程序或者用Python写一个简单的脚本直接向ESP32的WebSocket端点发送JSON命令。这能快速验证固件逻辑是否正确隔离前端问题。供电是万恶之源任何奇怪的、随机的灯光问题首先怀疑电源。用一个质量好、功率足的5V电源单独给灯带供电问题往往就消失了。这个项目从一块小小的FireBeetle 2 ESP32 C6开发板出发串联起了硬件电路、嵌入式编程、网络通信和移动应用开发多个环节。当你亲手用自己写的APP点亮第一串彩灯并随心所欲地变换色彩和效果时那种成就感是无可替代的。它不仅仅是一个彩灯控制器更是一个可扩展的物联网原型平台。你可以基于此加入传感器如声音传感器随音乐律动、人体感应自动亮灯或者接入更智能的家居平台探索的空间非常大。希望这篇详尽的记录能为你点亮灵感动手创造出属于自己的智能光效作品。