💡 划重点
- 门口三色灯环实时显示会议室空闲、占用、即将释放,减少无效推门。
- ESP32 通过 WiFi 拉取飞书日历,同时支持 MQTT 远程同步和按键手动切换。
- WS2812 接 GPIO13,按键接 GPIO0,整体成本控制在 65 元内。
- 核心逻辑是状态机:日历优先、手动覆盖、MQTT 兜底,灯效直观不刺眼。
需求洞察
目标用户是办公室行政、会议室高频使用团队,以及临时找会议室的项目组。真实痛点很具体:飞书日历上明明显示空闲,走到门口却发现有人临时开会;或者会议已结束,日历没释放,别人不敢进。纸质登记、白板擦写、群里问“有人吗”都依赖人工,信息滞后且容易冲突。会议室门口缺一个低成本的“物理状态指示器”。这个项目值得做,因为它不改造门禁、不侵入现有系统,只用一盏灯把日历数据变成一眼可见的信号,安装简单,维护成本低,还能扩展到多个会议室统一管理。
方案设计
感知层负责获取状态来源。第一路是飞书日历开放平台,通过应用凭证和会议室 ID 拉取当前日程,判断是否处于占用时段;第二路是 MQTT 订阅,接收其他系统或手动控制端发来的状态指令;第三路是 GPIO0 按键,用于临时会议、设备故障或日历不同步时手动切换。控制层由 ESP32 承担,运行一个有限状态机,状态包括空闲、占用、即将释放、未知。优先级设计为:手动按键短时覆盖日历,MQTT 指令可远程覆盖,日历数据作为默认依据。执行层使用 WS2812 灯环,绿色表示空闲,红色表示占用,黄色表示即将释放或数据待确认,灯效采用呼吸或缓慢闪烁,避免走廊里过于刺眼。日志与联网层负责 WiFi 连接、MQTT 上报、串口调试和异常重连。ESP32 定时向 MQTT 主题发布当前状态、日历来源和时间戳,方便后台排查。若网络断开,设备进入未知状态,黄灯慢闪,同时保留按键切换能力。
物料清单
| 物料 | 数量 | 用途 | 估算单价 | 合计 |
|---|---|---|---|---|
| ESP32 开发板 | 1 | 主控与联网 | 25 元 | 25 元 |
| WS2812 灯环 | 1 | 三色状态显示 | 12 元 | 12 元 |
| 轻触按键 | 1 | 手动切换状态 | 0.5 元 | 0.5 元 |
| 10k 电阻 | 1 | GPIO0 上拉 | 0.1 元 | 0.1 元 |
| 330 欧电阻 | 1 | WS2812 数据限流 | 0.1 元 | 0.1 元 |
| 5V 电源适配器 | 1 | 供电 | 8 元 | 8 元 |
| 亚克力外壳或 3D 打印件 | 1 | 门口安装 | 10 元 | 10 元 |
| 杜邦线、洞洞板 | 若干 | 接线 | 5 元 | 5 元 |
| 合计 | 60.7 元 |
接线与结构
WS2812 的 DIN 接 ESP32 GPIO13,VCC 接 5V,GND 共地。数据线串 330 欧电阻,灯环电源两端并联 1000 微法电容更稳。按键一端接 GPIO0,另一端接 GND,GPIO0 通过 10k 电阻上拉到 3.3V。GPIO0 是启动模式引脚,上电瞬间不能按住按键,否则会进入下载模式;正常运行时按键可安全使用。若现场安装不便,可微调为 GPIO4,但本方案按给定接线。结构上,灯环朝走廊方向,按键放在外壳下方,ESP32 和电源藏在盒内。外壳留出散热孔和 USB 调试口。
核心代码
以下为 Arduino 骨架,包含 WiFi、MQTT、WS2812 和按键状态切换。飞书日历部分用 HTTP 请求占位,实际需替换应用凭证和会议室 ID。
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_NeoPixel.h>
#define LED_PIN 13
#define KEY_PIN 0
#define LED_COUNT 12
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
WiFiClient espClient;
enum RoomState { FREE, BUSY, SOON, UNKNOWN };
RoomState state = UNKNOWN;
unsigned long lastKey = 0;
void setColor(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) setColor(0, 80, 0);
else if (state == BUSY) setColor(120, 0, 0);
else if (state == SOON) setColor(120, 80, 0);
else setColor(60, 60, 0);
}
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 checkKey() {
if (digitalRead(KEY_PIN) == LOW && millis() - lastKey > 300) {
lastKey = millis();
state = (RoomState)((state + 1) % 3);
applyState();
client.publish("room/status", state == FREE ? "free" : state == BUSY ? "busy" : "soon");
}
}
void fetchFeishuCalendar() {
// 使用 HTTPClient 请求飞书日历接口,解析当前日程。
// 若存在进行中的会议,state = BUSY;若 10 分钟内开始,state = SOON;否则 FREE。
// 建议每 60 秒轮询一次,并加入 token 缓存与失败重试。
}
void setup() {
strip.begin();
strip.show();
while (WiFi.status() != WL_CONNECTED) delay(500);
client.setCallback(callback);
}
void loop() {
if (!client.connected()) client.connect("room-esp32");
client.loop();
checkKey();
static unsigned long last = 0;
if (millis() - last > 60000) { last = millis(); fetchFeishuCalendar(); applyState(); }
delay(10);
}
调试步骤
第一步,不接灯环,先烧录串口测试,确认 ESP32 能连接 WiFi 并打印 IP。第二步,接入 WS2812,运行红绿黄三色测试,检查数据线和供电是否稳定。第三步,配置 MQTT 服务器,用手机或电脑发布 free、busy、soon,观察灯环变化和串口日志。第四步,测试按键,确认短按能循环切换状态,且上电时不误触。第五步,申请飞书开放平台应用,获取 tenant_access_token,读取会议室日历,先用串口输出日程判断结果。第六步,把飞书结果映射到状态机,设置 60 秒轮询。第七步,整机装壳,固定在门口,观察一天,重点检查网络断开、会议延长、临时取消三种异常。