📋 專案概述
姍姍八個月大,每晚七點的洗澡時間都是她媽媽的焦慮表演:手肘內側試水溫試了三分鐘還在猶豫——水太熱怕燙傷,太涼怕著涼,而且澡盆就那麼大,水溫每分鐘掉 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 蜂鳴警報;一顆電池用半年 |
功能清單
- 非接式測水溫:MLX90614ESF-BAA(±0.5°C、0.02°C 解析度、90° 視場,Melexis 官方規格),對水面 5–8cm 量測,鏡片永不碰水
- 發射率補償內建:出廠預設 ε=1.0,韌體以 T⁴ 公式+環境溫補正回水面真實溫度(ε=0.95)
- 四色直覺:藍 <36°C(加水)、綠 36–39.5°C(正好)、黃 40–41°C(注意)、紅 >41°C+蜂鳴警報
- 夾合開工/收起睡覺:澡盆夾內嵌磁鐵、本體裝干簧管——夾上喚醒每 5 秒一測,取下 30 秒後深睡(ESP32-C2 深睡典型 5µA,ESP8684 datasheet)
- 蒸汽防呆:連續讀值波動 >2°C 判定鏡片起霧,黃燈快閃提示擦拭,不拿髒鏡片嚇人
- 低電提醒:鋰電 <3.45V 紅燈慢閃,USB-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 支。
💻 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。