💡 划重点
- 用 ESP32 驱动 WS2812 灯环,门口三色显示会议室状态:绿色空闲、红色占用、黄色即将开始。设备通过 Wi-Fi 连接 MQTT,订阅飞书日历同步服务下发的状态,也支持本地按键手动切换。整机成本约 60 元,不依赖云端大屏,不打扰办公,安装即用。
需求洞察
目标用户是办公室员工、行政人员和会议室管理者。真实痛点是:会议室门口没有实时状态,员工走到门口才发现被占用;飞书日历里已经排了会,但门口没有同步标识;临时会议冲突时,里面的人不方便被打断,外面的人也不知道能不能进。行政统计会议室利用率时,也缺少轻量数据来源。
为什么值得做?会议室是高频共享空间,状态不透明会直接增加找房时间、沟通成本和会议迟到率。一个 60 元以内的三色灯,比智能门牌、电子屏更容易部署,不破坏墙面,不采集隐私,还能把飞书日历真正延伸到物理空间。它解决的是“最后一米”的信息差。
方案设计
感知层:飞书日历由一台常开电脑或服务器定时拉取,判断当前会议状态;ESP32 本地按键用于人工纠偏;MQTT 指令可作为远程控制入口。
控制层:ESP32 解析状态为三种:空闲、占用、即将开始。状态优先级为“占用 > 即将开始 > 空闲”。按键短按循环切换,长按 3 秒可恢复自动模式。控制层还负责心跳、离线重连和状态去重。
执行层:WS2812 灯环或灯板安装在门口,绿色呼吸表示空闲,红色常亮表示占用,黄色慢闪表示 10 分钟内即将开始。亮度自动限制在 80,避免夜间刺眼。
日志与联网层:ESP32 通过 Wi-Fi 连接 MQTT,状态发布到 meeting/room1/state,订阅 meeting/room1/cmd。MQTT 使用保留消息,新订阅者立即拿到当前状态。串口输出连接日志、按键日志和错误码,便于现场排查。
物料清单
| 物料 | 数量 | 用途 | 估算单价 | 合计 |
|---|---|---|---|---|
| ESP32 DevKit 开发板 | 1 | 主控与联网 | 18 元 | 18 元 |
| WS2812B 8 位灯板 | 1 | 三色状态显示 | 9 元 | 9 元 |
| 6×6 轻触按键 | 1 | 本地切换状态 | 1 元 | 1 元 |
| Type-C 数据线 | 1 | 供电与烧录 | 5 元 | 5 元 |
| 5V 1A 电源适配器 | 1 | 长期供电 | 10 元 | 10 元 |
| 亚克力或 3D 打印外壳 | 1 | 门口固定与遮光 | 12 元 | 12 元 |
| 杜邦线、端子、电阻、电容 | 1 套 | 接线与滤波 | 5 元 | 5 元 |
| 合计 | 60 元 |
飞书开放平台接口和 MQTT 服务器可复用现有资源,不计入硬件成本;若用公共测试 Broker,成本为 0 元。
接线与结构
WS2812 的 DIN 接 GPIO13,5V 接 5V,GND 接 GND。数据线串联 330Ω 电阻,灯板电源两端并联 1000μF 电容,降低上电冲击。ESP32 的 3.3V 数据电平驱动 WS2812 可能偏低,若出现首灯闪烁,可加 74HCT245 电平转换,或缩短数据线到 10cm 以内。
按键一端接 GPIO0,另一端接 GND,使用内部上拉,按下为低电平。GPIO0 是 ESP32 的启动模式引脚,上电瞬间不能按住,否则会进入下载模式。结构上建议按键放在外壳侧面,并加 100nF 电容消抖。外壳背面留 Type-C 口和散热孔,灯板朝外,半透明亚克力做柔光。
核心代码
以下为 Arduino 骨架,使用 Wi-Fi、PubSubClient 和 Adafruit_NeoPixel。
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_NeoPixel.h>
#define LED_PIN 13
#define KEY_PIN 0
#define LED_COUNT 8
const char* WIFI_SSID = "你的WiFi";
const char* WIFI_PASS = "你的密码";
const char* MQTT_HOST = "broker.example.com";
const int MQTT_PORT = 1883;
const char* TOPIC_STATE = "meeting/room1/state";
const char* TOPIC_CMD = "meeting/room1/cmd";
WiFiClient espClient;
PubSubClient mqtt(espClient);
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
enum RoomState { FREE, BUSY, SOON };
RoomState state = FREE;
unsigned long lastKey = 0;
unsigned long lastReport = 0;
void showColor(uint8_t r, uint8_t g, uint8_t b) {
for (int i = 0; i < LED_COUNT; i++) strip.setPixelColor(i, strip.Color(r, g, b));
strip.show();
}
void applyState() {
if (state == FREE) showColor(0, 180, 0);
else if (state == BUSY) showColor(180, 0, 0);
else showColor(180, 120, 0);
}
const char* stateText() {
if (state == FREE) return "free";
if (state == BUSY) return "busy";
return "soon";
}
void callback(char* topic, byte* payload, unsigned int len) {
String msg;
for (unsigned int i = 0; i < len; i++) msg += (char)payload[i];
if (msg == "free") state = FREE;
else if (msg == "busy") state = BUSY;
else if (msg == "soon") state = SOON;
applyState();
}
void reconnect() {
while (!mqtt.connected()) {
String cid = "esp32-room-" + String((uint32_t)ESP.getEfuseMac(), HEX);
if (mqtt.connect(cid.c_str())) {
mqtt.subscribe(TOPIC_CMD);
} else {
delay(2000);
}
}
}
void setup() {
strip.begin();
strip.setBrightness(80);
applyState();
while (WiFi.status() != WL_CONNECTED) delay(500);
mqtt.setCallback(callback);
}
void loop() {
if (!mqtt.connected()) reconnect();
mqtt.loop();
if (digitalRead(KEY_PIN) == LOW && millis() - lastKey > 300) {
lastKey = millis();
state = (RoomState)((state + 1) % 3);
applyState();
}
if (millis() - lastReport > 30000) {
lastReport = millis();
}
delay(20);
}
飞书日历同步在服务端完成:定时读取日历事件,判断当前是否有会、是否 10 分钟内开始,再向 MQTT 发布 free、busy 或 soon。设备端只负责显示和本地纠偏,逻辑更稳定。
调试步骤
- 不接灯板,先上电测 ESP32 串口和 Wi-Fi 连接。
- 接 WS2812,确认 GPIO13 输出,灯板依次显示红、绿、黄。
- 接按键,测试短按切换和消抖,观察串口状态变化。
- 用 MQTT 客户端手动发布 free、busy、soon,确认设备订阅和显示同步。
- 启动飞书日历同步脚本,模拟会议开始、结束、即将开始三种场景。
- 装入外壳,固定在会议室门口,连续运行 24 小时观察重连和发热。