简介:LightControl是一个基于Java开发的照明控制系统项目,面向智能家居、物联网或嵌入式控制方向的中初级开发者,可用于学习Java网络通信、设备控制与用户界面设计。资源共35个文件,压缩包仅65KB,结构紧凑;其中27个Java源文件构成核心业务逻辑,properties与xml为配置和构建支持,jar为依赖库,sql为数据库脚本,md为项目说明。目前已有53人学习,适合作为Java实战入门案例。通过压缩包可获取完整源码、Maven构建配置(pom.xml)、资源文件及Git忽略规则,便于直接导入IDE运行调试;结合README与SQL脚本,可快速理解设备发现、灯光控制、定时任务、场景预设等模块的实现思路,对希望掌握Java桌面或后端开发流程的读者很有参考价值。 晚上十点半,我坐在沙发上看着客厅主灯明晃晃地亮着,想换成暖光弱一点,又不想起身去够开关。这个念头反复出现了很多次,终于有一天我决定不再忍了,动手做了一套局域网内的智能灯光控制系统,起名就叫 LightControl。它的目标很明确:不依赖任何云平台,不买一堆专用网关,把家里已有的灯接进来,通过浏览器、手机或者自动定时都能控制。这套系统我跑了一个多月,把中间踩的坑和最终的方案完整地记录下来,想自己动手做智能家居或者不想被某一家生态绑定的朋友,应该能从里面找到不少能直接用的经验。
1. 为什么自己动手做一套 LightControl,而不是买现成的智能灯
1.1 我对市售方案越来越不满意的三个原因
市面上能买到的智能灯泡、智能开关确实不少,我也装过一些。但用了一段时间之后,有三点让我觉得不太舒服。
首先是平台绑定问题。买 A 家的灯泡,就得装 A 家的 App,后续想加 B 家的传感器,又得看它能不能接入同一个系统。折腾一圈下来,手机里装了好几个控制软件,明明只是开关灯,却要在一堆 App 之间来回切换。这种“生态围墙”短期看是方便,长期看就是给自己挖坑。
其次是本地可用性问题。绝大多数成品方案,控制指令都要先上云,再从云端下发到设备。一旦家里的宽带出故障,或者服务商的服务器不稳定,你甚至连开灯关灯这种基础操作都做不了。我个人的底线是:灯光这种东西,应该是本地优先,哪怕整个外网都断了,家里控制页面照样能打开,这才叫可靠。
最后是灵活性不足。成品的智能灯泡,亮度、色温、场景模式看起来很多,真到用的时候总感觉隔着一层。我想实现“按下墙上开关关灯,但保留遥控器还能再开”这种逻辑,市售产品很难配合。与其去适应它们的设计,不如按自己的需求从零搭一套,这就是 LightControl 出现的原因。
1.2 决定自己做之前,先给边界画一条线
一开始我差点把这件事想得过于宏大:想做 App、做语音助手、做自动化场景,还要做能耗统计。还好我在动手前及时冷静下来,给这个项目画了几条边界。
第一版 LightControl 只做三件事:局域网内 Web 控制、两路 PWM 调光、三路继电器开关。定时功能放在二期,语音接入放到三期,App 干脆不做,因为手机浏览器配合 PWA 加到桌面,体验已经和原生 App 差不多。这样边界清晰以后,整个项目的复杂度立刻降下来。
还有一个很重要的决定:所有控制逻辑全部跑在 ESP32 上,不依赖家里的电脑长期开机。这意味着我需要一个能独立运行、功耗低、价格便宜的控制核心,后面我选了 ESP32,理由后面细说。对大多数想复刻这套系统的朋友,我建议也先想清楚自己的“最小可用版本”是什么,先让它稳定跑起来,再考虑加东西,否则很容易烂尾。
2. 硬件怎么搭:ESP32 主控、继电器、PWM 调光的完整链路
2.1 主控选型:ESP32 是“刚刚好”的选择
控制核心我最终选了 ESP32 DevKitC 开发板,而不是树莓派。核心原因是价格、功耗和性价比的平衡。树莓派做这种控制器当然可以,还能跑完整的 Home Assistant,但一是贵,二是要给它配 TF 卡、供电、散热,整机功耗比 ESP32 高一个数量级。而 ESP32 自带 Wi-Fi、蓝牙、双核处理器和丰富的外设,市价只要二三十块钱,跑一个灯光控制服务器绰绰有余。
更重要的是 ESP32 的 PWM 输出能力。它内部的 LEDC 模块可以输出最高几十 kHz 的 PWM 信号,频率、分辨率都程序可调,这给了我做调光很大的余地。后面讲频闪问题时会提到,如果当初选了普通 Arduino Uno,想做到同样的效果就得额外加芯片,会费不少事。
2.2 调光与开关的分工:不是所有灯都需要调光
我家里三路灯光,需求各不相同。客厅主灯是普通 LED 吸顶灯,只需要开关,不需要调光,所以我用一路继电器控制火线通断。书桌灯带是 12V 的 LED 灯带,需要无级亮暗调节,我给它单独配了一路 MOSFET 做 PWM 调光。卧室床头灯也是 LED,但要满足夜间弱光需求,所以同样走 PWM。
这里想提醒一下:不要试图让继电器去做调光的事。继电器本质是机械开关,频繁通断不仅会产生噪音、缩短寿命,而且因为通断响应速度跟不上人眼感知,灯光会出现肉眼可见的闪烁,体验非常差。调光这件事,交给 MOSFET 或者可控硅来做,才是正路。
两种驱动方式的分工整理如下:
| 灯光类型 | 控制方式 | 关键器件 | 适用场景 |
|---|---|---|---|
| LED 吸顶灯、白炽灯 | 开关 | 继电器模块 | 只做通断,不需要亮度变化 |
| 12V/24V LED 灯带 | 调光 | MOSFET(PWM) | 需要无级亮暗、场景光 |
| 墙壁开关替代 | 开关 | 继电器模块 | 保留物理开关的位置和手感 |
| 交流调光 | 调光 | 可控硅/后沿调光模块 | 需要交流灯具亮度调节,但对 EMI 要求高 |
2.3 接线细节:继电器怎么驱动、MOSFET 怎么选
接线是这套系统里最容易出错的地方,尤其是 ESP32 的 GPIO 引脚和强电部分混在一起时,稍不注意就是烧板子甚至更严重的问题。
我用的继电器模块是低电平触发的光耦隔离型,控制端本身带三极管放大电路,所以 ESP32 的 GPIO 可以直接接。但如果你用的是裸继电器模块,一定要在 GPIO 和继电器之间加一个 NPN 三极管(比如 S8050)做驱动,否则 GPIO 的电流根本拉不动继电器线圈。模块上通常有 JD-VCC 跳线,用来把继电器线圈的电源和逻辑电源隔离开,建议按照模块说明把跳线设成隔离模式,避免继电器吸合瞬间产生的反冲电压影响到 ESP32。
PWM 调光部分,我选了 AO3400 这颗 N-MOSFET,导通电阻低、逻辑电平驱动,3.3V 的 GPIO 可以直接开启,对 12V 灯带这种负载来说完全够用。栅极要加一个 10kΩ 下拉电阻到地,防止 ESP32 启动过程中 GPIO 处于高阻状态时,MOSFET 意外导通、灯带突然亮起来。电源部分我用了一个 220V 转 5V 的隔离式开关电源,给 ESP32 和继电器模块供电;灯带本身用单独的 12V 电源,两个电源的 GND 根据需要共地或不共地。第一次通电前,我强烈建议先用万用表量一遍各点电压,确认没有短路再接灯具,这一步能帮你省下大把排查问题的时间。
3. 软件设计:一套轻量协议,让网页、定时和状态同步各司其职
3.1 通信方式:WebSocket 与 HTTP 分开用
控制页面的通信协议,我最初想全部用 HTTP,后来发现不行。HTTP 是请求-响应模式,适合“客户端主动问、服务端被动答”的操作,但做实时状态同步非常别扭:网页要不停地轮询,延迟高、开销大。所以我最终采用了混合方案:控制指令用 HTTP,状态推送用 WebSocket。
ESP32 上我跑了两个服务,一个 HTTP 服务,一个 WebSocket 服务。网页打开后先通过 HTTP 请求/api/state拉取当前所有灯的状态,然后建立 WebSocket 连接。之后网页上每按一次开关,就发一条 POST 请求到/api/control,带上通道编号和目标值;ESP32 收到后执行控制动作,再通过 WebSocket 向所有在线客户端广播最新状态。这样只要有一个客户端在控制,其他打开的页面都能马上同步,不用刷新。
消息格式我用的是 JSON,简单直观。控制指令长这样:
{ "channel": "desk", "action": "set_brightness", "value": 180 }状态广播长这样:
{ "channel": "desk", "state": "on", "brightness": 180, "timestamp": 1720000000 }这套协议不复杂,但对灯光控制这种简单场景来说完全够用,而且调试非常方便。
3.2 状态机设计:开机恢复、多端同步、故障回滚
软件层面最重要的不是那些 API,而是状态管理。我开始写的版本非常简陋,每次控制只管改 GPIO 输出,不保存任何状态,结果路由器一重启,所有灯恢复成默认值,家里人就跑来问“为什么书桌灯突然亮了”。这就是吃了没做状态持久化的亏。
后来我重新设计了状态机:每次控制动作执行成功后,把当前状态写入 ESP32 的 NVS 闪存;系统启动时,先读取 NVS 里的记录,恢复各个通道到上次关机前的状态,然后再启动 Web 服务。这里有一个细节,恢复的顺序要讲究:继电器通道应该先设置成关闭状态,再延迟几百毫秒设置成开通状态,避免上电瞬间所有 GPIO 处于不确定电平,导致继电器误动作。
多端同步的“故障回滚”也值得一提。我遇到过一种情况:网页上发出调节亮度指令,但执行过程中灯带负载异常,驱动器没有按预期响应。如果服务端直接广播“亮度已经调到 200”,其他页面就会显示一个实际上没发生的结果。所以我在指令和状态之间做了区分:指令是用户想要的结果,状态是硬件实际返回的结果。ESP32 执行完控制后,通过 ADC 采样确认灯带电流是否确实变化,如果没变化,就回滚状态并向客户端返回错误码。这个逻辑在普通灯光项目里很多人会忽略,但对可靠性要求高的场景特别有用。
3.3 定时任务与离线时钟:不依赖云也能准时
定时开关灯是智能灯最基本的需求,但实现起来有个容易忽略的问题:单片机怎么知道现在是几点?
最省事的方式是联网用 NTP 协议同步时间,但这样依赖外网。我的想法是,灯光系统本就应该离线可用,所以我给 ESP32 加了一个 DS3231 RTC 模块,做本地时间基准。每次开机时如果网络可用,就通过 NTP 校准一次;如果网络不可用,就继续用 RTC 的计时而并非做不到。这样定时任务只依赖本地时钟,不受云端状态影响。
定时任务的调度,我用简单的 FreeRTOS 软件定时器就够了,每秒钟检查一次当前时间,并和周计划表做匹配。比如工作日晚上六点半自动打开书桌灯,亮度设为 60%;周末早上九点自动关掉所有灯。计划表存在 NVS 里,通过 HTTP 接口可以随时修改。这套设计跑了一个月,没有出现时间偏移导致错乱的问题,DS3231 的温补优势很明显。
4. 实测踩坑记录:PWM 频闪、Wi-Fi 掉线、继电器粘连的修复过程
4.1 调光灯带在手机摄像头下频闪:PWM 频率需要匹配驱动器
第一版调光代码写得很简单,初始化 LEDC 时直接用默认的 500Hz 频率,然后调到任意亮度。肉眼看好像没问题,但拿手机摄像头对着灯带录像,画面里全是波浪纹,这说明 PWM 频率太低,和传感器快门产生了明显的差拍。
这个问题背后的原因是 LED 灯带本身没有高频滤波能力,它的驱动方式就是直接通断,所以 PWM 频率越低,电流纹波越明显。解决办法是提高 PWM 频率,但也不能无限高,频率太高之后 MOSFET 的开关损耗会增加,发热变严重。我在灯带上实测了几个频率:2kHz 时摄像头可见轻微条纹,5kHz 时基本消失,10kHz 时完全干净但 MOSFET 温升比 5kHz 高约 3℃。最终我把默认频率定在 5kHz,亮度调节分辨率保持 10 位(0~1023)。如果你用的是恒流驱动电源来带灯,还需要结合驱动器说明书,有些驱动器要求 PWM 频率不低于 1kHz,否则会出现吱吱声。
4.2 Wi-Fi 频繁掉线:罪魁祸首是 ESP32 的省电模式
系统跑了一周后,我遇到一个很烦的问题:控制页面经常打不开,重启 ESP32 后恢复,过一两天又死。一开始怀疑是代码内存泄漏,后来排查日志发现,设备掉线的根本原因是 Wi-Fi 连接丢失后重连逻辑写得太粗暴。
ESP32 默认开启了 modem sleep 省电模式,这个模式下 Wi-Fi 模块会在空闲时进入低功耗状态,对电池供电的设备是好消息,但对我这种需要随时响应外部请求的设备来说就是灾难:只要网页和 ESP32 之间的通信间隔稍长,Wi-Fi 模块就可能睡过头,醒来后路由器已经把它踢掉了。
修复方法很直接:在初始化 Wi-Fi 时调用esp_wifi_set_ps(WIFI_PS_NONE)关闭省电模式。另外还要设置静态 IP 或 DHCP 租约足够长,避免 IP 地址频繁变化。关闭省电之后,我连续观察了二十多天,没有再出现掉线无法唤醒的情况。如果你是给插电设备做控制类项目,建议从第一天就关掉省电模式,别像我一样等到线上故障才去排查。
4.3 开关与调光同时动作时,继电器为什么会粘连
有一次我在测试联动场景:晚上打开书桌灯时,先让继电器吸合给灯带供电,紧接着调整 PWM 亮度。结果继电器触点出现粘连,灯带不受控制地一直亮着,直到我断了总闸才停下来。
拆开继电器检查,触点表面有明显的拉弧痕迹。原因其实很简单:继电器带载吸合或断开时,瞬间电流会造成触点拉弧,次数多了就会烧蚀粘连。我的 PWM 调光用 MOSFET 控制的是灯带负极,继电器控制的是正极电源,那问题就变成:开灯瞬间,先通电源再调光,还是先调光再通电源?关灯瞬间,先断电源再调暗,还是先调暗再断电源?
正确的动作顺序应该是:开灯时先把 PWM 占空比拉到最大(也就是灯带完全导通状态),此时 MOSFET 的压降最小,然后再让继电器吸合,吸合完成后延时 50ms 再把 PWM 调到目标亮度。关灯时反过来,先把 PWM 占空比降到 0,等 50ms 让负载电流衰减到接近零,再断开继电器。这样继电器触点承受的电流非常小,拉弧概率大幅下降。这个时序是用 FreeRTOS 的延时函数实现的,代码逻辑不复杂,但非常关键。
4.4 强电弱电互相干扰,导致随机误触发的布线教训
最后一个坑是环境问题。系统装好后,偶尔会出现灯光自己跳变的情况,比如书桌灯突然从 50% 亮度跳到 100%,或者卧室灯莫名其妙开了一下又关掉。一开始我怀疑是代码逻辑 bug,但看了半天日志发现,这些误触发都发生在附近有大功率电器启停的时间点,比如冰箱压缩机启动、空调变频器动作。
问题出在布线上。最初为了好看,我把 220V 交流电线和 ESP32 的 GPIO 信号线一起穿进同一根线槽,两者距离非常近。220V 线路上的高 dv/dt 会通过寄生电容耦合到信号线上,一旦干扰幅度超过 GPIO 的阈值,就会产生一个假的触发信号。后来我把强电线和弱电线完全分两路走,间隔至少 10cm,并且给 MOSFET 栅极信号线加了 100Ω 串联电阻和 1nF 对地电容,构成低通滤波,干扰问题基本消失。如果你要在配电箱或者狭小空间里做这类项目,这个教训尤其值得借鉴:强电、弱电之间要有物理距离,共地要谨慎,信号线要做滤波,否则排查起来非常费时。
5. 跑了一个月的真实体验,以及后续还能怎么扩展
5.1 三路灯光实际运行情况:稳定但不完美
LightControl 在我家稳定运行大概四十天,整体表现算可靠。最常用的场景是客厅主灯定时关闭,书桌灯带从 100% 亮度随日落渐渐降到 30%,卧室床头灯保留手动调节。家人一开始觉得累赘,后来习惯了“手机浏览器点一下就能化调整”的方式,反而觉得比墙上的开关方便多了。
它不完美的地方也有:一个是 ESP32 的 Web 页面响应速度,在局域网内通常在 100~300ms 之间,虽然体感流畅,但和原生 App 的秒开还是有差距;另一个是拔掉电源再插上后,Wi-Fi 重连需要几秒钟,期间控制页面无响应,属于妥协接受的程度。另外,我的灯具本身不是调光型电源,用 PWM 直接斩波会产生轻微的电流声,夜深人静时能听到,这个后来我把 PWM 频率调成 8kHz 之后基本听不到了,但代价是 MOS 管温度略升,算是此消彼长吧。
5.2 下一步扩展:雷达传感器联动与语音接入
这套系统稳定后,我给它留了扩展接口。一个是把 HLK-LD2410 人体雷达接到 ESP32 的 UART 上,这样人在书房时自动把书桌灯调到 70%,人离开超过五分钟降到 20%,再超过十分钟直接关灯。另一个是接入语音,我不打算用带麦克风的专用设备,而是准备利用家里现有的智能音箱,通过局域网 HTTP 请求直接触发 LightControl 的接口。这两个扩展都不改动核心架构,只要在现有 HTTP 服务上增加几个 endpoint 就行。
5.3 给想复刻的人的三条实操建议
第一,安全永远是第一位。这个项目涉及 220V 交流电,如果你没有电工基础,建议先在 12V 灯带上把逻辑全部调通,再考虑接线到墙面的交流灯具。不要为了省钱省略隔离电源和继电器模块上的光耦隔离。
第二,先写好状态持久化和启动恢复逻辑,再去做 Web 界面。很多新手喜欢先把页面做得花里胡哨,结果基础状态管理一塌糊涂,每次重启设备灯的状态全部乱掉,这是最容易劝退人的一个点。稳定的系统,核心不是功能多,而是状态对。
第三,留出调试接口。我在代码里加了一个串口调试模式,可以实时打印每个通道的当前状态和最后一次控制指令来源。这个调试模式在正式使用中几乎没开过,但它的存在让我排查问题时心里有底。做嵌入式控制类项目,永远别把后门堵死,给自己留一条排查路径,能省下大把时间。
本文还有配套的精品资源,点击获取