1. 项目概述
如果你和我一样,是个喜欢在桌面上折腾点小玩意儿,又对Elgato那套简洁高效的补光灯系统情有独钟的创作者,那你肯定也想过:能不能摆脱手机App,用一个更直接、更有“实体感”的控制器来管理灯光?今天分享的这个项目,就是基于这个想法诞生的。它本质上是一个用CircuitPython和ESP32-S3实现的Elgato智能灯光物理控制器。核心思路很简单:利用Elgato灯光开放的HTTP API,让一块开发板接入你的Wi-Fi网络,然后通过几个物理按钮和旋钮,直接向灯光发送控制指令。这听起来像是软件工程师的活儿,但得益于CircuitPython的易用性,即使你嵌入式开发经验不多,也能亲手把它做出来。
这个控制器能做什么?它集成了三颗物理按键和一个旋转编码器,配合一块小巧的TFT屏幕,实现了对Elgato Key Light或Key Light Mini的完全控制。你可以用它开关灯光、无级调节亮度(10%-100%)和色温(2900K-7000K),并且屏幕会实时显示当前状态。更重要的是,它还能从灯光读取当前设置,确保控制器和灯光状态同步,避免了你用手机App调完,控制器还显示旧数据的尴尬。这不仅仅是复制手机App的功能,而是创造了一个专属于工作台的、触手可及的交互终端,特别适合直播、视频会议或专注创作时快速调整光线,那种“即拧即用”的物理反馈是触摸屏无法替代的。
2. 核心硬件选型与设计思路
2.1 主控板:为什么是Feather ESP32-S3 Reverse TFT?
这个项目的核心大脑是Adafruit的Feather ESP32-S3 Reverse TFT开发板。选择它,而不是更常见的ESP32开发板,是经过一番考量的。首先,ESP32-S3芯片提供了可靠的Wi-Fi连接能力,这是与Elgato灯光通信的基础。其次,这块板子“All in One”的特性极大地简化了我们的硬件设计。它板载了TFT显示屏,省去了我们额外连接屏幕的麻烦;采用了Feather标准外形和接口,与大量Grove/STEMMA QT传感器模块兼容;更重要的是,其“Reverse TFT”设计意味着屏幕和主要元件在板子两面,为我们后续装入3D打印外壳提供了完美的布局,能让屏幕正面朝外,而USB接口和电池接口朝内或朝侧,非常整洁。
注意:市面上ESP32开发板型号繁多,务必确认你拿到的是Feather ESP32-S3 Reverse TFT(产品号5691)。它的引脚定义、屏幕驱动库以及物理尺寸都与常规ESP32开发板不同,直接替换可能导致代码和结构都需要大改。
2.2 输入设备:旋转编码器与按键的搭配逻辑
人机交互部分,我们采用了“旋钮+多按键”的组合,这是一种经过验证的高效交互模式。
- 旋转编码器:负责调节亮度/色温这两个连续变量。与电位器相比,旋转编码器可以无限旋转,没有物理终点,并且通常带有触感定位(detent),每“格”的反馈清晰,非常适合进行精细或快速的数值调整。我们通过I2C接口的Seesaw芯片来读取它,这比直接接GPIO占用更少的引脚,且库函数成熟稳定。
- 三颗独立按键:赋予明确、离散的功能。D0键设计为“开关/发送”键,短按即可切换灯光开关状态,并将当前旋钮设定的亮度、色温值发送出去。D1键是“仅发送”键,当你只想更新亮度色温而不改变灯的开关状态时使用。D2键是“读取”键,用于从灯光同步当前状态到控制器。这种分工明确的按键逻辑,能有效避免误操作,让交互意图清晰。
2.3 供电与结构设计考量
项目支持USB-C供电和3.7V锂聚合物电池供电两种方式。这意味着你可以把它做成一个完全无线的遥控器,随意放在桌面的任何位置。在结构设计上,使用3D打印外壳将整个系统封装起来,不仅美观,也能保护内部电路。外壳设计需要精确考虑几个要点:为TFT屏幕开窗、为旋转编码器的旋钮和NeoPixel指示灯留出孔位、为三个按键预留按压空间,以及妥善安置电池。原设计提供的STL文件已经很好地平衡了这些需求,如果你需要自定义,这些就是关键参考点。
3. 软件环境搭建与核心配置
3.1 CircuitPython固件刷写与开发环境
CircuitPython是MicroPython的一个分支,由Adafruit主导开发,其最大优势在于将开发板变成一个可移动存储设备(CIRCUITPY盘符),你可以像编辑文本文件一样用任何编辑器编写code.py,保存后代码自动运行。这彻底告别了传统的“编译-烧录”循环,极大地提升了原型开发速度。
刷写步骤与避坑指南:
- 获取固件:访问CircuitPython官网,找到“Feather ESP32-S3 Reverse TFT”型号,下载最新的
.uf2固件文件。 - 进入引导模式:用一条可靠的数据线连接开发板和电脑。快速双击板载的
RESET按钮。此时,RGB状态指示灯会先变绿,然后迅速变紫。关键操作来了:在指示灯变紫的瞬间,再次单击RESET按钮。成功后,电脑上会出现一个名为FTHRS3BOOT的U盘。 - 拖入固件:将下载的
.uf2文件拖入FTHRS3BOOT盘符。完成后,该盘符会消失,并出现一个新的CIRCUITPY盘符。至此,CircuitPython环境就绪。
实操心得:很多新手卡在双击复位进入引导模式这一步。失败的主要原因有两个:一是使用了只能充电不能传输数据的USB线,务必换用手机原装数据线或明确支持数据传输的线;二是双击节奏不对。如果第一次没成功,多试几次,掌握“第一次双击等绿灯,看见紫灯马上再点一下”的节奏。
3.2 关键配置文件:settings.toml的深入解析
从CircuitPython 8开始,推荐使用settings.toml替代旧的secrets.py来管理敏感信息。这是一个纯文本配置文件,放在CIRCUITPY根目录。
文件内容示例:
CIRCUITPY_WIFI_SSID = "你的Wi-Fi名称" CIRCUITPY_WIFI_PASSWORD = "你的Wi-Fi密码" ELGATO_LIGHT = "192.168.1.100" # 你的Elgato灯光IP地址为什么这么设计?
- 安全分离:将Wi-Fi密码、设备IP等敏感信息与主程序代码
code.py分离。你可以放心地分享代码,而不用担心泄露家庭网络凭证。 - 灵活管理:项目代码无需硬编码这些信息。当更换网络环境或控制不同的灯光时,只需修改
settings.toml,无需重新编辑和调试代码。 - 环境变量式访问:在
code.py中,通过os.getenv("ELGATO_LIGHT")即可获取IP地址,代码清晰且符合现代编程实践。
如何获取Elgato灯光的IP地址?这是项目成功的关键。打开手机上的Elgato Control Center App,进入灯光设备的设置界面,通常在网络或关于设备的信息中,可以找到它的本地IP地址。确保你的手机和灯光连接在**同一个Wi-Fi网络(同一个子网)**下。
4. 核心代码逻辑与通信原理拆解
4.1 网络连接与HTTP请求框架
控制器与灯光通信的基石是HTTP协议。Elgato灯光在本地网络中扮演了一个小型HTTP服务器的角色,监听9123端口。
连接初始化流程:
import wifi import socketpool import adafruit_requests import ssl # 1. 连接Wi-Fi wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD')) # 2. 创建Socket池和请求会话 pool = socketpool.SocketPool(wifi.radio) requests = adafruit_requests.Session(pool, ssl.create_default_context())这段代码首先利用settings.toml中的凭证连接Wi-Fi。成功后,创建一个socketpool用于管理网络连接,最后初始化一个requests会话对象。这个requests对象是对底层socket的友好封装,让我们能够像在桌面Python中一样使用requests.get()和requests.put()方法。
4.2 Elgato灯光API交互详解
Elgato的API设计非常简洁,主要使用两个端点:
- 读取状态:
GET http://<light_ip>:9123/elgato/lights - 设置状态:
PUT http://<light_ip>:9123/elgato/lights
控制灯光(ctrl_light函数)剖析:这个函数负责向灯光发送控制指令。它构建一个JSON数据包,通过PUT请求发送。
json = { "numberOfLights": 1, "lights": [{ "on": True, # 布尔值,开关状态 "brightness": 50, # 整数,范围0-100 "temperature": 200 # 整数,范围143-344 }] }关键点解析:
brightness:0到100的整数,代表亮度百分比。temperature:这是最容易混淆的参数。它不是开尔文温度值,而是Elgato内部的一个映射值,范围是143到344。代码中的kelvin_to_elgato()和elgato_to_kelvin()函数就是用来在这两者之间转换的。转换公式基于线性映射:elgato_value = kelvin * 0.05。所以,2900K对应145,7000K对应350,但API限定了143-344的实际有效范围。
健壮性设计:代码中包含了一个重试循环(for i in range(5):)。网络通信可能不稳定,如果一次请求失败(状态码非200),它会等待2秒后重试,最多5次。同时,通过板载NeoPixel的颜色变化(绿->黄->红)和屏幕状态文本(“sending..” -> “..sending..”),给用户提供了清晰的视觉反馈。
4.3 用户界面(UI)与状态显示
TFT屏幕的UI布局虽简单,但信息密度高:
- 顶部:显示Elgato灯光的IP地址,用于连接确认。
- 左侧中部:状态区域,显示“Connected”、“reading..”、“sent!”等实时操作反馈。
- 右侧中部:大字体显示当前色温值(单位K)。
- 底部右侧:大字体显示当前亮度值(单位%)。
- 左上角:一个圆形指示灯,填充黄色代表灯光开启,空心代表关闭。
使用displayio和bitmap_label来管理这些显示元素是CircuitPython的标准做法。它采用“组(Group)”的概念,将所有图形元素(文本、形状)添加到同一个组中,然后一次性刷新到屏幕,效率很高。
4.4 主循环与输入处理逻辑
主循环 (while True) 是控制器的大脑,它需要不间断地做四件事:
- 扫描旋转编码器:检查旋钮位置是否变化,根据
adjust_temp标志位决定是调整色温(步进100K)还是亮度(步进10%)。这里用了last_position变量来记录上一次的位置,通过比较差值判断是顺时针还是逆时针旋转。 - 检测编码器按键:按下编码器中间的开关,用于切换
adjust_temp标志,从而改变旋钮控制的对象(色温/亮度)。 - 检测三个独立按键:分别触发“开关/发送”、“仅发送”、“读取”功能。代码中使用了
button_state变量进行边沿检测,确保一次按下只触发一次动作,防止按键抖动导致重复触发。 - 更新状态显示:使用
ticks_ms()函数实现非阻塞延时。在发送或读取操作后,状态文本会更新为“sent!”或“read!”,并在3秒后自动恢复为“Connected”,保持界面清爽。
这种事件驱动的循环结构,确保了界面响应及时,同时不会因为某个操作(如网络请求)而卡死整个系统。
5. 硬件组装与焊接要点
5.1 分步组装指南
- 连接旋转编码器:使用一根4针STEMMA QT/Qwiic连接线,将旋转编码器模块与Feather开发板的任意一个I2C端口(STEMMA QT接口)连接。这种防反插接口极大简化了连接,无需焊接。
- 安装Feather到上盖:使用提供的M2.5螺丝和螺母,将Feather主板固定到3D打印外壳的上盖(Lid)内部。注意对齐螺丝孔,避免过度拧紧导致塑料件开裂。
- 固定旋转编码器到底座:将4个M2.5的尼龙螺柱用螺母固定在编码器模块的四个角上。然后,从外壳底座(Case)外部,将编码器模块放入对应位置,从内部用M2.5螺丝锁紧螺柱。确保编码器的旋钮轴和NeoPixel灯珠正对底座的开口。
- 合盖与最终装配:将上盖与底座对齐,轻轻按压使其卡扣结合。最后,将旋钮帽按在编码器的轴上,并将透明的NeoPixel光扩散片压入底座的对应小孔。
5.2 焊接与连接注意事项
- 电池连接:如果你计划使用电池供电,建议焊接一个JST-PH延长线到Feather的电池接口。这样,电池可以方便地放入外壳内,并通过延长线连接,便于日后更换电池。
- 引脚检查:确保Feather上的D0、D1、D2按键没有被任何飞线或元件短路。这些按键是板载的,直接使用即可。
- I2C地址冲突:本项目使用的旋转编码器模块I2C地址默认为
0x36。如果你未来想添加其他I2C设备,需要注意地址不能冲突。
6. 使用操作与高级调试技巧
6.1 基本操作流程
- 上电与连接:用USB线或电池为控制器供电。首次启动时,它会尝试连接
settings.toml中配置的Wi-Fi,并读取指定IP地址的Elgato灯光状态。成功连接后,屏幕会显示灯光IP和“Connected”,并显示当前的亮度、色温和开关状态。 - 调节与发送:
- 旋转旋钮:调整屏幕上显示的亮度或色温值(通过按下旋钮切换调节对象)。
- 按下D0键:将当前屏幕显示的值发送给灯光,并切换灯光开关状态。例如,灯原来是关的,按D0会开灯并应用新值。
- 按下D1键:将当前屏幕显示的值发送给灯光,但不改变灯光当前的开关状态。例如,灯是开着的,你调低了亮度,按D1后灯保持开启,但亮度变低。
- 状态同步:按下D2键,控制器会向灯光发起查询,并用灯光返回的实际状态更新屏幕显示。这在用手机App调整灯光后,让控制器同步状态非常有用。
6.2 串口调试与故障排查
当项目不按预期工作时,串口调试是定位问题的利器。用数据线连接控制器和电脑,使用串口终端工具(如Mu编辑器、Thonny或screen/putty)打开对应的串口(如COMx, /dev/ttyACM0)。
常见问题速查表:
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 屏幕无显示,RGB灯不亮 | 供电问题或固件未刷入 | 1. 检查USB线是否数据线。 2. 重新执行CircuitPython固件刷写流程。 3. 检查 CIRCUITPY盘符是否存在。 |
| 屏幕显示但卡在“Connecting to WiFi” | Wi-Fi连接失败 | 1. 打开串口监视器,查看具体错误信息。 2. 检查 settings.toml中的SSID和PASSWORD是否正确,注意大小写和特殊字符。3. 确认路由器是否设置了MAC地址过滤等限制。 |
| 显示“Could not find your Elgato light” | 无法与灯光通信 | 1.最重要:确认Elgato灯光已开机并接入同一局域网。 2. 检查 settings.toml中的ELGATO_LIGHTIP地址是否正确。3. 在电脑上 ping一下灯光的IP地址,看是否通。4. 检查路由器防火墙或客户端隔离设置。 |
| 按键或旋钮无反应 | 硬件连接或代码问题 | 1. 检查旋转编码器的STEMMA QT线是否插紧。 2. 在串口监视器中旋转或按下按键,看是否有调试信息输出(代码中 print语句)。3. 确认使用的代码库版本与教程一致。 |
| 控制指令发送成功但灯光无变化 | API参数或灯光状态问题 | 1. 在串口监视器中查看发送的JSON数据是否正确。 2. 确认灯光是否处于可被网络控制的状态(某些模式可能禁用API)。 3. 尝试用D2键读取状态,看返回的数据是否正常。 |
高级调试技巧:你可以在代码中的关键位置(如网络请求前后、函数入口)添加print()语句,打印变量值或状态标志。例如,在ctrl_light函数里打印出构建的URL和JSON,能最直观地确认发送的数据是否正确。
7. 项目扩展与优化思路
这个项目提供了一个坚实的基础框架,你可以根据自己的需求进行扩展:
- 控制多盏灯:代码中
num_lights变量目前为1。Elgato API支持在同一个JSON请求中控制多盏灯(理论上)。你可以尝试修改数据结构,并设计UI来切换当前控制的是哪一盏灯(例如,用编码器切换设备ID)。 - 保存偏好设置:利用ESP32-S3的少量非易失性存储(NVS)或创建一个简单的文本配置文件,保存几组常用的亮度/色温组合(如“直播模式”、“阅读模式”、“休息模式”),并通过长按某个按键快速调用。
- 增加传感器反馈:接入一个环境光传感器,让灯光能根据环境光照自动调整亮度,实现简单的自适应照明。
- 美化外壳与UI:使用更高级的图形库,设计更炫酷的UI动画。或者用激光切割亚克力制作外壳,获得不同的质感。
- 集成到智能家居平台:虽然Elgato灯光本身不直接支持Home Assistant等平台,但你可以将ESP32-S3控制器作为桥梁,让它同时连接家庭Wi-Fi和Elgato灯光,并暴露一个MQTT或REST API接口给Home Assistant,从而实现更广泛的智能联动。
这个项目的魅力在于,它用一个具体的例子,串起了CircuitPython编程、嵌入式硬件交互、网络通信协议(HTTP/JSON)和产品原型设计等多个知识点。完成它,你收获的不仅仅是一个好用的桌面工具,更是一套可复用的物联网设备开发方法论。