📋 專案概述
去年冬天,樓下鄰居按了三個早上的電鈴。廚房水槽下方的角閥墊片老化滲水,水滴在櫃底板縫裡整整滴了兩週——等到樓下天花板出現一大片咖啡色水漬,沒有人想到問題出自我家水槽下方。最後的結算:樓下重新油漆天花板 18,000 元、我家系統櫃底板整片換掉 9,500 元,外加跟鄰居來回協調的兩個月心累。
事後我查了市售「漏水偵測器」,發現兩個尷尬的事實:
- 單點式 300~600 元,但多數只會「本地嗶嗶叫」——問題是漏水發生在廚房櫃子裡、人在外面上班時,嗶給誰聽?
- 會推播 Wi-Fi 的款要 1,200 元起跳,還綁品牌 App 與雲端訂閱;我家水槽下、熱水器旁、洗衣機進水管,三處就要 3,600 元。
這個專案用不到 130 元做一颗真正會「打電話叫你回家」的偵測器:
- 每 30 秒深度睡眠輪詢一次探棒,平均電流不到 0.1 mA,一顆 18650 理論上跑一年以上
- 連續 3 個輪詢週期都濕(排除凝結水誤報)→ 本地蜂鳴器狂叫 + Telegram 推播:「💧 偵測到漏水!」
- 擦乾後自動回報「✅ 已恢復乾燥」,不用懷疑是不是誤報
- 每 24 小時發一封「❤️ 系統正常」心跳訊息——安控設備最重要的是讓你確定它還活著
130 元 vs. 27,500 元的教訓,這是全系列性價比最高的一篇。
為什麼選 ESP32-C2?
| 需求 | C2 的優勢 |
|---|---|
| 超低待機 | 深度睡眠約 5~7 µA,全系列待機最省電的型號之一,電池供電的命脈 |
| 成本 | ESP8684 主控的 C2 SuperMini 板單片約 35 元,比買一顆「會叫的」商業偵測器便宜 10 倍 |
| 够用就好 | 本專案只需要「醒來 → 讀一個接腳 → 報告 → 睡回去」,C2 的 2.4GHz Wi-Fi 完全足夠,不必為用不到的雙核 BLE 付錢 |
| 體積 | SuperMini 尺寸 25 × 24 mm,塞進 3D 列印的扁盒剛好 |
🧱 硬體清單
| 項目 | 型號 | 數量 | 預估單價 | 備註 |
|---|---|---|---|---|
| 主控板 | ESP32-C2 SuperMini(ESP8684) | 1 | ~35 元 | 各廠 C2 板皆可;勿買僅燒錄版(無排針) |
| 漏水感測 | YL-69 / 帶比較器模組(數位輸出 DO) | 1 | ~20 元 | 務必選附比較器電路的模組,輸出乾淨的 HIGH/LOW |
| 電池 | 18650 + 帶保護板 | 1 | ~45 元 | 拆機動力電池也行,要有保護板 |
| 充電板 | TP4056(帶保護焊盤版) | 1 | ~8 元 | 選「帶保護板」版本,防過放 |
| 穩壓 | XC6206-3.3 LDO | 1 | ~3 元 | 靜態電流僅 ~1 µA;Wi-Fi 突波靠 220 µF 電容撐 |
| 蜂鳴器 | 有源蜂鳴器 3–5V + S8050 電晶體 | 1 | ~8 元 | GPIO 不直接驅動,走電晶體開集極 |
| 被動件 | 1kΩ / 10kΩ 電阻、100nF / 220µF 電容 | 各 1 | ~3 元 | 基極 1k、柵極下拉、去耦 |
| 導線 | 杜邦線 20cm 母對母 | 4 | ~6 元 | 探棒延長線建議 30cm 以上 |
| 合計 | ~128 元 | 三處防漏總成本仍低於一顆商業 Wi-Fi 偵測器 |
🔌 接線圖
詳細接線
| ESP32-C2 腳位 | 連接 | 說明 |
|---|---|---|
| GPIO4 | 電源閘電晶體基極(經 1kΩ) | 輪詢瞬間才開啟感測模組電源;待機時感測器 0 耗電 |
| GPIO3 | 感測模組 DO | 模組內部已有上拉;多數模組「未觸水 = HIGH、觸水 = LOW」,出廠請先驗證(見問題排除) |
| GPIO5 | 蜂鳴器(經 S8050 + 1kΩ) | 避開 C2 的 strapping 腳 GPIO2/8/9;GPIO5 為安全通用腳 |
| 3V3 / GND | 系統電源 | 電池版由 XC6206 供 3.3V;USB 調試版直接插 USB |
ESP32-C2 SuperMini 漏水模組 (YL-69+比較器) 蜂鳴器電路
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ GPIO4 ●───1kΩ──┤S8050 電源閘├─→ VCC │ │
│ GPIO3 ●──────────┤DO │ │ + ┌───┐ │
│ GPIO5 ●───1kΩ──┤S8050 蜂鳴器 ├──────────┤ ⊕ │ 3–5V │ │
│ GND ●──────────┤GND │ │ − └─┬──┘ │
│ 3V3 ───────────┤(經電源閘) │ └──────┼──────┘
└─────────────┘ └──────────────┘ 12V/5V 端→3V3
探棒 2 線 → 模組 A/B(不分極性)
⚠️ 探棒到模組之間的延長線兩條要絞在一起(絞合線),平行長線會像天線一樣把雜訊耦合進比較器,造成乾燒誤報。
🖨️ 3D 列印零件
零件清單
| 零件 | 數量 | 材質建議 | 顏色 | 列印時間 |
|---|---|---|---|---|
| 主機盒(上蓋) | 1 | PLA | 白色 | ~1.5 hr |
| 主機盒(下蓋) | 1 | PLA | 白色 | ~2 hr |
| 探棒固定座 | 1 | PETG | 藍色 | ~25 min |
| 壁-mounting 卡扣(選配) | 2 | PLA | 白色 | ~15 min |
列印設定
- 材質:機盒 PLA 即可(安裝位置在櫥櫃內、不日曬);探棒固定座接觸水,用 PETG
- 填滿:15% 網格/探棒固定座 100%
- 層厚:0.2mm/固定座 0.16mm
- 壁厚:3 層起——機盒不防水,但至少要防櫥櫃內灑水清洗時的飛濺
設計要點
- 機盒離地 5cm 安裝:偵測器本體最怕的不是漏水而是「積水淹過本體」,上蓋所有開孔(蜂鳴器孔、指示燈孔)一律開在側面,頂面密閉
- 探棒低位走線槽:下蓋底部開 4mm 走線槽,探棒線以「滴水迴圈」(線先垂下再上彎進孔)方式進線,順著線流進盒裡的水會在最低點滴落
- 探棒固定座 3° 傾角:探棒不是平貼地面,而是微傾——水膜一接觸兩條探針就觸發,蒸發後水珠自動滑離,不留殘水造成「永遠濕」
- 電池艙側抽式:18650 從側面滑入,換電池不用拆機盒螺絲
💻 程式碼
📐 註解規範:區塊以框註分段;函式以框註說明用途/參數/回傳;行內註解一律解釋「為什麼」而非「做什麼」。全系列文章採同一規範。
/* ============================================================
* ESP32-C2 水槽下方漏水偵測器 | Under-Sink Water Leak Detector
* ------------------------------------------------------------
* 功能:每 30 秒深度睡眠輪詢漏水探棒;連續 3 次觸水 → 蜂鳴器
* + Telegram 推播;乾燥後回報恢復;每 24 小時心跳報平安
* 硬體:ESP32-C2 SuperMini + YL-69 漏水模組(比較器 DO)
* + 有源蜂鳴器(S8050 驅動)+ 18650 + XC6206 3.3V
* 供電:電池 3.7V → XC6206 LDO(待機 <10µA);調試可插 USB
* 相依:Arduino core for ESP32 ≥ 3.3(C2 支援自 3.3 起)
* 板型:Board: "ESP32C2 Dev Module"
* ============================================================ */
#include <WiFi.h>
#include <HTTPClient.h>
#include <esp_sleep.h> // esp_deep_sleep_start / esp_sleep_enable_timer_wakeup
/* ------------------------------------------------------------
* 1. 使用者設定區 —— 全篇只有這段需要修改
* ------------------------------------------------------------ */
const char* WIFI_SSID = "YOUR_WIFI_SSID"; // 僅 2.4GHz(C2 無 5GHz)
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 int PROBE_PWR_PIN = 4; // 電源閘:HIGH = 幫感測模組通電
const int PROBE_DATA_PIN = 3; // 模組 DO:未觸水 HIGH / 觸水 LOW(出廠請先驗證!)
const int BUZZER_PIN = 5; // 蜂鳴器(經 S8050,HIGH = 叫)
const uint64_t SLEEP_US = 30ULL * 1000000ULL; // 輪詢間隔 30 秒
const uint8_t WET_CONFIRM_COUNT = 3; // 連續 3 個週期濕才報警:凝結水/噴水清洗撐不過 90 秒
const uint16_t HEARTBEAT_CYCLES = 2880; // 30s × 2880 = 24 小時發一次心跳
const uint8_t ALARM_RETRY_CYCLES = 20; // 報警後每 20 週期(10 分鐘)重複推播,直到乾燥
/* ------------------------------------------------------------
* 3. RTC 持久狀態 —— 深度睡眠 = 復位,一般全域變數會歸零,
* 必須用 RTC_DATA_ATTR 放在慢速 RAM 才能跨睡眠保存
* ------------------------------------------------------------ */
RTC_DATA_ATTR uint8_t wetStreak = 0; // 連續觸水週期計數
RTC_DATA_ATTR uint16_t hbCycles = 0; // 心跳計數器
RTC_DATA_ATTR bool leakLatched = false; // 已報警旗標:乾燥時才發「恢復」並重置
RTC_DATA_ATTR uint16_t alarmCycles = 0; // 報警後的重複推播節流
/* ------------------------------------------------------------
* probeIsWet —— 通電讀取探棒狀態
* 回傳:true = 觸水
* 做法:電源閘開啟 → 等 5ms 讓比較器穩定 → 讀 DO → 立刻斷電
* 為什麼斷電:YL-69 通電會電解探棒(即使直流,殘留極化也會加速
* 鏽蝕),而且待機 30 秒期間它根本不需要活著
* ------------------------------------------------------------ */
bool probeIsWet() {
digitalWrite(PROBE_PWR_PIN, HIGH);
delay(5); // 比較器 + RC 濾波穩定時間,讀太快會拿到上一週期的殘值
bool wet = (digitalRead(PROBE_DATA_PIN) == LOW); // 多數模組:觸水拉 LOW
digitalWrite(PROBE_PWR_PIN, LOW);
return wet;
}
/* ------------------------------------------------------------
* beepPattern —— 非阻塞感序列蜂鳴(報警三長音 / 恢復一短音)
* 參數:longBeeps = 長音數(400ms 響 + 200ms 停)
* shortBeeps = 短音數(100ms 響 + 100ms 停)
* ------------------------------------------------------------ */
void beepPattern(int longBeeps, int shortBeeps) {
for (int i = 0; i < longBeeps; i++) { digitalWrite(BUZZER_PIN, HIGH); delay(400); digitalWrite(BUZZER_PIN, LOW); delay(200); }
for (int i = 0; i < shortBeeps; i++) { digitalWrite(BUZZER_PIN, HIGH); delay(100); digitalWrite(BUZZER_PIN, LOW); delay(100); }
}
/* ------------------------------------------------------------
* sendTelegram —— 發送訊息到 Telegram Bot
* 參數:message = 推送文字(支援 Markdown)
* 回傳:是否成功(HTTP 200)
* 注意:本函式只在「確定要發」時呼叫——開 Wi-Fi 是省電大戶,
* 一次連線+發送約耗 3 秒 × 120mA,絕不用在心跳以外的空轉
* ------------------------------------------------------------ */
bool sendTelegram(const String& message) {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
for (int i = 0; i < 20 && WiFi.status() != WL_CONNECTED; i++) delay(500); // 10 秒逾時
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 +
"\",\"parse_mode\":\"Markdown\",\"text\":\"" + message + "\"}";
bool ok = (http.POST(json) == 200);
http.end();
WiFi.disconnect(true); // 主動關射頻:深度睡眠前把射頻電容放電乾淨
return ok;
}
/* ------------------------------------------------------------
* handleWet —— 觸水狀態機(在 wetStreak 已達門檻後進入)
* ------------------------------------------------------------ */
void handleWet() {
if (!leakLatched) {
beepPattern(3, 0); // 本地三長音:人在廚房也聽得到
leakLatched = sendTelegram("💧 **漏水警報!**\n水槽下方偵測到積水,請立即關閉角閥!");
alarmCycles = 0;
} else if (alarmCycles >= ALARM_RETRY_CYCLES) {
// 已報過警但水還在:每 10 分鐘追發一次,Telegram 訊息被蓋掉也能發現
sendTelegram("💧 **漏水仍未處理**(已持續 >10 分鐘)");
alarmCycles = 0;
}
}
/* ------------------------------------------------------------
* handleDry —— 乾燥狀態機:負責「恢復通知」與心跳
* ------------------------------------------------------------ */
void handleDry() {
if (leakLatched) {
beepPattern(0, 1);
if (sendTelegram("✅ **已恢復乾燥。**\n漏水警報解除,請檢查來源是否真的修好了。"))
leakLatched = false;
}
hbCycles++;
if (hbCycles >= HEARTBEAT_CYCLES) { // 每 24 小時報一次平安
if (sendTelegram("❤️ 漏水偵測器運作正常(過去 24 小時無警報)")) hbCycles = 0;
// 發送失敗不歸零:下個週期再試,確保心跳不會無聲消失
}
}
/* ------------------------------------------------------------
* setup —— 深度睡眠架構下所有邏輯寫在 setup():
* 每次醒來 = 一次完整執行 → 結尾睡回去,loop() 永遠到不了
* ------------------------------------------------------------ */
void setup() {
pinMode(PROBE_PWR_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(PROBE_DATA_PIN, INPUT_PULLUP); // 模組斷電期間保持上拉,避免浮接誤讀
digitalWrite(PROBE_PWR_PIN, LOW);
digitalWrite(BUZZER_PIN, LOW);
bool wet = probeIsWet();
if (wet) {
if (wetStreak < 255) wetStreak++; // 飽和計數,防 uint8 溢位回繞
if (wetStreak >= WET_CONFIRM_COUNT) { handleWet(); alarmCycles++; }
hbCycles = 0; // 警報期間不發心跳,避免洗版
} else {
wetStreak = 0; // 單次乾燥即重置:凝結水「濕-乾-濕」無法累積
handleDry();
}
esp_sleep_enable_timer_wakeup(SLEEP_US);
esp_deep_sleep_start();
}
void loop() {
esp_deep_sleep_start(); // 保險:若邏輯未來改出 return,也不空轉
}
📱 設定 Telegram Bot
- 開啟 Telegram,搜尋 @BotFather
- 輸入
/newbot,依指示命名(例:leak_alarm_bot) - 複製 Bot Token(形如
123456789:AAH...) - 搜尋你的 Bot,先發一句任意訊息(Bot 不能主動找你)
- 瀏覽器開
https://api.telegram.org/bot<TOKEN>/getUpdates - 找
"chat":{"id":XXXXXXXXXX},複製這組數字當TG_CHAT_ID
🏗️ 組裝步驟
📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
- 列印零件:機盒上下蓋 PLA、探棒固定座務必 PETG
- 焊電源閘與蜂鳴器電路:兩顆 S8050 + 兩顆 1kΩ 基極電阻,洞洞板 3×5 孔就夠
- 接感測模組:VCC 接電源閘輸出、DO→GPIO3、GND 共地;探棒兩線絞合後接 A/B
- 驗證觸水邏輯:燒錄前先開序列埠看 DO 極性——若你的模組是「觸水 HIGH」,把
probeIsWet()的== LOW改成== HIGH - 固定主板與電池艙:雙面膠貼平,18650 側插入 TP4056 板
- 定位安裝:機盒鎖在櫥櫃側壁離地 5cm 以上,探棒固定座用螺絲鎖在地面最低點(可用水平儀或倒一杯水確認低點)
- 測試:探棒壓濕盤上 2 分鐘 → 蜂鳴器叫 + 手機收到推播 → 擦乾放回 → 收到恢復訊息,即完成
🔧 問題排除
| 問題 | 原因 | 解決方法 |
|---|---|---|
| 沒水也一直報警 | 探棒線太長未絞合 / 模組電位器太靈敏 | 探棒線絞合、远离電源適配器;用小起子把模組電位器逆時針轉 1/4 圈降靈敏 |
| 觸水完全沒反應 | DO 極性與預期相反 | 序列埠確認極性後修改 probeIsWet() 比較式 |
| 電池兩週就沒電 | 用了板載 LDO 且 Wi-Fi 每 30 秒重連 | 確認程式是「只有要發訊息才開 Wi-Fi」;量待機電流應 < 0.1 mA |
| 深度睡眠後狀態不見了 | 狀態變數忘了加 RTC_DATA_ATTR | 跨睡眠變數必須放 RTC RAM,一般全域變數復位即歸零 |
| Wi-Fi 連不上 | 路由器只有 5GHz SSID | C2 只支援 2.4GHz;廚房路由器建議把 2.4G SSID 分開 |
| 半年後探棒鏽蝕 | 長期通電電解 + 水垢 | 本設計待機已斷電大幅延壽;探棒屬耗材,10 元備料 yearly 換 |
🎯 進階延伸
- 多點偵測:C2 剩餘 GPIO 再拉 2 組探棒(電源閘共用),一顆看水槽、熱水器、洗衣機三個點
- 自動關水:加 5V 電磁閥(12V 車用閥 + 升壓),警報時直接切斷角閥——出差党終極形態
- 太陽能補充:櫥櫃門開關有光的話,加 5V 小太陽能板 + TP4056,理論永動
- 改 ESPHome:C2 支援 ESPHome,接進 Home Assistant 做全屋水保全(本程式碼為獨立運作版,不依賴任何 Hub)
📦 檔案清單
water-leak-detector/
├── src/
│ └── leak-detector.ino # ESP32 程式碼(本頁完整程式)
├── stl/
│ ├── box-top.stl # 機盒上蓋
│ ├── box-bottom.stl # 機盒下蓋
│ └── probe-mount.stl # 探棒固定座(PETG)
├── f3d/
│ └── leak-detector-design.f3d # Fusion 360 原始檔
├── manual/
│ └── assembly.html # 圖文組裝說明書
└── README.md # 說明文件