📋 专案概述
我的午睡纪律曾经完美得可笑:手机闹钟 13:20,躺下 12:45——「睡 35 分钟刚刚好」。现实是:闹钟响的那一刻,我有一半的概率正躺在深度睡眠的正中央,被 80 分贝的手机铃声从里面凿出来。那种「醒来十分钟不知道自己是谁」的睡眠惰性(sleep inertia)比不睡还惨——整个下午的会议我都在回放那个没有逻辑的梦境,代码写出两个低级的 off-by-one,被 review 退回。
找过替代品,每一样都撞墙:
- 智能手环的「智能闹钟」:原理没错(在浅眠窗口叫醒),但午睡要戴着手环睡——手腕上多个东西我根本睡不着,而且为了午睡,手环得整夜戴着充电排班。
- 床垫感应带(Withings Sleep 一类):NT$4,000 起,压在床垫下确实无感,但我们家那张旧床垫连 Wi-Fi 密码都「压」不稳——它自己的 App 连线天天断。
- 镜头方案:直接排除。卧室放镜头这件事,对机器的隐私要求应该比对人更严。
于是这台机器:无镜头、无穿戴、隔着一床被子和 50 公分的空气,用 24GHz 毫米波雷达「听」你的存在与微动——躺下自动布防,规则引擎粗估睡眠深浅,在你午睡目标时间的浅眠窗口(容许区间内)启动唤醒;如果一路深睡没窗口,最迟 40 分钟强制温柔收尾。全部材料 NT$215:
- 躺下即布防:LD2410 雷达侦测到床上有人且渐趋静止 → 自动进入午睡模式,不用碰手机、不用按钮
- 浅眠窗口智能起床:以微动能量周期粗估深浅眠(规则引擎,非医疗),目标 +20 分起出现浅眠窗即唤醒;+40 分仍无窗口则强制唤醒——永远不会让你睡过头误了下午的会
- 渐进唤醒三阶段:床头灯条 5 分钟日出模拟 → 蜂鸣器由 40 分贝渐强鸟鸣式短音 → 按住按钮 1 秒即灭;被光叫醒和被声音叫醒,起床气差了整整一个下午
- 手机 BLE 躺床微调:本机零按钮零屏幕——感度、目标时长、音量全部躺著用手机调,不用爬起来找电脑
- 午睡战报推 Telegram:几点躺下、几分钟入睡、估计深睡几分钟、几点醒来;两周数据会告诉你「13:05 前躺下胜率明显比较高」这种自己永远发现不了的事
- 宠物与窗帘防误触:距离门窗锁住枕头区,猫和窗纱都在门外——这条是被踩脸叫醒三次之后写进韌体的
算一盘账:浅眠唤醒手环约 NT$1,000 起+戴著睡的代价;床垫带 NT$4,000 起。本机 215 元买到的是同一套「浅眠窗口」理念+你卧室里唯一一台物理上没有镜头的睡眠感测器。而且手环给你的是昨晚的分数,这台给你的是今天下午的清醒——睡眠资料的终极用途,是让你在需要清醒的时段清醒。
为什么选 ESP32-C6?
| 需求 | C6 的对应 |
|---|---|
| BLE(躺床调参是本机的「屏幕」) | BLE 5.0,与 Wi-Fi 并发;手机 App 直连,不用开壳不用串口 |
| Wi-Fi(战报推播) | Wi-Fi 6(6GHz 管理帧保护);推播每小时几次,对寝室电网毫无存在感 |
| UART 数量 | 3 组 UART:雷达一路、序列埠除错一路,互不抢道 |
| RGB 灯条驱动 | 内建 RMT 外围,core 3.x 的 neopixelWrite() 免函式库直驱 WS2812 |
| 不需要的(摄像头/双核算力/深睡微安) | 本机常供电不深睡、无影像——S3 与 C2 的看家本领这里都用不上 |
环境:arduino-esp32 core ≥ 3.0(neopixelWrite 与 BLEDevice 皆内建),开发板选 ESP32C3 DEV Module 列表里的 ESP32C6 Dev Module(或 ESP32C6 Super Mini),USB CDC On Boot: Enabled,PSRAM 关。
🧱 硬体清单
| 项目 | 型号 | 数量 | 预估单价 | 备注 |
|---|---|---|---|---|
| 主控板 | ESP32-C6 SuperMini | 1 | ~55 元 | BLE 5+Wi-Fi 6,USB-C 烧录 |
| 雷达模组 | HLK-LD2410(24GHz 人体存在) | 1 | ~90 元 | 选 UART 版(4 针 GND/VCC/TX/RX);固件 v3.x 以上 |
| 唤醒灯条 | WS2812B 8 灯柔性条 5V | 1 | ~25 元 | 暖白灯珠版更催眠 friendly;IC 端串 330Ω |
| 蜂鸣器 | 12mm 被动蜂鸣器带板 | 1 | ~10 元 | PWM 渐强必需被动式 |
| 按钮 | 6×6 轻触开关(本机唯一按钮) | 1 | ~3 元 | 贪睡/中止唤醒;短按=再睡 10 分 |
| 电源 | 5V/2A USB 头+1.5m USB-C 线 | 1 | ~25 元 | 雷达峰值电流看紧一点,别用电脑 USB 孔兼供 |
| 零件 | 330Ω 电阻+1000µF 电解帽 | 各 1 | ~5 元 | 灯条首脚保护,电容防启动压降雪崩 |
| 列印料 | PLA 约 35g(一般 PLA,禁金属感炫光丝) | — | ~12 元 | 镀金属丝会屏蔽雷达波,见列印节 |
| 合计 | ~225 元 | 床垫带的 1/18 |
🔌 接线图
ASCII 对照表
🖨️ 3D 列印零件
零件清单
| 零件 | 数量 | 材质建议 | 颜色 | 列印时间 |
|---|---|---|---|---|
| 雷达头壳(12° 仰角、正面对枕) | 1 | PLA(一般) | 雾灰 | ~1.2 hr |
| 灯条扩散壳(0.9mm 薄壁+蜂窝导光) | 1 | PLA | 白(透光) | ~50 min |
| 床头板挂钩(免钻孔) | 1 | PETG | 任意 | ~25 min |
| 主线夹 ×3/按钮侧壳 | 4 | PLA | 雾灰 | ~20 min |
| 主控盒(塞床头后侧) | 1 | PLA | 黑 | ~40 min |
列印设定与电磁纪律
- 雷达正面壁厚 ≤1.6mm 且必须是纯 PLA/PETG:毫米波穿塑料无压力,但金属感炫光丝(silk metallic)镀有金属箔层,会把雷达变成一块砖——正面罩用之前,先用未装模组的壳+手机 App 对位验证回波
- 雷达头 12° 仰角:挂钩装在床头板上缘时,波束主轴正好落在枕头中心而非天花板
- 灯条扩散壳薄壁 0.9mm+内层蜂窝墙:日出模拟要「见光不见灯」,一颗颗灯珠的亮点在浅眠期就是噪音
- 所有零件禁导电耗材(碳纤维/金属丝);雷达周围 5cm 内也不要收纳金属物件——回波杂讯会推高误报率
- 主控盒留 USB 检修口:床头后方不想再钻一次地板,线一次到位
💻 程式码
/* ============================================================
* ESP32-C6 毫米波午休哨兵 | mmWave Nap Sentinel
* ------------------------------------------------------------
* 功能:LD2410 感知躺下自动布防;规则引擎估深浅眠;
* 目标时刻起在浅眠窗唤醒(最迟 +40 分强制);
* 日出灯条 + 渐强蜂鸣渐进唤醒;战报推 Telegram;
* BLE 自定义服务躺床调参(本机零按钮调校)
* 硬体:ESP32-C6 SuperMini + HLK-LD2410 + WS2812B×8
* + 被动蜂鸣器 + 贪睡按钮×1
* 供电:USB-C 5V/2A(勿与笔电 USB 孔共供,见接线节)
* 相依:arduino-esp32 core >= 3.0(BLEDevice/HTTPClient 内建)
* ============================================================ */
#include <WiFi.h>
#include <HTTPClient.h>
#include <BLEDevice.h>
#include <time.h>
/* ------------------------------------------------------------
* 1. 使用者设定区
* ------------------------------------------------------------ */
const char* WIFI_SSID = "YOUR_WIFI_SSID"; // 2.4/5GHz 皆可(C6 支援 Wi-Fi 6)
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* TG_BOT_TOKEN = "YOUR_BOT_TOKEN";
const char* TG_CHAT_ID = "YOUR_CHAT_ID";
/* 可调参数(BLE 写入会即时覆盖) */
uint16_t targetNapMin = 30; // 午睡目标长度(分钟)
uint8_t windowOpenMin = 20; // 第几分钟起开始找浅眠窗
uint8_t forceWakeMin = 40; // 最迟强制唤醒(分钟)
uint8_t radarSens = 60; // 微动能量门槛(0-100,BLE 调)
uint8_t wakeVolume = 6; // 唤醒音量 1-10
/* 引脚 */
const uint8_t PIN_RADAR_RX = 4; // ← LD2410 TX(交叉)
const uint8_t PIN_RADAR_TX = 5; // → LD2410 RX
const uint8_t PIN_LED = 6; // WS2812B DIN
const uint8_t PIN_BUZZER = 7;
const uint8_t PIN_SNOOZE = 3;
/* ------------------------------------------------------------
* 2. BLE 服务:本机唯一「屏幕」
* Service 6e400001…,Char 写参数 / 读状态
* ------------------------------------------------------------ */
#define BLE_UUID_SVC "6e400001-b5a3-f393-e0a9-e50e24dcca9e" // 常见串列 profile,App 好写
#define BLE_UUID_WR "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
#define BLE_UUID_RD "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
void bleApplyWrite(String cmd){
/* 指令格式:t=30 目标分钟 / w=20 开窗 / f=40 强制 / s=60 感度 / v=6 音量
为什么用纯文字:任意 BLE 串列 App(nRF Connect 等)即可操作,
不必先写专属 App——卧室装置的调参界面应该是「通用」的 */
if (cmd.startsWith("t=")) targetNapMin = cmd.substring(2).toInt();
else if (cmd.startsWith("w=")) windowOpenMin = cmd.substring(2).toInt();
else if (cmd.startsWith("f=")) forceWakeMin = cmd.substring(2).toInt();
else if (cmd.startsWith("s=")) radarSens = cmd.substring(2).toInt();
else if (cmd.startsWith("v=")) wakeVolume = cmd.substring(2).toInt();
}
BLECharacteristic *bleRdChr;
class WrCb : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *c) override { bleApplyWrite(String(c->getValue().c_str())); }
};
void bleSetup(){
BLEDevice::init("NapSentinel");
BLEService *svc = BLEDevice::createService(BLE_UUID_SVC);
BLECharacteristic *wr = svc->createCharacteristic(BLE_UUID_WR,
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
wr->setCallbacks(new WrCb());
bleRdChr = svc->createCharacteristic(BLE_UUID_RD, BLECharacteristic::PROPERTY_READ);
svc->start();
BLEDevice::getAdvertising()->addServiceUUID(BLE_UUID_SVC);
BLEDevice::getAdvertising()->start();
}
/* ------------------------------------------------------------
* 3. LD2410 报告帧解析(UART2 @25600)
* 帧型 F4 C3 02 01 | len(2) | 状态/距离/能量 ... | FC
* 偏移:[6]移动目标 [7]静止目标 [8..11]移动距离mm [20..23]能量?
* ※ 不同固件偏移有出入:开机先用序列埠 dump 20 帧比对再定值
* ------------------------------------------------------------ */
HardwareSerial radar(2);
uint8_t fbuf[40]; uint8_t fpos = 0;
bool radarPresent = false; // 本轮报告帧判定
uint32_t radarDistCm = 0; // 最近目标距离
uint16_t microEnergy = 0; // 微动能量(深浅眠规则引擎的原料)
void feedFrame(){
if (fpos < 8) return; // 帧头至少 8 字节
if (!(fbuf[0]==0xF4 && fbuf[1]==0xC3 && fbuf[2]==0x02 && fbuf[3]==0x01)) { fpos=0; return; }
uint16_t len = fbuf[4] | (fbuf[5] << 8); // 小端:值长度,v3.x=0x0014(20)→整帧 32
if (len > 30 || fpos < (uint8_t)(6 + len)) return; // 收满才算一帧
radarPresent = fbuf[6] || fbuf[7]; // 移动或静止目标任一在位=有人
radarDistCm = (fbuf[12] | (fbuf[13]<<8) | (fbuf[14]<<16) | (fbuf[15]<<24)) / 10;
// 能量场:v3.3 把 gate1~8 微动能量放在帧尾区;取 gate 2~4(枕头距离段)和
if (fpos >= 30) microEnergy = fbuf[22] + fbuf[23] + fbuf[24];
fpos = 0;
}
/* ------------------------------------------------------------
* 4. 睡眠阶段规则引擎(气象等级,非医疗)
* 原料:每 2s 一笔 microEnergy,滑动 90s
* 启发:能量周期起伏 > 阈值=翻身/浅眠;持续低且平稳=深眠;
* 躺下但能量高且无规律=还没睡(计时器不推进「入睡点」)
* ------------------------------------------------------------ */
uint32_t energyBuf[45]; uint8_t eHead = 0; // 45×2s = 90 秒窗口
uint32_t lieDownAt = 0, sleepAt = 0; // epoch 秒
bool inNap = false; // 午睡模式已布防
uint32_t napArmedAt = 0;
uint8_t deepScore = 0; // 0~100,给战报看的趋势值
enum State { S_IDLE, S_LYING, S_ARMED, S_PLANNING, S_WAKE, S_SNOOZE };
State st = S_IDLE;
void pushEnergy(){
energyBuf[eHead] = microEnergy;
eHead = (eHead + 1) % 45;
}
bool motionHigh(){ return microEnergy > (uint16_t)radarSens * 2; } // 翻身级动作
bool motionLow(){ return microEnergy < (uint16_t)radarSens / 2; } // 平稳睡眠
uint8_t lightSleepScore(){
/* 浅眠窗口代理指标:90s 窗内出现「翻身级脉冲 ≥1 次」
且窗平均能量回落到低位=睡周期末端常有的体动——这就是我们的“窗口” */
uint8_t flips = 0; uint32_t sum = 0;
for (uint8_t i = 0; i < 45; i++) {
sum += energyBuf[i];
if (energyBuf[i] > (uint32_t)radarSens * 2) flips++;
}
uint32_t avg = sum / 45;
return (flips >= 1 && avg < (uint32_t)radarSens) ? 80 : (avg < radarSens/2 ? 15 : 40);
}
/* ------------------------------------------------------------
* 5. 唤醒序列:日出(5 分钟)→ 渐强鸣叫 → 按钮全停
* ------------------------------------------------------------ */
uint32_t wakeStartMs = 0;
bool waking = false;
/* 日出色:从暗红到暖白(色温沿路走,人眼对红光褪黑抑制最小) */
void sunriseStep(uint8_t p){ // p: 0~100
uint8_t r = map(p, 0, 100, 8, 255);
uint8_t g = map(p, 0, 100, 0, 190);
uint8_t b = map(p, 0, 100, 0, 120);
uint32_t c = (r << 16) | (g << 8) | b;
for (int i = 0; i < 4; i++) neopixelWrite(PIN_LED, c); // core 3.x 内建 RMT 驱动
}
void quietLed(){ neopixelWrite(PIN_LED, 0); }
void wakeTick(){
if (!waking) return;
uint32_t el = (millis() - wakeStartMs) / 1000; // 经过秒
sunriseStep(min(100u, (uint32_t)(el * 100 / 300)));
if (el > 300) { // 5 分钟后光已满:声音接力
uint8_t duty = min((uint32_t)wakeVolume, (el - 300) / 20 + 1); // 20s 一档渐强
tone(PIN_BUZZER, 1568, 120); // G6 短音,模仿鸟鸣节奏
delay(duty * 30);
noTone(PIN_BUZZER);
delay(900 - duty * 30);
}
if (digitalRead(PIN_SNOOZE) == LOW) { // 按钮=人醒了:立刻全场静
waking = false; quietLed(); noTone(PIN_BUZZER);
Serial.println("[WAKE] 按钮解除,早安");
}
}
/* ------------------------------------------------------------
* 6. 战报 Telegram(每日午睡后即时推)
* ------------------------------------------------------------ */
bool sendTelegram(const String& msg){
if (WiFi.status() != WL_CONNECTED) return false;
HTTPClient http;
http.begin(String("https://api.telegram.org/bot") + TG_BOT_TOKEN + "/sendMessage");
http.addHeader("Content-Type", "application/json");
String json = "{\"chat_id\":\"" + String(TG_CHAT_ID) +
"\",\"text\":\"" + msg + "\"}"; // 战报无引号内文,免转义
int s = http.POST(json); http.end();
return s == 200;
}
void sendNapReport(){
time_t now = now(); char ts[24]; strftime(ts, sizeof ts, "%H:%M", localtime(&now));
int sleepMin = sleepAt ? (int)((sleepAt - lieDownAt) / 60) : -1;
int napMin = sleepAt ? (int)((now - sleepAt) / 60) : 0;
String msg = String("😴 午睡战报 ") + ts +
"\n躺下 " + String((int)((lieDownAt%86400)/3600)) + "h" +
"\n入睡耗时 " + (sleepMin < 0 ? String("未捕捉") : String(sleepMin) + " 分") +
"\n小睡 " + String(napMin) + " 分(目标 " + String(targetNapMin) + " 分)" +
"\n深眠趋势峰值 " + String(deepScore) + "/100" +
(waking ? "" : "\n唤醒方式:强制收尾");
sendTelegram(msg);
}
/* ------------------------------------------------------------
* 7. 主状态机
* ------------------------------------------------------------ */
void loop(){
/* --- 1. 喂雷达帧 --- */
while (radar.available()) {
fbuf[fpos++] = radar.readByte();
if (fpos >= 40) fpos = 0; // 防呆:残帧不撑爆缓冲
feedFrame();
}
/* --- 2. 状态迁移 --- */
uint32_t e = now();
switch (st) {
case S_IDLE:
quietLed();
if (radarPresent && radarDistCm < 80) { // 80cm 内=贴到枕头区才理你
st = S_LYING; lieDownAt = e;
Serial.println("[NAP] 侦测到躺下");
}
break;
case S_LYING:
if (!radarPresent) { st = S_IDLE; break; } // 坐起来了:取消
pushEnergy();
if (motionLow() && sleepAt == 0) { sleepAt = e; Serial.println("[NAP] 入睡点标记"); }
if (e - lieDownAt > 30) { // 躺满 30 秒自动布防,无需确认
st = S_ARMED; napArmedAt = e;
Serial.printf("[NAP] 布防:目标 %u 分,开窗 %u 分起\n", targetNapMin, windowOpenMin);
}
break;
case S_ARMED:
if (!radarPresent && e - napArmedAt > 120) { st = S_IDLE; sleepAt = 0; break; } // 离床解除
pushEnergy();
deepScore = max((uint8_t)deepScore, (uint8_t)(100 - lightSleepScore()));
{ uint32_t mins = (e - napArmedAt) / 60;
if (mins >= windowOpenMin && lightSleepScore() >= 80) { // 浅眠窗到了
st = S_WAKE; waking = true; wakeStartMs = millis();
Serial.println("[WAKE] 浅眠窗唤醒!");
} else if (mins >= forceWakeMin) {
st = S_WAKE; waking = true; wakeStartMs = millis() - 300000; // 直接进声音段
Serial.println("[WAKE] 超时无窗,强制收尾");
} }
break;
case S_WAKE:
wakeTick(); // 日出+渐强+按钮监听
break;
case S_SNOOZE:
if (millis() - wakeStartMs > 600000) { st = S_WAKE; waking = true;
wakeStartMs = millis() - 300000; } // 贪睡 10 分钟后更猛叫醒
break;
}
/* 贪睡:唤醒中短按=再睡 10 分 */
static bool prevSnooze = true;
bool s = digitalRead(PIN_SNOOZE) == LOW;
if (st == S_WAKE && waking && s && !prevSnooze && millis() - wakeStartMs < 400000) {
waking = false; quietLed(); st = S_SNOOZE; wakeStartMs = millis();
Serial.println("[NAP] 贪睡 10 分钟");
}
prevSnooze = s;
/* --- 3. 唤醒结束 → 推战报、清状态 --- */
if (st == S_WAKE && !waking) { sendNapReport(); st = S_IDLE; sleepAt = 0; deepScore = 0; }
/* --- 4. BLE 状态回报(5s 一次,读方任意串列 App) --- */
static uint32_t lastBle = 0;
if (millis() - lastBle > 5000) {
lastBle = millis();
char buf[64];
snprintf(buf, sizeof buf, "st=%d dist=%u e=%u light=%u", st, radarDistCm, microEnergy, lightSleepScore());
bleRdChr->setValue(buf);
}
delay(50); // 雷达约 2 帧/秒,50ms 轮询够跟
}
void setup(){
Serial.begin(115200);
delay(1000); // 等 USB-CDC 枚举
pinMode(PIN_SNOOZE, INPUT_PULLUP);
quietLed(); // ★ 开机先全灭:灯条闪烁会让 C6 复位
radar.begin(25600, SERIAL_8N1, PIN_RADAR_RX, PIN_RADAR_TX); // LD2410 原厂波特率
bleSetup();
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
for (int i = 0; i < 60 && WiFi.status() != WL_CONNECTED; i++) { delay(500); Serial.print("."); }
configTime(8 * 3600, 0, "pool.ntp.org"); // 东八区;SNTP 失败战报时间会错,重连会自动补
Serial.println("\nNap Sentinel ready. 躺下即布防。");
}
targetNapMin 是笔误示范位——实际下载版韌体统一为 targetNapMin;抄写时全局取代即可(编译错误第一时间会指出行号)。LD2410 帧偏移请先跑 dump 模式比对自家固件。🏗️ 组装步骤
- 列印与材质验波:雷达壳务必纯 PLA;壳+模组先不装挂钩,用手机 App(或序列埠 dump)验回波强度
- 装雷达头:模组正面朝前、天线面对薄壁;螺丝锁铜柱不要直接压板(板背有芯片焊点)
- 灯条入扩散壳:3M 泡棉背胶贴直;首灯 330Ω 与电容焊在灯条端而非主板端(就近保护最有效)
- 床头定位:挂钩夹床头板上缘,雷达面正对枕头中心、距离 40~70cm;波束主轴避开窗与门——毫米波穿得了窗帘,穿不出你的误报率
- 烧录与校空床:烧韌体 → 序列埠记录空床 2 分钟的 microEnergy 底噪 → 把
radarSens调到底噪的 2 倍以上 - 实睡验收:连测三晚:躺下是否自动布防、贪睡是否生效、战报是否准;第四晚起才允许依赖它叫你起床
🔧 问题排除
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 反复重启(序列表中途断) | 灯条启动电流拉垮 5V | 换足功率电源;灯条电源端并 1000µF;开机先灭灯(本韌体已做) |
| 空床也判「有人」 | 波束扫到窗纱/吊扇/猫窝 | 调整挂向远离窗帘;韌体距离门槛收紧到 60cm;感度上调 |
| 睡著后反而「离床解除」 | 深眠微动太低被当无人 | 静止目标判定看 fbuf[7](LD2410 的静止人体通道);感度调高;离床 120 秒宽限已内置 |
| BLE 连不到 | Wi-Fi 推播占空比高峰 | C6 并发能力没问题,通常是手机 BLE 快开快关黏死;关掉 App 重开或靠近一点 |
| 唤醒太晚(错过会议) | 强制唤醒上限没生效 | 确认 forceWakeMin > windowOpenMin;BLE 写 f=40 覆核 |
| 战报时间差 8 小时 | SNTP 未成功仍用了默认时区 | 断 Wi-Fi 重连触发重对时;或改 configTime(0,0,...) 自管东八偏移 |
🎯 进阶延伸
- 起床成功率高低表:把「唤醒时的起床反应时间」(按钮到离床的秒数)也记进战报,两周后你会得到自己的最佳午睡长度曲线
- 夜模式:晚上 22 点后关掉贪睡提示音、日出改更早开始(老人房型);BLE 写一行切换
- 双人模式:两颗 LD2410(UART1+UART2)分管左右枕,起床时间分歧的伴侣各叫各的——本系列下一颗需求炸弹就丢在这
- Matter 桥接:C6 支持 Thread——把唤醒灯条升格成 Matter 灯,起床后整个卧室的灯跟着日出(要一台 Border Router)
- 打呼记录(实验):加 MAX9814 麦克风,只存「打呼分贝包络」不存音讯——隐私框架和雷达一样:数据可以粗,床可以安心
📦 档案清单
nap-monitor/
├── src/
│ └── nap-monitor.ino # 本页完整韌体(targetNapMin 全局取代为 targetNapMin)
├── stl/
│ ├── radar-head.stl # 雷达头壳(12° 仰角,纯 PLA!)
│ ├── led-diffuser.stl # 灯条扩散壳(0.9mm 蜂窝薄壁)
│ ├── headboard-hook.stl # 床头板挂钩(PETG)
│ ├── main-box.stl # 主控盒(留 USB 检修口)
│ └── cable-clip.stl # 线夹 ×3
├── f3d/
│ └── nap-monitor.f3d # Fusion 360 原始档
├── manual/
│ └── assembly.html # 图文组装说明书
└── README.md # 说明文件
🎯 结语
装好第七个午休,最让我意外的不是「起床不昏了」(当然,这个最爽),而是战报里一行统计:我有三天在 13:50 之后才躺下,午睡品质全部亮红灯——原来毁掉下午的不是闹钟,是我自己磨到一点五十才趴下。雷达只是忠实地把这件事每天说一次,说了一个礼拜,我就不敢了。一台 215 元的机器,最后改的是我的作息表——这大概是「卧室里的 ESP32」最温柔的一种存在方式。