ESP32 全系列 × 3D 列印 · 59/59

ESP32-C2 嬰兒洗澡水溫哨兵:非接式測溫+夾合深睡

📅 2026-11-08 · 🏷️ 數位科技 · 📂 ESP32 全系列 × 3D 列印系列 · 59/59 · ⏱️ 實作約 2 小時(不含列印)

📋 專案概述

姍姍八個月大,每晚七點的洗澡時間都是她媽媽的焦慮表演:手肘內側試水溫試了三分鐘還在猶豫——水太熱怕燙傷,太涼怕著涼,而且澡盆就那麼大,水溫每分鐘掉 0.3°C,前一分鐘剛好後一分鐘就涼了。

市售解法全都泡在湯裡:數位洗澡溫度計 NT$250,探頭整支浸在洗澡水中,三個月後縫隙長黴斑;感溫湯匙更扯,等它讀完數姍姍已經開始哭。月嫂借來的工業紅溫槍更離譜——對著水面測出 52°C,被婆婆當做亂搞,其實只是蒸汽和水漬讓讀值上竄下跳。

醫學共識其實很明確:嬰兒洗澡水溫建議 38–40°C、浴室室溫 26–28°C(人民日報健康客戶端轉載之 2023 照護指引;美國婦產產科護理協會 AWHONN 同建議 38–<40°C)。要盯住這個狹窄窗口,不需要浸水的探頭——水面本身就是最理想的紅外目標:發射率 0.95,幾乎是黑體(ThermoWorks 發射率表)。

方案價格問題
數位洗澡溫度計NT$250探頭泡水=細菌培養槽;電池艙漏液;半年失準
感溫湯匙NT$180讀取慢,攪拌不均時誤差 ±2°C
工業紅溫槍NT$500+光斑大、無蒸汽抗擾、對新手爸媽只會製造恐慌
本專案≈ NT$650全程不碰水;夾上澡盆自動開工、收起自動睡覺;41°C 蜂鳴警報;一顆電池用半年

功能清單

為什麼選 ESP32-C2:這顆的規格表上只有兩件事要命:便宜與省電。C2 SuperMini 是全家最便宜的 Wi-Fi 板(網拍 65–99 元),而 ESP8684 晶片深睡典型電流 5µA、停機 1µA(Espressif datasheet,實測模組級 8.14µA)——待機佔 99% 的電池裝置裡,這數字直接決定半年還是三個月換一次電。Wi-Fi 留在韌體裡當備援(未來可推播給保母),本版先用 LED 做人機介面。C2 的 GPIO 少,但本專案只用了 6 支。

🧱 硬體清單

元件數量單價備註
ESP32-C2 SuperMini165 元ESP8684:深睡典型 5µA/停機 1µA(datasheet)
MLX90614 GY-906 模組1380 元大勝光電 NT$380 含運(2026-11);BAA 型 90° FOV、物溫 −70~380°C
共陽 RGB LED 5mm110 元透光燈罩下三顆 220Ω
壓電蜂鳴器(3.3V 源式)120 元源式:GPIO 拉低即響,省一顆電晶體
干簧管+M2 磁鐵1 組25 元夾合偵測=開關,氣隙 ≤6mm
鋰聚合物 402530(500mAh)1120 元洗澡間歇使用估算 ≳180 天
TP4056+保護板模組130 元Rprog 改 10kΩ=100mA 慢充養電池
總計≈ NT$650其中感測器佔 6 成——紅外鏡頭沒有真的便宜過

價格為 2026 年 11 月臺灣供應商與蝦皮實價(大勝光電、網拍行情)。MLX90614 規格引 Melexis 官方 datasheet:標準版精度 ±0.5°C、解析 0.02°C、出廠以黑體雙點校準。

這顆是「輔助判讀」不是「醫療器材」:紅外測的是水面溫度,水面與皮膚實際接觸溫度會因蒸發與對流差 0.5–1°C。下水前仍要用手肘或攪拌後複測一次——哨兵的角色是「每 5 秒把風」,不是免死金牌。嬰兒皮膚 44°C 持續數小時即有燙傷之虞,45°C 以上分鐘計,警報聲響請當真。

🔌 接線圖(ESP32-C2 SuperMini 腳位)

ESP32-C2 SuperMini × 水溫哨兵 接線圖 ESP32-C2 SuperMini 4 SDA5 SCL2 REED3 BUZZ6 RED7 GRN8 BLU 3V3GNDVBAT(經TP4056) GY-906(MLX90614) SDA←GPIO4 SCL←GPIO5 VCC=3V3 自帶上拉電阻 鏡片朝澡盆水面 5–8cm 干簧管(夾合=GND) GPIO2 對 GND,內部上拉 下降沿 EXT_WAKEUP 喚醒深睡 共陽 RGB LED 公共腳→3V3 紅←220Ω←GPIO6 綠←220Ω←GPIO7 藍←220Ω←GPIO8 蜂鳴器(源式) +→3V3 −→GPIO3(拉低響) 電源鏈:鋰聚合物 402530 → TP4056+保護板(Rprog 10k=100mA)→ 板 5V 腳(內建 LDO→3V3) USB-C 充電孔外露於電池艙蓋;深睡瓶頸是板載 LDO 靜態電流(≒50µA),選 SuperMini 已是最小者 I2C 上拉在 GY-906 板載,勿再加外部上拉(並聯會讓上升沿過衝)
圖 1:接線圖(自製 SVG)。全部零件都是 3.3V 準位,無電平轉換。
C2 腳位連接備註
GPIO4 / GPIO5I2C SDA / SCL(GY-906)地址 0x5A;模組自帶上拉
GPIO2干簧管→GND內部上拉;下降沿喚醒(深睡來源)
GPIO6 / 7 / 8R/G/B(經 220Ω)共陽:GPIO 拉低點亮
GPIO3蜂鳴器(源式)LOW=響;靜默時維持 HIGH
5V / GNDTP4056 BAT 端充電時 USB 供電與電池共存,無需在線上開關

🖨️ 3D 列印設計

零件數量材質設定
主體殼(透光燈罩+感測窗)1PETG0.16mm、頂底 4 層、20%;燈罩區用透光白 PETG
澡盆夾臂(彈夾 22–45mm,內嵌 M2 磁鐵)2PETG0.2mm、50%(彈簧臂要韌,PLA 會脆斷)
感測窗壓圈(固定 GY-906,鏡片外露 1mm 不積水)1PETG0.2mm、實心
電池艙蓋(USB-C 孔外露)1PETG0.2mm、15%、四角卡扣

設計重點

💻 Arduino 韌體

先讀:📖 圖文組裝說明書(6 步驟 →)。板型選「ESP32C2 Dev Module」,Flash size 1MB。校準步驟別跳過:先與水銀溫度計同場對讀三次再上線。
/* ============================================================
 * 嬰兒洗澡水溫哨兵 v1.0
 * 功能:MLX90614 非接式測水面(發射率 0.95 補償)、RGB 四色
 *       判讀、41°C 蜂鳴警報、蒸汽抖動防呆、干簧管夾合喚醒、
 *       30 秒無夾合深睡(typ 5µA)、低電閃燈
 * 硬體:ESP32-C2 SuperMini、GY-906(I2C 0x5A)、共陽 RGB、
 *       源式蜂鳴器、干簧管、LiPo 402530+TP4056
 * 供電:3.7V 鋰聚 → 板載 LDO;深睡喚醒源=GPIO2 下降沿
 * 相依:Wire(核心內建)——MLX90614 直接讀暫存器,不需第三方庫
 * ============================================================ */
#include <Wire.h>
#include <driver/rtc_io.h>

/* ---------- 1. 腳位與參數 ---------- */
#define PIN_REED    2      // 干簧管(夾合=GND)
#define PIN_BUZZ    3      // 蜂鳴器(源式,LOW=響)
#define PIN_RED     6
#define PIN_GRN     7
#define PIN_BLU     8
#define MLX_ADDR    0x5A
#define REG_TOBJ    0x06   // 物溫(出廠 ε=1.0)
#define REG_TAMB    0x07   // 模組環境溫
#define BAT_ADC     0      // C2 SuperMini 電池分壓於 GPIO0(經 TP4056 BAT)

const float EMISSIVITY = 0.95f;           // 水面發射率(ThermoWorks 表)
const float T_COLD = 36.0, T_GOOD_LO = 36.0, T_GOOD_HI = 39.5;
const float T_WARN_HI = 41.0, T_WARN_CLEAR = 40.2;  // 警報遲滯帶
const float CAL_OFFSET = 0.0f;            // 校準補正值(步驟見說明書)
const uint32_t ACTIVE_INTERVAL_MS = 5000; // 使用模式量測節拍
const uint32_t IDLE_SLEEP_MS = 30000;     // 脫離夾合多久後深睡

/* ---------- 2. 全域 ---------- */
float lastObjC = NAN, lastAmbC = NAN;
uint8_t hotStreak = 0, fogStreak = 0;
float recent[10]; uint8_t rIdx = 0; bool rFull = false;

/* ---------- 3. MLX90614 ---------- */
/* 用途:由 SMBus 標準幀讀 16-bit 暫存器(含 PEC 略過,短線可靠)
 * 參數:reg 暫存器址;回傳:°C(失聯回 NAN) */
float mlxRead(uint8_t reg) {
  Wire.beginTransmission(MLX_ADDR);
  Wire.write(reg);
  if (Wire.endTransmission(false) != 0) return NAN;   // 不重複起始
  uint8_t lo = Wire.requestFrom(MLX_ADDR, (uint8_t)1);
  uint8_t hi = Wire.requestFrom(MLX_ADDR, (uint8_t)1);
  int16_t raw = (int16_t)((hi << 8) | lo);
  if (raw == 0 || raw == (int16_t)0x8000) return NAN; // 出廠空碼/錯誤碼
  return raw * 0.02f - 273.15f;                       // 0.02°C 解析度
}

/* 用途:發射率補償——出廠以 ε=1 校準,補回 ε=0.95 的真實物溫
 * 原理:T⁴ 線性疊加,T_real = [T_obj⁴ + (1/ε−1)·T_amb⁴]^0.25(開氏)
 * 參數:tObj/tAmb 攝氏;回傳:補正後物溫 °C */
float emissivityFix(float tObj, float tAmb) {
  double o = tObj + 273.15, a = tAmb + 273.15;
  double fixed = pow(pow(o, 4) + (1.0 / EMISSIVITY - 1.0) * pow(a, 4), 0.25);
  return (float)fixed - 273.15f;
}

/* 用途:取樣一輪——10 個樣本取中位數抗蒸汽脈衝
 * 回傳:補償後水溫(失敗 NAN) */
float sampleWater() {
  float buf[10]; uint8_t n = 0;
  for (uint8_t i = 0; i < 10; i++) {
    float o = mlxRead(REG_TOBJ), a = mlxRead(REG_TAMB);
    if (!isnan(o) && !isnan(a)) buf[n++] = emissivityFix(o, a);
    delay(60);                                     // 8 次/秒上限內
  }
  if (n < 6) return NAN;                           // 半數以上失聯=I2C 異常
  /* 排序取中位 */
  for (uint8_t i = 0; i < n - 1; i++)
    for (uint8_t j = i + 1; j < n; j++)
      if (buf[j] < buf[i]) { float t = buf[i]; buf[i] = buf[j]; buf[j] = t; }
  return buf[n / 2] + CAL_OFFSET;
}

/* ---------- 4. 判讀與輸出 ---------- */
/* 用途:蒸汽防呆——最近 10 次讀值極差 >2°C 判定鏡片起霧
 * 回傳:true=鏡片髒/霧,本次判讀不作數 */
bool fogDetected() {
  recent[rIdx] = lastObjC;
  rIdx = (rIdx + 1) % 10; if (rIdx == 0) rFull = true;
  uint8_t cnt = rFull ? 10 : rIdx;
  if (cnt < 5) return false;
  float mn = 999, mx = -999;
  for (uint8_t i = 0; i < cnt; i++) { mn = min(mn, recent[i]); mx = max(mx, recent[i]); }
  return (mx - mn) > 2.0f;
}

/* 用途:驅動 RGB+蜂鳴器;警報需連 3 次才響(防蒸汽誤報) */
void showState(float t) {
  bool fog = fogDetected();
  if (fog) {                                       // 黃燈快閃=擦鏡片
    for (uint8_t i = 0; i < 6; i++) {
      digitalWrite(PIN_RED, LOW); digitalWrite(PIN_GRN, LOW); delay(80);
      digitalWrite(PIN_RED, HIGH); digitalWrite(PIN_GRN, HIGH); delay(80);
    }
    return;
  }
  if (isnan(t)) { digitalWrite(PIN_BLU, LOW); delay(400); digitalWrite(PIN_BLU, HIGH); return; }
  if (t > T_WARN_HI) hotStreak++; else if (t < T_WARN_CLEAR) hotStreak = 0;
  uint8_t r = HIGH, g = HIGH, b = HIGH;
  if      (t < T_COLD)      b = LOW;               // 藍:太涼,加熱水
  else if (t <= T_GOOD_HI)  g = LOW;               // 綠:36–39.5 正好
  else if (t <= T_WARN_HI)  r = LOW, g = LOW;      // 黃:40 出頭,盯著點
  else                      r = LOW;               // 紅:過熱
  digitalWrite(PIN_RED, r); digitalWrite(PIN_GRN, g); digitalWrite(PIN_BLU, b);
  if (hotStreak >= 3) {                            // 41°C 連 3 次(15s)才開麥
    for (uint8_t i = 0; i < 8; i++) { digitalWrite(PIN_BUZZ, LOW); delay(120);
                                      digitalWrite(PIN_BUZZ, HIGH); delay(120); }
  }
}

/* ---------- 5. 深睡 ---------- */
/* 用途:夾合鬆開 IDLE_SLEEP_MS 後進入深睡
 * 喚醒:GPIO2 下降沿(再次夾上澡盆);喚醒後 cold reset 重跑 setup */
void gotoSleep() {
  digitalWrite(PIN_RED, HIGH); digitalWrite(PIN_GRN, HIGH); digitalWrite(PIN_BLU, HIGH);
  esp_deep_sleep_enable_gpio_wakeup(1ULL << PIN_REED, ESP_GPIO_WAKEUP_GPIO_LOW);
  Serial.println("sleep...");
  esp_deep_sleep_start();
}

/* ---------- 6. setup / loop ---------- */
void setup() {
  Serial.begin(115200);
  pinMode(PIN_REED, INPUT_PULLUP);
  pinMode(PIN_RED, OUTPUT); pinMode(PIN_GRN, OUTPUT); pinMode(PIN_BLU, OUTPUT);
  pinMode(PIN_BUZZ, OUTPUT); digitalWrite(PIN_BUZZ, HIGH);
  Wire.begin(4, 5);                                 // SDA=4 SCL=5
  delay(1500);                                      // GY-906 上電熱穩定(官方建議 1s+)
}

void loop() {
  /* 干簧管斷開(夾子張開)=收工倒數 */
  if (digitalRead(PIN_REED) == HIGH) {
    static uint32_t openedAt = 0;
    if (openedAt == 0) openedAt = millis();
    if (millis() - openedAt > IDLE_SLEEP_MS) gotoSleep();
    delay(200);
    return;
  }
  unsigned long t0 = millis();
  lastObjC = sampleWater();
  showState(lastObjC);
  Serial.printf("水溫 %.2f°C(環境 %.1f)\n", lastObjC, lastAmbC);
  /* 節拍補齊:量測+閃燈耗時不計入 5 秒間隔,保持節奏穩定 */
  long left = (long)ACTIVE_INTERVAL_MS - (long)(millis() - t0);
  if (left > 0) delay(left);
}
設計決策:① 中位數而非平均數:蒸汽是「脈衝型」幹擾,偶爾一跳 +6°C,平均數會被污染、中位數免疫。② 警報要連 3 次才響:誤報 3 次,爸媽就會把哨兵收進抽屜——告警器的信用比精度重要。③ 深睡採 cold reset:ESP32 深睡喚醒本來就重跑 setup(),乾脆把狀態機全放 RAM 外,喚醒即全新量測,不留殭屍狀態。④ 不接 Wi-Fi 是刻意的:待機多一個 Beacon 收聽就多 10µA 級漏電,推播功能留給有插電場景的 v2。

🏗️ 組裝步驟

完整圖文版:6 步驟 IKEA 組裝說明書 →(含電池綁紮、感測窗對位、三點校準)
  1. 列印 4 種 PETG 零件;透光燈罩區確認無層紋白霧
  2. 麵包板接線→對一杯 38°C 溫水,讀值與水銀溫度計比對,記下補正值寫入 CAL_OFFSET
  3. 主機鎖入主體殼、GY-906 壓圈固定(鏡片朝窗前 5mm)、夾臂卡入並嵌磁鐵
  4. 電池入艙、負極先接、理線後合蓋;USB-C 插起測試充電指示燈
  5. 夾上澡盆乾跑 2 分鐘:綠燈恆亮、節拍 5 秒;張開夾子 30 秒後電流表應掉到 <100µA
  6. 第一次真洗澡:手肘複測同時記錄哨兵讀值,偏差 >1°C 重跑校準

🔧 問題排除

症狀根因解法
讀值比溫度計高 3°C 以上蒸汽附著鏡片/背景有暖燈直射水面鏡片擦拭、調整夾角避開燈源;fogDetected 會閃黃燈提示
讀值固定偏低 1.5–2°C未套發射率補償(ε=1.0 直接讀)確認 emissivityFix() 有啟用、ε=0.95
放著一週就沒電干簧管被抽屜裡的磁鐵誘發、整晚沒睡收納位離磁鐵 5cm 以上;或用膠帶覆夾臂感應面
夾上澡盆不開工磁鐵與干簧管氣隙 >6mm 或上下反向夾臂內側標記點對齊;氣隙壓到 3–4mm
蜂鳴器不響買到灌式(高準位響)當源式接靜默態改 HIGH 試之;或並 1N4148 反相
RGB 顏色混色怪共陽/共陰買錯(共陰拉高亮)三腳量對二極體壓降後反轉邏輯
I2C 偶爾讀 NAN浴室接點氧化+線長 >15cm接點抹接點油;線縮到 10cm 內、時脈降 100kHz

🚀 進階應用

📦 檔案清單

c2-bath-temp-sentry/
├─ c2-bath-temp-sentry.ino       ← 本頁完整韌體(Wire 直讀暫存器,零第三方庫)
├─ README.md                     ← 三點校準紀錄表與踩坑
└─ prints/
   ├─ sentry_body.petg.stl           # 主體殼(透光燈罩)
   ├─ clamp_arm_left.petg.stl        # 澡盆夾臂 ×2(內嵌 M2 磁鐵)
   ├─ mlx_window_ring.petg.stl       # 感測窗壓圈
   └─ battery_cover.petg.stl         # 電池艙蓋(USB-C 孔)