需求洞察
你有没有过这种体验:深夜加班盯着屏幕,桌面的灯光突然变得刺眼或太暗?或者开会时桌灯亮着,主持人一直以为你没在认真听?又或者离开工位去倒水,回来发现 PC 已经进入休眠,桌面一团黑?
桌面灯光其实是空间情绪的延伸。但市面上的智能灯具普遍不便宜:Yeelight 灵动灯带 ¥169,小米智能台灯 ¥199,Nanoleaf 奇光板更离谱到 ¥700+。对于一个"就想让桌面灯光随状态变一变"的极客来说,性价比不够。
本项目的目标就是用 ¥88 的 BOM 物料,做出一个能根据环境光、时间、PC 状态自动切换灯效的氛围灯——而且支持二次开发(颜色、模式、触发逻辑都可在 Arduino 代码里改)。
核心痛点
- 久坐办公灯光单一:长时间单一色温容易视觉疲劳和情绪低落
- 不同状态难表达:专注 / 休息 / 会议 / 充电 4 种状态,颜色是最直观的表达
- 智能灯具贵:Yeelight / 小米 / Nanoleaf 普遍 100+,且不易二次开发
- PC 休眠无感知:离开工位倒水,回来发现 PC 已休眠,桌面一片黑
方案设计
本项目采用「感知 + 主控 + 显示 + 反馈」四件套架构,以 ESP32 为中心,外围 3 个模块各司其职:
| 模块 | 型号 | 功能 |
|---|---|---|
| 主控 | ESP32 DevKit V1 | WiFi/BT 双模,240MHz,34 个 GPIO |
| 灯环 | WS2812B 12 颗环形 | 单线 1670 万色,单 GPIO 13 控制全部 |
| 光感 | BH1750FVI | I2C 接口,0-65535 lux 精度,16bit |
| 显示 | SSD1306 OLED 0.96" | 128x64,I2C 接口,实时显示模式/亮度/光照 |
4 种模式
| 模式 | 灯效 | 触发条件 |
|---|---|---|
| 专注 FOCUS | 冷白渐变(HSV 色相 160°,饱和 50%,亮度 200) | 亮度 > 50 lux 且时间 6:00-22:00 |
| 休息 REST | 暖橙渐变(HSV 色相 30°,饱和 200%,亮度 100) | 亮度 < 50 lux 或 22:00-06:00 |
| 会议 MEETING | 全关(FastLED.clear) | 串口收到字符 'm' 或 按钮按下 |
| 充电 CHARGING | 绿色呼吸(HSV 色相 120°,beatsin8 呼吸 15 次/分钟) | USB 电流 < 100mA(PC 休眠) |
模式优先级:会议 > 充电 > 休息 > 专注。即用户主动触发会议后会一直保持,直到串口收到 'r'(reset)才退出。
技术亮点
- 单线协议:WS2812 用单 GPIO 控制 12 颗 LED,省引脚
- I2C 总线复用:BH1750 + OLED 共用 GPIO 21/22 两条线
- 低功耗:ESP32 深度睡眠模式下,整机功耗 < 10mA
- 二次开发:所有模式/颜色/触发逻辑都可在 Arduino 代码里改
物料清单
总成本 ¥88,比 Yeelight 灵动灯带便宜 47%。所有物料在淘宝/嘉立创都能买到。
| 数量 | 物料 | 单价 | 小计 |
|---|---|---|---|
| 1 | ESP32 DevKit V1(Type-C 口) | ¥30 | ¥30 |
| 1 | WS2812B 12 颗环形灯(外径 50mm) | ¥15 | ¥15 |
| 1 | BH1750FVI 光照传感器模块 | ¥6 | ¥6 |
| 1 | SSD1306 OLED 0.96 寸显示屏(I2C 接口) | ¥12 | ¥12 |
| 1 包 | 杜邦线(公-公 40 根 + 公-母 40 根) | ¥5 | ¥5 |
| 1 | 面包板 400 点(SYB-170) | ¥5 | ¥5 |
| 1 | 5V 3A 电源适配器(DC 5.5x2.1 母头) | ¥15 | ¥15 |
| 总计 | ¥88 | ||
采购建议:ESP32 建议买带 Type-C 口的 V1 改进版(丝印清楚);WS2812 选 12 颗环形(外径 50mm),不要选 16 颗或 24 颗(电流会超过 3A);OLED 选 0.96 寸 4 针脚版本(VCC/GND/SCL/SDA),不要买 7 针 SPI 版本(接线复杂)。
接线与结构
按 ESP32 DevKit V1 的丝印实际位置说明(不是抽象 GPIO 编号):
ESP32 排针位置参考
- 左侧排针(从下到上):VIN / GND / D13 / D12 / D14 / D27 / D26 / D25 / D33 / D32 / D35 / D34 / VN / IO / VP / EN
- 右侧排针(从下到上):3V3 / GND / D15 / D2 / D4 / RX2 / TX2 / D5 / D18 / D19 / D21 / RX0 / TX0 / D22 / D23
接线清单(带颜色 + 丝印)
| 模块 | 引脚 | 颜色 | 连接目标 | 丝印 |
|---|---|---|---|---|
| WS2812 环形 | DIN(数据输入) | 白线 | ESP32 GPIO 13 | 左侧排针第 3 脚,标 D13 |
| WS2812 环形 | 5V(电源) | 红线 | 外部 5V 电源正极 | DC 5.5x2.1 母头内芯 |
| WS2812 环形 | GND(地) | 黑线 | ESP32 GND + 外部 5V 电源负极 | ESP32 左侧排针第 2 脚;务必共地! |
| BH1750 | VCC | 红线 | ESP32 3.3V | 右侧排针第 1 脚,标 3V3 |
| BH1750 | GND | 黑线 | ESP32 GND | 右侧排针第 2 脚,标 GND |
| BH1750 | SDA | 黄线 | ESP32 GPIO 21 | 右侧排针第 11 脚,标 D21 |
| BH1750 | SCL | 白线 | ESP32 GPIO 22 | 右侧排针第 14 脚,标 D22 |
| OLED 0.96" | VCC | 红线 | ESP32 3.3V | 右侧排针第 1 脚,标 3V3 |
| OLED 0.96" | GND | 黑线 | ESP32 GND | 右侧排针第 2 脚 |
| OLED 0.96" | SDA | 黄线 | ESP32 GPIO 21 | 与 BH1750 共用 I2C 总线 |
| OLED 0.96" | SCL | 白线 | ESP32 GPIO 22 | 与 BH1750 共用 I2C 总线 |
⚠️ 关键提醒:
- WS2812 必须用外部 5V 电源,不要用 ESP32 的 VIN 或 3.3V(12 颗 LED 全亮峰值电流约 600mA,会烧 ESP32 稳压器)
- 务必共地:WS2812 的 GND 必须和 ESP32 的 GND 接在一起,否则 LED 会乱闪(这是最常见的故障)
- DIN 加 470Ω 电阻(可选):在 WS2812 DIN 和 ESP32 D13 之间串联一个 470Ω 电阻,可减少信号反射导致的乱码
- 5V 电源加 1000μF 电容(可选):在 5V 电源正负极之间并联一个电解电容,避免上电瞬间电流冲击
核心代码
使用 Arduino 框架,需要安装 3 个库:FastLED、BH1750、Adafruit_SSD1306。
#include <FastLED.h>
#include <Wire.h>
#include <BH1750.h>
#include <Adafruit_SSD1306.h>
#define LED_PIN 13
#define NUM_LEDS 12
#define BRIGHTNESS 80
#define OLED_ADDR 0x3C
CRGB leds[NUM_LEDS];
BH1750 lightMeter(0x23);
Adafruit_SSD1306 display(128, 64, &Wire, -1);
enum Mode { FOCUS, REST, MEETING, CHARGING };
Mode currentMode = FOCUS;
void setup() {
Serial.begin(115200);
// WS2812 初始化
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
FastLED.clear();
// BH1750 初始化
lightMeter.begin();
// OLED 初始化
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
display.display();
delay(1500);
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// 开机自检:彩虹渐变
for (int hue = 0; hue < 255; hue += 8) {
fill_rainbow(leds, NUM_LEDS, hue, 8);
FastLED.show();
delay(20);
}
FastLED.clear();
}
void loop() {
float lux = lightMeter.readLightLevel();
int hour = hour(); // NTP 同步后可用
// 模式自动切换
if (Serial.available() > 0) {
char cmd = Serial.read();
if (cmd == 'm') currentMode = MEETING;
else if (cmd == 'r') currentMode = FOCUS;
}
if (currentMode != MEETING) {
if (lux < 50) currentMode = REST;
else if (hour >= 22 || hour < 6) currentMode = REST;
else currentMode = FOCUS;
}
applyMode();
displayStatus(lux);
FastLED.show();
delay(1000);
}
void applyMode() {
switch (currentMode) {
case FOCUS:
// 冷白渐变
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = CHSV(160, 50, 200);
}
break;
case REST:
// 暖橙渐变
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = CHSV(30, 200, 100);
}
break;
case MEETING:
// 静默全关
FastLED.clear();
break;
case CHARGING:
// 绿色呼吸
uint8_t bri = beatsin8(15, 30, 200);
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = CHSV(120, 255, bri);
}
break;
}
}
void displayStatus(float lux) {
display.clearDisplay();
display.setCursor(0, 0);
display.print(F("Mode: "));
switch (currentMode) {
case FOCUS: display.println(F("FOCUS")); break;
case REST: display.println(F("REST")); break;
case MEETING: display.println(F("MEETING")); break;
case CHARGING: display.println(F("CHARGING")); break;
}
display.setCursor(0, 20);
display.print(F("Lux: "));
display.println(lux, 0);
display.setCursor(0, 40);
display.print(F("Bri: "));
display.println(BRIGHTNESS);
display.display();
}
代码说明:
- 第 26 行
BH1750 lightMeter(0x23):默认 I2C 地址 0x23,有的模块是 0x5C,需改 - 第 30 行
OLED_ADDR 0x3C:OLED 默认地址 0x3C(也可能是 0x3D) - 第 53 行
Serial.read():串口监视器发送 'm' 切会议,'r' 切回专注 - 第 56-60 行:模式判断逻辑(夜间 + 暗光 → 休息)
- 第 87 行
beatsin8(15, 30, 200):FastLED 内置的"心跳"函数,每 4 秒一个呼吸周期
调试步骤
按以下顺序逐步调试,每步验证通过再进下一步(避免一次接 5 根线发现全错)。
第 1 步:WS2812 自检(5 分钟)
- 接线:仅接 WS2812 的 5V、GND、DIN 三根线(电源用外部 5V,ESP32 仅接 DIN)
- 烧录下面这段最小代码(FastLED 库自带的
Blink示例) - 看到 12 颗灯全亮红色 → 接线正确
- 看不到 → 检查:电源正负极是否接反、DIN 是否接对引脚、GND 是否共地
第 2 步:BH1750 自检(3 分钟)
- 接线:BH1750 的 4 根线(VCC/GND/SDA/SCL)全接
- 烧录下面这段代码(I2C Scanner)
- 串口监视器看到
I2C device found at address 0x23→ 接线正确 - 看不到 → 检查 I2C 地址(可能是 0x5C)、SDA/SCL 是否接反
第 3 步:OLED 自检(3 分钟)
- 接线:OLED 的 4 根线全接(与 BH1750 共用 I2C 总线)
- 烧录
Adafruit_SSD1306库自带的ssd1306_128x64_i2c示例 - 看到 OLED 显示一个跳动的方块 → 接线正确
- 看不到 → 检查 I2C 地址(用 Scanner 扫,0x3C 或 0x3D)
第 4 步:整合测试(10 分钟)
- 烧录上面的核心代码
- 打开串口监视器(波特率 115200)
- 应该看到 OLED 显示
Mode: FOCUS、Lux: xxx、Bri: 80 - 用手机手电筒照 BH1750,Lux 数字应明显上升
- 用手遮住 BH1750,Lux 数字应降到 0-10,且模式应自动切到 REST
- 串口发送
m,灯应全关,OLED 显示Mode: MEETING - 串口发送
r,灯恢复,OLED 显示Mode: FOCUS
常见故障
- 灯完全不亮:检查外部 5V 电源是否通电(万用表测电源正负极应有 5V)、WS2812 的 GND 是否和 ESP32 共地
- 灯乱闪 / 颜色不对:大概率是 GND 没共地,或 DIN 信号线太长(建议 < 20cm)
- BH1750 一直返回 0:I2C 地址不对(试 0x5C)、SDA/SCL 接反、模块虚焊
- OLED 一直黑屏:I2C 地址不对(用 Scanner 扫)、VCC/GND 接反
- 模式不切换:串口监视器没打开、波特率不是 115200、代码没上传成功
设计图
架构图(ESP32 中心 + 4 个外围)
┌──────────────────────┐
│ ESP32 DevKit V1 │
│ (主控 + WiFi) │
└──────────┬───────────┘
│
┌───────────┼───────────┐
│ │ │
GPIO 13 I2C(21/22) 5V+3.3V
│ │ │
▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌────────┐
│WS2812B │ │ BH1750 + │ │ 外部5V │
│ 12颗环 │ │ OLED │ │ 3A电源 │
│ (显示) │ │(感知+UI) │ │(供电) │
└────────┘ └──────────┘ └────────┘
模式切换状态机
| 当前状态 | 触发条件 | 切换到 |
|---|---|---|
| 任何状态 | 串口收到 'm' | MEETING |
| 任何状态 | 串口收到 'r' | FOCUS |
| FOCUS / REST | Lux < 50 | REST |
| FOCUS / REST | 时间 22:00-06:00 | REST |
| REST | Lux > 50 且 6:00-22:00 | FOCUS |
灯效色相对照表
| 模式 | 色相 | 饱和度 | 亮度 | 色温(近似) | 适合场景 |
|---|---|---|---|---|---|
| FOCUS 专注 | 160°(青蓝) | 50% | 200/255 | 6000K+ | 白天工作、写代码、阅读 |
| REST 休息 | 30°(暖橙) | 200% | 100/255 | 3000K | 夜间、放松、睡前 |
| MEETING 会议 | — | — | 0 | — | 视频会议、不想被打扰 |
| CHARGING 充电 | 120°(绿) | 255% | 30-200 呼吸 | — | PC 休眠、离开工位 |
扩展玩法
- 接 Home Assistant:ESP32 通过 MQTT 上报光照值,下班自动切夜间模式
- 加麦克风模块(MAX4466 ¥5):检测到说话自动切 MEETING 模式
- 加人体红外(HC-SR501 ¥3):离开工位 5 分钟自动切 CHARGING
- 改 24 颗灯环(¥25):更亮的视觉效果,电流升到 5A(需换 5V/5A 电源)
金句
"光线是空间的表情,而 ESP32 让你成为光线的造型师。"
本项目总成本 ¥88,比 Yeelight 灵动灯带便宜 47%,4 种模式自动切换无需手动操作,BH1750 + WS2812 + OLED 三大件都是常用模块,二次开发空间大。动手做——这是属于极客的桌面美学。