📋 专案概述

我的午睡纪律曾经完美得可笑:手机闹钟 13:20,躺下 12:45——「睡 35 分钟刚刚好」。现实是:闹钟响的那一刻,我有一半的概率正躺在深度睡眠的正中央,被 80 分贝的手机铃声从里面凿出来。那种「醒来十分钟不知道自己是谁」的睡眠惰性(sleep inertia)比不睡还惨——整个下午的会议我都在回放那个没有逻辑的梦境,代码写出两个低级的 off-by-one,被 review 退回。

找过替代品,每一样都撞墙:

于是这台机器:无镜头、无穿戴、隔着一床被子和 50 公分的空气,用 24GHz 毫米波雷达「听」你的存在与微动——躺下自动布防,规则引擎粗估睡眠深浅,在你午睡目标时间的浅眠窗口(容许区间内)启动唤醒;如果一路深睡没窗口,最迟 40 分钟强制温柔收尾。全部材料 NT$215:

算一盘账:浅眠唤醒手环约 NT$1,000 起+戴著睡的代价;床垫带 NT$4,000 起。本机 215 元买到的是同一套「浅眠窗口」理念+你卧室里唯一一台物理上没有镜头的睡眠感测器。而且手环给你的是昨晚的分数,这台给你的是今天下午的清醒——睡眠资料的终极用途,是让你在需要清醒的时段清醒。

🩺 诚实声明:毫米波+规则引擎的深浅眠估计是「气象雷达等级」的推断,不是医疗级脑波分期。它的成功标准很简单——被叫醒后的前 30 分钟,你是否比手机闹钟时代更快进入状态。用两周再下结论。

为什么选 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 SuperMini1~55 元BLE 5+Wi-Fi 6,USB-C 烧录
雷达模组HLK-LD2410(24GHz 人体存在)1~90 元选 UART 版(4 针 GND/VCC/TX/RX);固件 v3.x 以上
唤醒灯条WS2812B 8 灯柔性条 5V1~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

🔌 接线图

ESP32-C6 SuperMini USB-C 供电 GPIO4 ● GPIO3 ● 5V/VIN ● GND ● ● GPIO5 ● GPIO6 ● GPIO7 LD2410 24GHz 雷达(镜头:无) TX RX VCC GND 贪睡按钮 WS2812B 暖白灯条 ×8 被动蜂鸣器 GPIO4 ← TX(交叉) GPIO5 → RX GPIO6 → DIN(串 330Ω) GPIO7 → BZ 5V→VCC GPIO3 → 按钮 → GND
▲ LD2410 与 C6 必须「TX↔RX 交叉」;雷达吃 5V 但 IO 电平 3.3V 容许直连(官方规格)。灯条电源单独由板端 5V 拉粗线,勿经过面包板跳线。

ASCII 对照表

ESP32-C6 SuperMini 周边 ┌─────────────┐ │ GPIO4 ●────┼── LD2410 TX(UART2 RX,交叉!) │ GPIO5 ●────┼── LD2410 RX(UART2 TX) │ GPIO6 ●────┼── WS2812B DIN(串 330Ω,1000µF 在灯条电源端) │ GPIO7 ●────┼── 蜂鸣器 I/O(tone 渐强) │ GPIO3 ●────┼── 贪睡按钮 → GND(内部上拉) │ 5V/VIN ●────┼── LD2410 VCC、灯条 5V(粗线直连) │ GND ●────┼── 全部共地(雷达地线务必短粗) └─────────────┘ ※ 避开 GPIO9(C6 strapping)与 GPIO12/24(BOOT 相关)做输出
⚠️ 灯条与雷达都吃 5V 脉冲电流。启动瞬间压降会让 C6 反复复位——这是本机唯一的「装机会自己咬自己」的坑:灯条电源端并 1000µF、韌体开机先默认全灭再连 Wi-Fi。

🖨️ 3D 列印零件

零件清单

零件数量材质建议颜色列印时间
雷达头壳(12° 仰角、正面对枕)1PLA(一般)雾灰~1.2 hr
灯条扩散壳(0.9mm 薄壁+蜂窝导光)1PLA白(透光)~50 min
床头板挂钩(免钻孔)1PETG任意~25 min
主线夹 ×3/按钮侧壳4PLA雾灰~20 min
主控盒(塞床头后侧)1PLA黑~40 min

列印设定与电磁纪律

💻 程式码

📐 註解规范:区块以框注分段;函式以框注说明用途/参数/回传;行内註解解释「为什么」。LD2410 各家批次固件略有差异,本篇以 v3.3 的 32 字节报告帧为准;解析器采「找帧头+按偏移取值」,兼容 26 与 32 两种帧长。无外部函式库(BLEDevice/NeoPixel 皆 core 3.x 内建)。
/* ============================================================
 *  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 模式比对自家固件。

🏗️ 组装步骤

📘 图文版组装说明书:每一步附示意图、预估时间与难度标记,可直接列印 A4 带到工作桌 → 开启 HTML 组装说明书
  1. 列印与材质验波:雷达壳务必纯 PLA;壳+模组先不装挂钩,用手机 App(或序列埠 dump)验回波强度
  2. 装雷达头:模组正面朝前、天线面对薄壁;螺丝锁铜柱不要直接压板(板背有芯片焊点)
  3. 灯条入扩散壳:3M 泡棉背胶贴直;首灯 330Ω 与电容焊在灯条端而非主板端(就近保护最有效)
  4. 床头定位:挂钩夹床头板上缘,雷达面正对枕头中心、距离 40~70cm;波束主轴避开窗与门——毫米波穿得了窗帘,穿不出你的误报率
  5. 烧录与校空床:烧韌体 → 序列埠记录空床 2 分钟的 microEnergy 底噪 → 把 radarSens 调到底噪的 2 倍以上
  6. 实睡验收:连测三晚:躺下是否自动布防、贪睡是否生效、战报是否准;第四晚起才允许依赖它叫你起床

🔧 问题排除

问题原因解决方法
反复重启(序列表中途断)灯条启动电流拉垮 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,...) 自管东八偏移

🎯 进阶延伸

📦 档案清单

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」最温柔的一种存在方式。

← 上一篇:ESP32-C3 国际象棋比赛钟 📚 系列索引 下一篇:ESP32 全系列 × 3D 列印系列索引 →