💡 划重点
- 核心逻辑:用 SCD41 实时感知书桌 CO₂ 浓度,当浓度超过 1000ppm 时自动切换 WS2812 灯环为警示色并触发通风提醒,浓度正常时保持暖白光专注模式。
- 成本控制:主控 + 传感器 + 灯环 + 零散配件总价控制在 120 元内,无外壳时成本可压至 100 元左右。
- 交互闭环:按键切换「专注白光 / 呼吸彩光 / 智能自动」三档,灯环颜色同时作为可视化数据反馈,蓝牙负责把日志发到手机。
- 技术选型:ESP32-C3 自带 WiFi/BLE 但功耗低,Arduino 框架开发快,SCD41 的 I2C 地址固定且支持单次测量(省电)。
- 接线要点:I2C 用 GPIO8/9(软 I2C 任意脚可配),灯环走 GPIO10,按键接 GPIO3(内部上拉,按下为低电平)。
- 可扩展性:代码预留 BLE 服务特征值,后期可接入 Home Assistant 或手机小程序做长期数据曲线。
需求洞察
目标用户是居家办公/备考学生、儿童房书桌场景下的长期伏案人群。这些人每天在书桌前连续待 4-8 小时,尤其冬天开暖气或夏天开空调时门窗紧闭,室内 CO₂ 浓度很容易从室外 400-500ppm 飙升至 1500-2000ppm。
真实痛点在于:CO₂ 浓度超过 1000ppm 后人会开始感到昏沉、注意力分散,超过 1500ppm 时甚至出现轻度头痛。但大多数人不知道是空气质量导致效率低,只会怪自己不够专注,导致困了强撑、累了刷手机,效率越来越差。
为什么值得做:市面上有 CO₂ 检测仪,但摆在那里也只是个数字,和「学习专注」没有直接情感关联。台灯是书桌天然标配,把环境数据转化为灯的色调和亮度变化,人类天然对光线变化有潜意识反馈——灯变红了,就意味着「该开窗了」。这比看仪表盘更直觉化。此外成本控制在 120 元内,对 DIY 玩家毫无压力,成品价值却远超物料,值得动手。
方案设计
整个系统分四层:
- 感知层:SCD41 负责 CO₂ 和温湿度采集。用 I2C 通讯(软 I2C 走 GPIO8/9),数据更新频率设为每 10 秒一次,因为 CO₂ 本身变化很慢,没必要高速采样。SCD41 支持单次测量模式,可降低功耗,不过台灯本身一直插电,功耗不是核心指标。
- 控制层:ESP32-C3 做主控,接收 SCD41 的原始数据并进行滤波(简单滑动平均,取 5 次采集中位数),维护一个
airQualityLevel枚举状态(0=优秀 / 1=一般 / 2=较差 / 3=危险),按键按下时切换运行模式。 - 执行层:WS2812 灯环 24 颗灯珠作为台灯光源 + 状态指示。不同模式下灯环呈现不同颜色布局:
- 专注模式(常亮):统一暖白色 2700K,亮度可调。
- 智能自动模式:CO₂ < 800ppm 显示淡青色(清爽),800-1000ppm 显示淡黄色(轻微注意),1000-1500ppm 显示橙色(轻度预警),> 1500ppm 显示红色呼吸灯(强通风提醒)。
- 彩灯模式:用户自定义循环渐变色,纯粹调节心情。
- 日志/联网层:BLE 低功耗蓝牙。ESP32-C3 作为 BLE Peripheral 设备,建立自定义 Service(UUID
0000FFF0-0000-1000-8000-00805F9B34FB),包含两个 Characteristic:一个可读的「环境数据」字段(CO₂+温度+湿度打包成 6 字节),另一个可写的「模式控制」字段(接收手机端下发模式指令)。手机打开 BLE Scanner 或简易小程序即可看实时数据。由于本方案目标是低成本离线设备,不做 WiFi 云上传统计,后期可加。
物料清单
| 物料 | 数量 | 用途 | 估算单价(元) | 小计(元) |
|---|---|---|---|---|
| ESP32-C3 SuperMini | 1 | 主控,带 BLE/WiFi | 12.50 | 12.50 |
| SCD41 传感器模块 | 1 | CO₂ + 温湿度测量,I2C 接口 | 75.00 | 75.00 |
| WS2812B 24 位灯环(内径约 6cm) | 1 | 台灯光源 / 状态彩灯 | 18.00 | 18.00 |
| EC11 旋转编码器(带按键) | 1 | 用户交互输入(旋转调亮度,按键切换模式) | 3.50 | 3.50 |
| 光敏电阻 + 10kΩ 电阻 | 各1 | 感知环境光强,自动降亮度(功能可选,但做出来体验好) | 1.00 | 1.00 |
| 5V 2A USB 电源线(或 DC 座) | 1 | 供电 | 4.00 | 4.00 |
| 杜邦线 + 洞洞板(或热缩管) | 若干 | 连接和焊接 | 3.00 | 3.00 |
| 台灯灯罩 / 漫反射板(透光亚克力/描图纸) | 1 | 柔化灯环光线,防眩光 | 2.00 | 2.00 |
| 74AHCT125 电平转换(双向) | 1 | SCD41 I2C 需要(可选,若 SCD41 模块内置安全则省略) | 0.00 | 0.00 |
| 合计 | - | - | - | 119.00 |
注:SCD41 模块购买时尽量选带 I2C 电平转换的版本,因为 ESP32-C3 的 GPIO 是 3.3V,SCD41 原片需要 2.4-5.5V 供电,通讯逻辑电平 2.4-3.6V,直接连模块通常 OK。如果买的是裸片+分压电路,则必须加转换,但本清单预算按安全模块版设计,超出的 1 元用于可焊接的排针或胶水。
接线与结构
接线定义(默认无洞洞板裸接逻辑):
| 模块 | 引脚 | 连接目标 |
|---|---|---|
| SCD41 VIN | 3.3V(外接供电) | ESP32-C3 3V3 或 USB 5V(用 LDO 转 3.3) |
| SCD41 GND | GND | ESP32-C3 GND |
| SCD41 SCL | ESP32-C3 GPIO8 | 软 I2C 时钟 |
| SCD41 SDA | ESP32-C3 GPIO9 | 软 I2C 数据 |
| WS2812 灯环 DIN | ESP32-C3 GPIO10 | 灯环数据输入 |
| WS2812 灯环 5V+ | USB 5V | 注意灯环全亮会超过 0.5A,若板载 AMS1117-3.3 则不可从 3V3 取电,直接从 5V 供电并共地 |
| WS2812 灯环 GND | GND | 共地 |
| EC11 按键引脚 SW | GPIO3 | 内部上拉,按下为低电平(有效) |
| EC11 旋转 A / B | GPIO1 / GPIO2 | 编码器相位检测(若只用按键可省略) |
| 光敏电阻(+10k 分压) | GPIO0(AH) (ADC) | 读取环境亮度 |
接线微调说明:原定 I2C 用 8/9,SCD41 模块上电需要 20ms 稳定时间,GPIO8/9 在 ESP32-C3 上是普通 I/O,没有特殊禁止位。关于 GPIO3,在 ESP32-C3 数据手册中它是带内部上拉的模式引脚(可作为按键)。不过 GPIO3 在下载时有特殊作用(某些开发板用于 BOOT 指示),实际开发时若不方便按 BOOT 键重新烧录,建议改用 GPIO4。如果你手头板子 GPIO3 引脚被 Flash 占用或带电感,可以微调至 GPIO4 并同时把逻辑改掉。本方案初稿按 GPIO3 来写,若编译烧录时遇到不能自动下载,将按键线从 GPIO3 拔掉焊到 GPIO4 即可,无需改代码(用宏定义切换)。
结构安装:
- 找一个底部直径略大于灯环外径的金属/纸质台灯罩,将 WS2812 灯环面朝上平放于底座中央,用热熔胶固定边缘。
- 在灯罩侧面开一个小孔(直径 5mm),把 SCD41 露出来,保证传感器通风口不被壳体完全挡住。CO₂ 传感器需要气体扩散,不要直接塞进密闭底座内部,否则测的是机器内部废气。建议将 SCD41 通过排线引出,固定在灯罩上沿接近桌面呼吸带高度(约 20-30cm 高处)。
- EC11 旋转编码器嵌在灯座侧面,方便随手旋转调节亮度、按下开关灯或切换模式。光敏电阻放灯罩顶部朝上(测环境光)并用热缩管做挡光避免台灯自带光源直射导致误判。
- ESP32-C3 主控板双面胶贴在底座内部远离发热区(SCD41 虽有自动加热但本体温度低)。
核心代码
// CO2 Desk Lamp for ESP32-C3 (Arduino Framework)
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_NeoPixel.h>
#include "Adafruit_SCD4x.h" // SCD41 库
// ---- 引脚配置 ----
#define I2C_SDA 9
#define I2C_SCL 8
#define LED_PIN 10
#define BTN_PIN 3
#define PHOTO_ADC 0 // 光敏电阻 ADC
#define NUMPIXELS 24
// ---- 全局对象 ----
TwoWire myWire = TwoWire(0);
Adafruit_SCD4x scd4x;
Adafruit_NeoPixel strip(NUMPIXELS, LED_PIN, NEO_GRB + NEO_KHZ800);
// ---- 状态 ----
volatile bool btnPressed = false;
uint8_t mode = 1; // 0: 专注白光, 1: 智能自动, 2: 呼吸彩灯
uint16_t co2 = 0; float temp = 0; float hum = 0;
int brightness = 100; // 0-255 近似亮度百分比
void setup() {
Serial.begin(115200);
delay(500);
// LED 灯环
strip.begin(); strip.show(); // 初始化全灭
strip.setBrightness(50);
// 按键
// I2C + SCD41(地址默认 0x62)
if (!scd4x.begin(&myWire;)) {
Serial.println("SCD41 not found!");
while (1) { delay(10); } // 错误指示用红灯闪烁代替
}
if (!scd4x.startPeriodicMeasurement()) {
}
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
// 按键可以切换模式
if (btnPressed) {
btnPressed = false;
mode = (mode + 1) % 3;
// 切换模式闪烁一下确认
}
// 读取光敏电阻调节全局亮度(可选)
int photo = analogRead(PHOTO_ADC);
int autoBright = map(photo, 0, 4095, 255, 40);
strip.setBrightness(autoBright); // 自动亮度调节
if (scd4x.readMeasurement()) {
co2 = scd4x.getCO2();
temp = scd4x.getTemperature();
hum = scd4x.getHumidity();
// 三种执行逻辑
switch(mode) {
case 0: renderFocusLight(); break; // 固定暖白灯
case 1: renderCO2Feedback(); break; // 根据CO2变色/呼吸
case 2: renderBreathRainbow(); break; // 彩色呼吸
}
printDebugInfo();
} else {
// 若没有数据,让灯环稍微降低亮度闪烁(不喧宾夺主)
}
delay(500); // 2Hz 查询频率够用,但实际SCD41周期测量约5秒完一次
}
void renderFocusLight() {
// 暖白 (R 255, G 200, B 150)可以近似模拟白炽灯
uint32_t warmWhite = strip.Color(255, 190, 120);
fillSolid(warmWhite);
}
void renderCO2Feedback() {
uint32_t c;
if (co2 < 800) c = strip.Color(120, 255, 180); // 翠绿偏舒适
else if (co2 < 1000) c = strip.Color(200, 255, 120); // 黄绿
else if (co2 < 1500) c = strip.Color(255, 150, 30); // 橙红预警
else { // >1500 红色呼吸
int breath = (sin(millis() * 0.002) * 0.5 + 0.5) * 200 + 55;
return;
}
fillSolid(c);
}
void renderBreathRainbow() {
int hue = (millis() / 20) % 65535; // 色相渐变循环
uint32_t rgb = strip.gamma32(strip.ColorHSV(hue, 255, brightness));
fillSolid(rgb);
}
void fillSolid(uint32_t color) {
for(int i=0; i<NUMPIXELS; i++) strip.setPixelColor(i, color);
strip.show();
}
void colorWipe(uint32_t color, int wait) {
for(int i=0; i<NUMPIXELS; i++) {
}
}
void IRAM_ATTR onButton() {
btnPressed = true; // 中断中不执行耗时操作
}
void printDebugInfo() {
Serial.printf("CO2: %d ppm, Temp: %.1f°C, RH: %.0f%%\n", co2, temp, hum);
}
说明:如果你用的是 PlatformIO,在platformio.ini中增加lib_deps = adafruit/Adafruit NeoPixel, adafruit/Adafruit SCD4x。IO 口定义有微调自由,I2C 库使用软件 TwoWire,GPIO8/9 没问题。传感器测量周期约 5 秒,但只判断 CO₂ 时 500ms 查询一次不会阻塞 I2C 总线。
调试步骤
- 第一优先级 - 通信验证:接线后先只烧 I2C 扫描代码,在串口确认 0x62 地址出现。SCD41 上电后 1 秒内不应响应,若有响应且重复出现,说明电气连接 OK。若扫描不到,大概率是接线错误或模块供电不足(换 USB 独立供电试试)。
- 灯环点亮测试:注释掉 SCD41 相关代码,设置 GPIO10 输出固定颜色,确认 LED 能亮起。如果完全不亮,检查 WS2812 DIN 口是否连对,GND 是否与主控共地,5V 供电是否足够(若用电脑 USB 可能电压不足,灯板全白亮度会显得很暗)。
- 按键中断测试:按下按键看串口是否有模式切换打印,如果没反应,多半是因为 GPIO3 在电路板上有外接电容或连接 BOOT 按键导致干扰,更换到 GPIO4(下载模式检查)。不要忽视——这个按键就是用户最直观的控制入口。
- 传感器精确度校准:SCD41 出厂已经校准过 CO₂,通常 400ppm 基准偏差 ±50ppm,不需额外校准。但若装置近期内受过酒精等污染,可以做一个 48 小时室外通风复位(户外开窗把设备放在窗台吹 48h 后调用
scd4x.performForcedCalibration(400)指令)。 - 蓝牙调试:手机下载 nRF Connect 或 LightBlue,扫描到名字带 ESP32-C3 的设备,连上后读「CO2 Data」特征值,看 Hex 数据是否能正确解析(定义协议头 + CO₂ 高字节 + 低字节 + 温度 + 湿度 + 校验)。后期可加一个简单的自定义 Service 做双向指令。
- 全系统综合烧录:调用
scd4x.startPeriodicMeasurement()后前 10 分钟数据上升属正常(模块自热效应)。让传感器在桌面静止放置 30 分钟后读数稳定在 600-900ppm(未开窗)。对着传感器哈一口气,看到 CO₂ 跳到 1500ppm 以上,灯环应变成红色呼吸提醒——此时测试通过。
设计图
该流程沿顺时针方向重复:感知层首先采集 CO₂ 浓度与环境温湿度;ESP32-C3 在本地对数据插值、滤波并判断当前空气质量级别,同时检测按键状态(旋转编码器 / 轻触按键)以更改用户模式;随后 WS2812 灯环呈现暖白专注光、智能变色或呼吸彩灯,完成物理层面反馈;最后通过 BLE 将本次采集的数据记录和模式状态推送给手机 App(或断开连接,日志仅在本地保持可视状态)。如果未来接上 WiFi,可将数据打包走 MQTT,但当前蓝牙版本足以完成核心场景——即监测、警示、提醒通风、专注氛围切换。