💡 划重点
- ESP32 作为主控,通过 Wi‑Fi 定时抓取飞书日历,解析会议室忙闲状态
- 使用单颗 WS2812 三色灯珠直观显示占用(红)、空闲(绿)、即将占用(黄)
- 支持本地按键一键切换状态,应对临时会议或紧急占用
- 全量数据通过 MQTT 上报,便于物业或行政系统集成
- 硬件成本锁定在 65 元以内,单点部署无压力
需求洞察
目标用户:需要频繁使用小型会议室的知识工作者、行政管理人员、共享办公空间运营方。
真实痛点:纸质预约表易被忽略,线上日历看不到门口实况;会议室门口“有人但未被预订”的冲突频发;临时占用后忘记释放导致下一位预约者等待;行政人员无法实时掌握空间使用数据。
为什么值得做:
- 飞书日历已成为许多团队的标准协作工具,直接拉取日历数据可以避免重复录入,降低使用门槛。
- 三色灯方案比屏幕方案成本低数倍,却依然能提供清晰的状态指示,适合批量铺设。
- 结合本地按键和云端上报,既保留了离线可用性,又为数据驱动的空间优化提供基础。
- 低成本硬件加开源代码,学生、创客、小团队都能快速复刻和二次开发。
方案设计
整体分四层:感知层、控制层、执行层、日志/联网层。
感知层
核心感知来自飞书日历 API,ESP32 每隔 60 秒通过 HTTPS GET 请求当前房间的日程列表。为避免频繁调用,引入本地缓存和忙闲推断逻辑。本地按键(GPIO 0)提供物理交互,短按在空闲与占用间切换,长按 3 秒进入“即将占用”状态(用于临时延长会议)。
控制层
ESP32 上运行 Arduino 框架,兼顾 Wi‑Fi 客户端、HTTP 请求、JSON 解析与 WS2812 驱动。主循环中维护状态机:IDLE、OCCUPIED、UPCOMING(15 分钟内即将开始)。状态机综合日历数据和按键输入决定最终颜色。为减少云端依赖,系统保留离线模式——无网络时以按键为主,并将脱机事件缓存在本地。
执行层
单颗 WS2812 灯珠布置在雾面亚克力散光罩内,通过三色明确传达会议室状态:
- 绿色常亮:当前及接下来 30 分钟均无预约,可自由使用。
- 黄色呼吸:当前空闲,但 15 分钟内有会议开始。
- 红色流水:会议进行中,请不要打扰。
灯效通过 FastLED 库实现非阻塞动画,例如黄色呼吸灯每 2 秒完成一次渐亮渐灭,红色流水灯以 60 帧速度平滑滚动。
日志/联网层
设备启动后连接指定 Wi‑Fi,并注册到 MQTT Broker(测试可用 EMQX Cloud 免费版或本地 Mosquitto)。每次状态变化时,向主题 room/{room_id}/status 发布 JSON 消息,包含 occupied、upcoming、timestamp、source(calendar / button)。同时订阅 room/{room_id}/override,允许远程管理系统强制设置状态(如物业紧急关闭会议室)。心跳包每 5 分钟上报一次,包含信号强度、堆栈余量、运行时间,便于集中监控。
物料清单
| 物料 | 数量 | 用途 | 估算单价(元) |
|---|---|---|---|
| ESP32‑WROOM‑32 开发板 | 1 | 主控 + Wi‑Fi | 18 |
| WS2812B 5050 灯珠 | 1 | 三色状态指示 | 1.5 |
| 轻触按键(6×6×5mm) | 1 | 本地切换 | 0.5 |
| 10kΩ 贴片电阻 | 2 | 按键上拉、限流 | 0.2 |
| 1000µF/6.3V 电解电容 | 1 | 稳定 WS2812 供电 | 0.5 |
| AMS1117‑3.3V 稳压模块 | 1 | 供电(可选,若开发板自带可省) | 1.5 |
| Micro‑USB 母座/供电线 | 1 | 取电 | 2 |
| 雾面亚克力外壳(定制雕刻) | 1 | 散光及保护 | 15 |
| 杜邦线、排针、热缩管 | 若干 | 内部连线 | 3 |
| 合计 | 42.2 |
目标成本 65 元,实际物料总价不到 43 元,留有充足余量应对单价波动或替换更美观的外壳。
接线与结构
遵照建议接线并微调:WS2812 数据线接 GPIO 13,理由是该引脚在 ESP32 上为 RMT 外设默认通道之一,FastLED 可无冲突驱动;按键一端接 GPIO 0,另一端接地,启用内部上拉以省掉外部电阻。补充说明:
- WS2812 VDD 与 GND 之间并联 1000µF 电容,靠近灯珠放置,防止瞬间电流拉低主控电压。
- 按键并联 0.1µF 电容硬件消抖,若代码中做软件消抖可省略。
- Type‑C / Micro‑USB 供电 5V 直接输入开发板 VIN,同时在 5V 与 GND 间加一颗自恢复保险丝 500mA 进行过流保护。
- 灯珠安装于外壳开孔处,罩上雾面亚克力片,使光线均匀柔和。
- 整个电路板可用热熔胶固定在外壳底部,仅露出 USB 供电口、按键和灯罩。
核心代码
以下为 Arduino 平台骨架,基于 ESP32 和 FastLED,已包含日历拉取、状态机、MQTT 上报框架。请自行替换 Wi‑Fi 凭证、飞书应用凭证及 MQTT 地址。
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <FastLED.h>
#include <PubSubClient.h>
#define LED_PIN 13
#define BTN_PIN 0
#define NUM_LEDS 1
CRGB leds[NUM_LEDS];
const char* ssid = "yourSSID";
const char* password = "yourPASS";
const char* mqtt_server = "broker.emqx.io";
const char* room_id = "meeting_room_301";
WiFiClient espClient;
PubSubClient mqtt(espClient);
enum State { FREE, UPCOMING, OCCUPIED };
State currentState = FREE;
State lastPublished = FREE;
unsigned long lastCalendarCheck = 0;
bool lastBtn = HIGH;
unsigned long btnPressStart = 0;
void setup() {
Serial.begin(115200);
FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(64);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
connectMQTT();
}
void loop() {
if (!mqtt.connected()) connectMQTT();
mqtt.loop();
// 日历拉取
if (millis() - lastCalendarCheck > 60000) {
checkCalendar();
lastCalendarCheck = millis();
}
// 按键处理
bool btnState = digitalRead(BTN_PIN);
if (lastBtn == HIGH && btnState == LOW) {
btnPressStart = millis();
} else if (lastBtn == LOW && btnState == HIGH) {
unsigned long pressLen = millis() - btnPressStart;
if (pressLen > 3000) {
currentState = UPCOMING;
} else if (pressLen > 50) {
currentState = (currentState == OCCUPIED) ? FREE : OCCUPIED;
}
publishState("button");
}
lastBtn = btnState;
// 灯效
updateLEDs();
}
void checkCalendar() {
HTTPClient http;
http.begin("https://open.feishu.cn/open-apis/calendar/v4/calendars/primary/events?page_size=1");
int code = http.GET();
if (code == 200) {
StaticJsonDocument<1024> doc;
bool hasActive = false, hasUpcoming = false;
// 解析 events,判断当前是否被占用或 15 分钟内即将开始
// 此处为示意,实际需根据 start_time/end_time 计算
// ...
if (hasActive) currentState = OCCUPIED;
else if (hasUpcoming) currentState = UPCOMING;
else currentState = FREE;
publishState("calendar");
}
http.end();
}
void updateLEDs() {
switch (currentState) {
case FREE:
leds[0] = CRGB::Green;
FastLED.show();
break;
case UPCOMING: {
uint8_t val = (sin8(millis() / 4) + 128) >> 1;
leds[0] = CHSV(42, 255, val);
FastLED.show();
break;
}
case OCCUPIED: {
uint8_t pos = (millis() / 10) % 255;
leds[0] = CHSV(0, 255, beatsin8(30, 50, 255));
break;
}
}
}
void publishState(String source) {
if (currentState == lastPublished) return;
lastPublished = currentState;
StaticJsonDocument<200> doc;
doc["occupied"] = (currentState == OCCUPIED);
doc["upcoming"] = (currentState == UPCOMING);
doc["timestamp"] = millis();
doc["source"] = source;
char buf[200];
serializeJson(doc, buf);
mqtt.publish(("room/" + String(room_id) + "/status").c_str(), buf);
}
void mqttCallback(char* topic, byte* payload, unsigned int length) {
// 接收远程 override 指令
}
void connectMQTT() {
while (!mqtt.connected()) {
delay(2000);
}
mqtt.subscribe(("room/" + String(room_id) + "/override").c_str());
}
完整工程可加入 OTA 更新、网页配网等功能,上述代码已覆盖核心业务流程。
调试步骤
- 上电前检查:用万用表确认 5V 与 GND 无短路,按键按下时 GPIO0 电压接近 0V,抬起时接近 3.3V。
- 串口监视:烧录后打开串口终端(115200 bps),观察 Wi‑Fi 连接与 MQTT 状态,连接失败会有打印提示。
- 灯色验证:初次启动默认为 FREE,灯珠应亮绿色。若灯珠不亮或颜色不对,检查 FastLED 芯片类型是否为 WS2812B、引脚定义是否匹配。
- 按键测试:短按按键,观察串口是否输出状态变化,灯色应由绿变红,再按变绿。长按 3 秒以上应显示黄色呼吸效果。
- 日历测试:在飞书日历中为指定房间创建一条进行中的事件,等待 60 秒后灯应变为红色流水;创建一个 15 分钟内开始的事件,空闲状态下应变为黄色。
- MQTT 检查:使用 MQTT 客户端订阅
room/meeting_room_301/status,每次状态变化应收到 JSON 消息;向room/meeting_room_301/override发送{"occupied":true}应强制红灯。 - 稳定性考核:连续运行 24 小时,通过串口观察堆栈和心跳消息是否正常,无重启或内存泄漏。