📋 專案概述
去年秋天我打開浴室燈,天花板角落那一片蜘蛛網狀的黑黴第一次讓我認真計算一件事:台灣一間標準衛浴除黴+磁磚縫重新拷膠的行情是 3,000~8,000 元,而整件事的起因只是「排風扇有人記得開、沒人記得關」。洗澡後鏡面起霧只是表象——真正的傷害是牆面與矽利康在 75~95% 相對濕度裡泡的那兩三個鐘頭,黴變反應在这个區間跑得最歡。
市售方案我用了兩套,都翻車:
- 濕度感應排風模組(600~900 元):邏輯是「濕度超標→轉、到達→停」。問題在於它完全不知道人在不在——泡澡時濕度爬到最高點、也是你最需要排風的時刻,它常常剛好選擇停機休息;而且它只看當下濕度,回南天那種 68% 悶整天的「不超標但一直在發黴」狀態,它完全無感。
- 定時插座:洗澡時間從來不準時,本質上是賭博。
這個專案用 ESP32-S3 + 毫米波雷達 + 高精度濕度計,把排風扇交給三個互相兜底的判據:
- 人在就轉:LD2410 毫米波抓到移動(淋浴的小動作也算),人進去 2 秒內排風啟動,不用等濕度慢慢爬
- 濕度滯回:RH ≥75% 強制排、一路降到 60% 才停——停在 70% 以下,是把牆面帶離黴變甜蜜點,不是「感覺乾了」就好
- 後吹乾燥 12 分鐘:人離開後再多轉 12 分鐘趕走牆面殘留水汽——這 12 分鐘是整台裝置的價值核心,市售款最捨不得給的就是它
- 潮浸積分日報:每天累計「RH>70% 秒數」,超過門檻就 Telegram 提醒你開除濕/開窗,把「會不會發黴」變成一個可以被管理的數字
- WS2812 環燈語言:青色呼吸=除濕中、綠=有人在、藍=乾燥安全、紅=潮浸超標;夜間自動滅燈
成本帳:一台濕度感应排風模組 600~900 元、外加除黴工班 3,000 元起跳;本專案 613 元,而且韌體在你手上——滯回區間、後吹時間、日報文案,全是可以改一行的事。
為什麼選 ESP32-S3?
| 需求 | S3 的優勢 |
|---|---|
| 多外設同時跑 | 環燈呼吸動畫(NeoPixel DMA 刷新)、I2C 取樣、HTTPS 推播同時進行——雙核心把動畫釘在 core1、Wi-Fi 協定堆疊獨佔 core0,環燈永遠不卡頓 |
| 恆供電裝置 | 本機接浴室燈電源降 5V,µA 級低功耗用不上——要的是餘裕不是省電,同價位的 C2 在此就是白省 |
| 安裝環境 | 原生 USB 直接從殼底检修孔燒錄;腳位餘裕大到把雷達、濕度、SSR、環燈全接完還剩一組 UART |
| 延伸預留 | S3 是全系列唯一帶攝像頭併口的型號——日後加顆 OV2640 用視覺比對黴斑面積,主機不用換 |
🧱 硬體清單
| 項目 | 型號 | 數量 | 預估單價 | 備註 |
|---|---|---|---|---|
| 主控板 | ESP32-S3-DevKitC-1(N16R8) | 1 | ~165 元 | N8R2 亦可;本專案不吃 PSRAM,選便宜的可再壓 30 元 |
| 存在感測 | 海凌科 LD2410 毫米波雷達 | 1 | ~160 元 | 買帶 OUT 腳的原版 LD2410;LD2410C 無 OUT 腳需改走 UART(見進階延伸) |
| 濕度感測 | SHT40 模組(I2C) | 1 | ~85 元 | ±1.8%RH,比 DHT22 準一個等級且不吃單線時序——浴室潮氣環境線路要越簡單越好 |
| 風負載開關 | 固態繼電器模組(SPC5821 過零型) | 1 | ~65 元 | 排風扇是電感性負載,過零 SSR 壽命是繼電器的 3~10 倍且無接點彈跳噪音 |
| 狀態指示 | WS2812B-49 環形燈板(直徑 60mm) | 1 | ~45 元 | 貼面蓋內側當「狀態瞳孔」,磨砂面蓋同時是擴散板 |
| 電源 | 5V 1A USB 降壓模組(燈座取電) | 1 | ~38 元 | 從浴室燈燈座並下 5V;不動強電者改走天花板检修口拉 USB 線 |
| 外殼底基 | 86 壁裝盒 + 3D 列印適配鎖套 | 1 | ~30 元 | 利用既有開關孔位走線,不倒漆不鑽新孔 |
| 雜項 | 端子台、防滴水面蓋、雙面膠、熱縮管 | 1 | ~25 元 | 面蓋預留 SHT40 進氣縫與雷達窗口 |
| 合計 | ~613 元 |
🔌 接線圖
ASCII 對照表
ESP32-S3 DevKitC-1 LD2410 / SHT40 / SSR / 環燈
───────────────── ─────────────────────────
5V ──── 紅線 ──── LD2410 VCC、環燈 5V
GND ──── 黑線 ──── LD2410 GND、SHT40 GND、SSR IN-、環燈 GND
GPIO7 ──── 黃線 ──── LD2410 OUT(有人=HIGH)
GPIO8 ──── 藍線 ──── SHT40 SDA
GPIO9 ──── 藍線 ──── SHT40 SCL
GPIO15 ──── 綠線 ──── SSR IN+(HIGH=風扇轉)
GPIO48 ──── 橘線 ──── WS2812 環燈 DIN
(LD2410 TX/RX 主線路不接,出廠 OUT 即為「移動=HIGH」;要微動靈敏度再並 UART 調參)
※ SHT40 用 3V3 供電,讀值直接對 3.3V 域;SSR 輸入側內部已有限流電阻,GPIO 直推
※ 110V 側:插座火線 → SSR「~」,SSR「~」→ 風扇火線;中性線直通不進 SSR
詳細接線
| ESP32-S3 腳位 | 連接 | 說明 |
|---|---|---|
| GPIO7 | LD2410 OUT | S3 任何 GPIO 皆可中斷喚醒與讀取;此腳只有雷達在用,預留上拉即可免焊 10k |
| GPIO8 / GPIO9 | SHT40 SDA / SCL | S3 的 Arduino 預設 I2C 腳,免 setPins;模組自带 10k 上拉,線長 30cm 內免外加 |
| GPIO15 | SSR IN+ | 過零型 SSR 輸入 3–32VDC,3.3V 邏輯直推沒問題;IN- 回 GND(低壓域共地) |
| GPIO48 | WS2812 DIN | 環燈 5V 供電但資料是 3.3V 位準——S3 GPIO 直接安全;上電瞬間環燈會閃一下屬正常,程式首幀先全滅 |
| 5V / GND | 燈座降壓模組 | 降壓模組鎖在燈座天花板側,USB 線穿检修口下來——浴室裡不出現任何裸露 110V |
⚠️ 強電三步鐵律:①切斷浴室燈斷路器後再用驗電筆確認才動線;②SSR 兩端 110V 全部鎖進阻燃盒、鎖好才通電;③第一次通電測風扇要用不含人的遠端測試(插馬桶沖水開關之類去碰開關都別笑——溼手碰強電才是本專案唯一會死人的劇本)。
🖨️ 3D 列印零件
零件清單
| 零件 | 數量 | 材質建議 | 顏色 | 列印時間 |
|---|---|---|---|---|
| 防滴主殼(倒扣式) | 1 | PLA | 白 | ~3 hr |
| 透波面蓋(雷達窗+進氣縫) | 1 | PLA(0° 圓盤紋) | 半透白 | ~45 min |
| 環燈導光圈 | 1 | PLA(磨砂面朝內) | 白 | ~20 min |
| 86 盒適配鎖套 | 2 | PETG(鎖孔受力件) | 白 | ~15 min×2 |
| SSR 阻燃盒適配蓋 | 1 | PETG | 黑 | ~25 min |
列印設定
- 材質:浴室頂部環境溫不超过 40°C,PLA 足夠;但鎖孔與受潮環境的 86 鎖套用 PETG——PLA 長期高濕環境會吸濕變韌變脆反覆交替,受力件不賭
- 面蓋用 0° 圓盤紋(concentric)列印:直線紋的縫隙會讓水氣直進 SHT40 進氣縫,圓盤紋把擴散路徑變成迷宮
- 層厚:主殼 0.2、面蓋 0.16 加四層實心頂(雷達窗 1.6mm 厚度對 24GHz 衰減約 0.8dB,實測比 ABS 好透波)
- 進氣縫 4×1mm ×6 條:SHT40 感測窗正下方,縫口朝下防淋浴水簾
設計要點
- 雷達窗正對淋浴區:面蓋上無開孔(防水),24GHz 直接穿 1.6mm PLA;LD2410 天線面貼窗內側,外殼-metal 件(螺絲)離天線 10mm 以上
- 散熱欄柵朝下+滴水彎道:殼體進出氣走 L 形迷宮,淋浴水霧進不來、SHT40 讀的是被動擴散的室溫濕度而不是淋到的水
- 電路板豎插導槽:DevKitC-1 長邊插進導槽再鎖兩顆 M3,检修時整片「抽屜式」抽出,不用拆殼翻線
- 環燈導光圈懸浮卡裝:燈環貼圈內側、圈卡進面蓋凹槽——狀態燈從整個面蓋透出,看不出有任何 LED 顆粒
💻 程式碼
📐 註解規範:區塊以框註分段;函式以框註說明用途/參數/回傳;行內註解一律解釋「為什麼」而非「做什麼」。全系列文章採同一規範。
📚 相依:板子選「ESP32S3 Dev Module」,USB CDC On Boot: Enabled;Lib Manager 裝
📚 相依:板子選「ESP32S3 Dev Module」,USB CDC On Boot: Enabled;Lib Manager 裝
Adafruit SHT4X Library + Adafruit NeoPixel。主迴路跑在 core1、Wi-Fi 堆疊在 core0(ESP32 Arduino 預設配置),環燈動畫不吃中斷延遲。/* ============================================================
* ESP32-S3 浴室潮氣哨兵 | Bathroom Moisture Guard
* ------------------------------------------------------------
* 功能:毫米波人在即排風 + SHT40 濕度滯回 + 後吹乾燥 12 分
* + 潮浸積分日報(Telegram)+ WS2812 狀態環燈
* 硬體:ESP32-S3 DevKitC-1 + LD2410(OUT=GPIO7) + SHT40(GPIO8/9)
* + 過零SSR(GPIO15) + WS2812環燈(GPIO48, 49顆)
* 供電:燈座降壓 5V → USB-C(恆供電,無深睡)
* 相依:Arduino core for ESP32 v3.x / Adafruit_SHT4X / Adafruit_NeoPixel
* 設計核心:發黴的充分條件是「濕度停留時間」,不是瞬間峰值——
* 所以本機同时管「現在」(三判據排風)與「昨天」(潮浸積分)。
* ============================================================ */
#include <WiFi.h>
#include <HTTPClient.h>
#include <time.h>
#include <Wire.h>
#include <Adafruit_SHT4X.h>
#include <Adafruit_NeoPixel.h>
/* ------------------------------------------------------------
* 1. 使用者設定區
* ------------------------------------------------------------ */
const char* WIFI_SSID = "YOUR_WIFI_SSID"; // 僅 2.4GHz
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* TG_BOT_TOKEN = "YOUR_BOT_TOKEN";
const char* TG_CHAT_ID = "YOUR_CHAT_ID";
/* ------------------------------------------------------------
* 2. 腳位
* ------------------------------------------------------------ */
const uint8_t PIN_RADAR = 7; // LD2410 OUT(移動=HIGH)
const uint8_t PIN_SSR = 15; // 高=風扇轉(過零 SSR)
const uint8_t PIN_RING = 48; // WS2812 DIN
const uint8_t RING_LEDS = 49;
/* ------------------------------------------------------------
* 3. 控制參數(全部經一個雨季實測調過)
* 滯回上下限:75 轉 / 60 停。停在 70 是發黴甜蜜區,要停就停穿它
* ------------------------------------------------------------ */
const float RH_FORCE_ON = 75.0; // 絕對濕度門檻(有人與否都轉)
const float RH_STOP = 60.0; // 降到此值才允許停機
const float SHOWER_RATE = 4.0; // 10 分鐘升 4%RH =「有人在洗澡」
const float SOAK_RH = 70.0; // 潮浸積分計時門檻
const uint16_t SOAK_ALERT_MIN = 90; // 昨日潮浸超此分鐘數 → 建議除濕
const uint32_t HOLD_MS = 8UL*60000; // 雷達最後動作後的「還在裡」推定
const uint32_t PURGE_MS = 12UL*60000;// 人走後的後吹乾燥
const uint32_t MIN_ON_MS = 5UL*60000; // 最短運轉(保護電機、排透牆面)
const uint32_t MIN_OFF_MS = 10UL*60000;// 最短停機(防頻繁啟停)
const uint8_t DARK_START = 22, DARK_END = 7; // 環燈夜間滅燈時段
/* ------------------------------------------------------------
* 4. 全域物件與狀態
* ------------------------------------------------------------ */
Adafruit_SHT4X sht40 = Adafruit_SHT4X();
Adafruit_NeoPixel ring(RING_LEDS, PIN_RING, NEO_GRB + NEO_KHZ800);
float rhNow = 0, tNow = 0;
uint32_t lastMotionMs = 0, lastShowerMs = 0;
bool fanOn = false;
uint32_t fanSinceMs = 0, lastFanToggleMs = 0, purgeUntilMs = 0;
uint32_t soakSecToday = 0; // 今日 RH>70% 累計秒
float rhMinDay = 100, rhMaxDay = 0;
uint16_t fanMinToday = 0; // 今日排風分鐘(整分累加)
bool soakAlerted = false; // 今日已提醒過(每日只洗版一次)
uint32_t lastSecMs = 0, lastMinuteMs = 0, lastSampleMs = 0;
bool yesterdayDone = true; // 跨日後待發日報旗標
char dayStats[220] = "";
/* 10 分鐘濕度趨勢環形緩衝:30 秒一格 × 20 格
* 為什麼要趨勢:雷達偶尔漏掉「淋浴中靜止」的人,濕度陡升不會說謊 */
float humRing[20]; uint8_t humIdx = 0; bool humRingFull = false;
/* ------------------------------------------------------------
* sendTelegram —— Telegram 推播(失敗不重試:日报错过比洗版討厭,
* 但推播失敗代表斷網,下小時狀態循環會自己補上重要性更高的警訊)
* 參數:msg 訊息本文;回傳:HTTP 200 與否
* ------------------------------------------------------------ */
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 = String("{\"chat_id\":\"") + TG_CHAT_ID +
"\",\"text\":\"" + msg + "\"}";
int st = http.POST(json);
http.end(); // HTTPS 資源必釋(避免堆積)
return st == 200;
}
/* ------------------------------------------------------------
* readSensors —— 讀 SHT40 並更新潮積日統計
* SHT40 I2C 讀取 <1ms,每 2 秒一讀對匯流排毫無壓力
* ------------------------------------------------------------ */
void readSensors() {
sensors_event_t hum, temp;
if (!sht40.getEvent(&hum, &temp)) return; // 感測器失聯:保留舊值繼續控制
rhNow = hum.relative_humidity;
tNow = temp.temperature;
if (rhNow < rhMinDay) rhMinDay = rhNow;
if (rhNow > rhMaxDay) rhMaxDay = rhNow;
}
/* ------------------------------------------------------------
* trendTick —— 每 30 秒存一格濕度,並偵測「洗澡斜率」
* 回傳:是否刚侦測到洗澡事件(10 分鐘升 >4%RH)
* ------------------------------------------------------------ */
bool trendTick() {
bool shower = false;
float old = humRing[humIdx]; // 寫入前先取「10 分鐘前」那格
humRing[humIdx] = rhNow;
humIdx = (humIdx + 1) % 20;
if (humIdx == 0) humRingFull = true;
if (humRingFull && old > 0 && (rhNow - old) >= SHOWER_RATE && rhNow > 60)
shower = true; // 陡升且已進入潮區=熱水正在流
return shower;
}
/* ------------------------------------------------------------
* wantFan —— 三判據決定「想不想轉」(不考慮最短鎖)
* A 雷達推定在場 B 洗澡斜率推定 C 絕對濕度
* 停機則用滯回:轉起來就要一路降到 RH_STOP 才罷休
* ------------------------------------------------------------ */
bool wantFan() {
bool presence = (millis() - lastMotionMs) < HOLD_MS;
bool shower = (millis() - lastShowerMs) < HOLD_MS;
bool force = rhNow >= RH_FORCE_ON;
if (presence || shower || force) return true;
return fanOn && rhNow > RH_STOP; // 停機滯回(轉著未達乾目標不松手)
}
/* ------------------------------------------------------------
* fanControl —— 帶最短啟停鎖與後吹乾燥的輸出級
* 為什麼要 purge:人離開≠牆面乾了,淋浴水霧在磁磚縫的毛細水
* 還要再蒸發十幾分鐘,這才是黴能吃到的飯
* ------------------------------------------------------------ */
void fanControl(bool want) {
uint32_t now = millis();
if (want && !fanOn) {
if (now - lastFanToggleMs >= MIN_OFF_MS) { // 停機鎖未滿:再等一下
fanOn = true; fanSinceMs = now; lastFanToggleMs = now;
digitalWrite(PIN_SSR, HIGH);
Serial.println("[FAN] ON");
}
} else if (!want && fanOn) {
if (now - fanSinceMs < PURGE_MS) return; // 後吹乾燥未滿:轉著
if (now - lastFanToggleMs >= MIN_ON_MS) { // 運轉鎖未滿:轉著
fanOn = false; lastFanToggleMs = now;
purgeUntilMs = now;
digitalWrite(PIN_SSR, LOW);
Serial.println("[FAN] OFF");
}
}
}
/* ------------------------------------------------------------
* lightEffect —— 環燈語言(20ms 刷新)
* 青呼吸=除濕中 綠=有人在 藍慢脈衝=乾燥 紅=潮浸超標
* 夜間時段全滅:浴室半夜不該有燈,感測器不靠看
* ------------------------------------------------------------ */
void lightEffect() {
struct tm t; bool dark = false;
if (getLocalTime(&t, 10))
dark = (t.tm_hour >= DARK_START || t.tm_hour < DARK_END);
if (dark) { ring.clear(); ring.show(); return; }
bool presence = (millis() - lastMotionMs) < HOLD_MS;
uint8_t breath = 90 + 80 * sinf(millis() / 700.0f);
uint32_t c;
if (!soakAlerted && presence) c = ring.Color(0, breath, 0);
else if (fanOn) c = ring.Color(0, breath, breath);
else if (soakAlerted || rhNow > 80) c = ring.Color(breath, 0, 0);
else c = ring.Color(0, 0, breath / 2 + 30);
ring.fill(c);
ring.show();
}
/* ------------------------------------------------------------
* dayTick —— 每日 00:01 結算昨日、07:50 發日報
* 沒有 NTP 同步前不結算(沒時間就沒有「昨天」)
* ------------------------------------------------------------ */
void dayTick() {
static uint8_t lastHour = 99;
struct tm t;
if (!getLocalTime(&t, 10)) return;
if (lastHour == 23 && t.tm_hour == 0) { // 跨日結算
snprintf(dayStats, sizeof(dayStats),
"📅 浴室日報|濕度 %.0f~%.0f%%|潮浸 %lu 分|排風 %u 分",
rhMinDay, rhMaxDay, soakSecToday / 60, fanMinToday);
yesterdayDone = false;
if (soakSecToday / 60 > SOAK_ALERT_MIN) soakAlerted = true; // 今天亮紅燈
rhMinDay = 100; rhMaxDay = 0; soakSecToday = 0; fanMinToday = 0;
}
lastHour = t.tm_hour;
if (!yesterdayDone && t.tm_hour == 7 && t.tm_min >= 50) {
sendTelegram(String(dayStats) +
(soakAlerted ? "\n⚠️ 潮浸超標,建議開除濕機或開窗通風半日" : "\n✅ 狀態良好,繼續保持"));
yesterdayDone = true;
soakAlerted = false; // 新的一天重新觀察
}
}
void setup() {
Serial.begin(115200);
pinMode(PIN_RADAR, INPUT_PULLUP); // LD2410 OUT 推挽輸出,上拉無害防悬空
pinMode(PIN_SSR, OUTPUT);
digitalWrite(PIN_SSR, LOW); // 開機預設停機(安全側)
ring.begin(); ring.clear(); ring.show();
Wire.begin();
if (!sht40.begin()) Serial.println("[SHT40] 未回應,以預設值續跑");
sht40.setSpeed(SHT4X_HIGH_SPEED);
sht40.setResolution(SHT4X_HIGH_RES);
configTzTime("CST-8", "pool.ntp.org", "time.google.com"); // POSIX 的時區寫法:CST-8
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("連線 Wi-Fi");
for (int i = 0; i < 60 && WiFi.status() != WL_CONNECTED; i++) { delay(500); Serial.print("."); }
Serial.println(WiFi.status() == WL_CONNECTED ? " OK" : " 失敗,將持續重試");
}
void loop() {
delay(20);
uint32_t now = millis();
/* --- 1. 秒級:雷達 + 控制 --- */
if (now - lastSecMs >= 1000) {
lastSecMs = now;
if (digitalRead(PIN_RADAR) == HIGH) lastMotionMs = now;
if (WiFi.status() != WL_CONNECTED && now % 30000 == 0) WiFi.reconnect();
fanControl(wantFan());
if (fanOn) { /* 分鐘統計在下面 lastMinuteMs */ }
}
/* --- 2. 兩秒:濕度取樣 --- */
if (now - lastSampleMs >= 2000) {
lastSampleMs = now;
readSensors();
if (rhNow > SOAK_RH) soakSecToday++; // 潮積秒計(1Hz 與取樣同週期,誤差 <2s)
Serial.printf("RH %.1f%% T %.1fC fan=%d presence=%d\r\n",
rhNow, tNow, fanOn, digitalRead(PIN_RADAR));
}
/* --- 3. 30 秒:趨勢環 + 洗澡偵測;60 秒:排風分鐘累計 --- */
if (now - lastSecMs >= 1000 && (now / 1000) % 30 == 0) {
if (trendTick()) { lastShowerMs = now; Serial.println("[SHOWER] 濕度斜率判定了一个人"); }
}
if (now - lastMinuteMs >= 60000) { lastMinuteMs = now; if (fanOn) fanMinToday++; }
/* --- 4. 環燈與日報 --- */
lightEffect();
static uint32_t lastDayMs = 0;
if (now - lastDayMs >= 60000) { lastDayMs = now; dayTick(); }
}
🏗️ 組裝步驟
📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
- 先驗雷達再封殼:tmp 版韌體先跑「雷達 LED 直读」——LD2410 板載指示燈閃爍對應 OUT,站在預裝位置模仿洗澡小動作,確認 3 公尺內穩定觸發才鑽孔
- 強電交給安全流程:斷電→驗電→SSR 串火線→阻燃盒鎖閉→通電→用電燈測試開關。風扇接回前,SSR 端絕對絕緣膠布暫封
- 感測器位置:雷達窗對淋浴區中心(別對著窗戶,窗外樹枝搖曳會誤報);SHT40 進氣縫朝下、離門縫對流路徑 30cm 以內,讀的是「浴室的濕度」而不是「殼裡的濕度」
- 封殼前整機測試:熱水放 5 分鐘對照手機濕度計(偏差 <5% 合格)、人進出看環燈與 Telegram、把 RH_FORCE_ON 暫時改成 40 驗證 SSR 開關與風扇
- 上線觀察 48 小時:看日報數字與你對浴室的直覺是否吻合,再微調滯回參數重燒
🔧 問題排除
| 問題 | 原因 | 解決方法 |
|---|---|---|
| 風扇都不會轉 | SSR 輸入接反(IN+/IN-)或低壓域未共地 | 萬用表二極體檔確認 IN+ 對 IN- 有 LED 壓降;GPIO15 手动寫 HIGH 聽…SSR 沒有聲音——安靜是-normal 的,用輸出端導通判斷 |
| 半夜環燈誤觸「有人在」 | 雷達看到浴簾飄動或窗外樹影 | 調整安裝角度讓雷達視窗避開窗;LD2410 用藍牙 APP 調低靈敏度档位 |
| 濕度永遠比手機低很多 | 進氣縫被列印餘料堵住、或殼內積水形成小氣候 | 針清進氣縫;殼體倒裝確認(本殼設計就是為了殼內不積水) |
| 日報潮浸時間誇張地高 | 感測器長期貼高濕環境產生滯後漂移 | SHT40 會漂但很小;把殼拆下放室內穩定一小時後對照參考值,必要時以 SOAK_RH 加補正值 |
| S3 一直重啟 | 燈座降壓模組電流不足(環燈全亮瞬間 3A 衝擊) | 環燈預設亮度 60;環燈顆數減半或改 5V 2A 電源 |
| 洗澡都要等到很濕才轉 | 雷達漏抓靜止的人且斜率門檻太高 | SHOWER_RATE 由 4.0 降到 3.0;HOLD_MS 拉長;雷達距離螺絲微調近距離檔 |
🎯 進階延伸
- LD2410 UART 深用水區:並上 TX/RX(GPIO17/18)後可用官方協定讀「微動能量值」,把「淋浴中靜止」與「洗澡後靜止」分開——OUT 腳方案的天花板,協定解析請參考 ESPHome 的 ld2410 元件原始碼
- 攝像頭黴斑比對:S3 攝像頭併口接 OV2640,每週拍一次角落,用影像差異當「黴變面積」長期指標——本專案換主板之外的硬體都不用動
- 聯動除濕機:IR 發射模組(GPIO 一條線)讓潮浸超標時自動開除濕機 2 小時,把「建議」變成「執行」
- 多浴室版:一台 S3 掃兩組 SHT40 + 兩路 SSR(GPIO 還剩一大把),雙衛浴共用一個日報
📦 檔案清單
bathroom-moisture-guard/
├── src/
│ └── moisture-guard.ino # ESP32-S3 程式碼(本頁完整程式)
├── stl/
│ ├── guard-shell.stl # 防滴主殼(PLA)
│ ├── face-cover.stl # 透波面蓋(圓盤紋)
│ ├── ring-diffuser.stl # 環燈導光圈
│ ├── wall-clip.stl # 86 盒適配鎖套(PETG)
│ └── ssr-cover.stl # SSR 阻燃盒適配蓋(PETG)
├── f3d/
│ └── moisture-guard.f3d # Fusion 360 原始檔
├── manual/
│ └── assembly.html # 圖文組裝說明書
└── README.md # 說明文件