📋 專案概述
我的午睡紀律曾經完美得可笑:手機鬧鐘 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」最溫柔的一種存在方式。