📋 專案概述

去年冬天,樓下鄰居按了三個早上的電鈴。廚房水槽下方的角閥墊片老化滲水,水滴在櫃底板縫裡整整滴了兩週——等到樓下天花板出現一大片咖啡色水漬,沒有人想到問題出自我家水槽下方。最後的結算:樓下重新油漆天花板 18,000 元、我家系統櫃底板整片換掉 9,500 元,外加跟鄰居來回協調的兩個月心累。

事後我查了市售「漏水偵測器」,發現兩個尷尬的事實:

  1. 單點式 300~600 元,但多數只會「本地嗶嗶叫」——問題是漏水發生在廚房櫃子裡、人在外面上班時,嗶給誰聽?
  2. 會推播 Wi-Fi 的款要 1,200 元起跳,還綁品牌 App 與雲端訂閱;我家水槽下、熱水器旁、洗衣機進水管,三處就要 3,600 元。

這個專案用不到 130 元做一颗真正會「打電話叫你回家」的偵測器:

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 LDO1~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 SuperMini USB(燒錄/供電) GPIO4 GPIO3 GND 漏水模組(比較器) DO GND VCC 探棒(平放地面) 蜂鳴器(經 S8050) 紅 VCC ← 3V3(經 GPIO4 電源閘) 黑 GND → GND 黃 GPIO3 ← DO(漏水=LOW) 黃 GPIO5 → 蜂鳴器基極(1kΩ)
▲ 感測模組 VCC 不直接接 3V3,而是接在 GPIO4 控制的電源閘(S8050)上——待機時感測器完全斷電。

詳細接線

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 列印零件

零件清單

零件數量材質建議顏色列印時間
主機盒(上蓋)1PLA白色~1.5 hr
主機盒(下蓋)1PLA白色~2 hr
探棒固定座1PETG藍色~25 min
壁-mounting 卡扣(選配)2PLA白色~15 min

列印設定

設計要點

  1. 機盒離地 5cm 安裝:偵測器本體最怕的不是漏水而是「積水淹過本體」,上蓋所有開孔(蜂鳴器孔、指示燈孔)一律開在側面,頂面密閉
  2. 探棒低位走線槽:下蓋底部開 4mm 走線槽,探棒線以「滴水迴圈」(線先垂下再上彎進孔)方式進線,順著線流進盒裡的水會在最低點滴落
  3. 探棒固定座 3° 傾角:探棒不是平貼地面,而是微傾——水膜一接觸兩條探針就觸發,蒸發後水珠自動滑離,不留殘水造成「永遠濕」
  4. 電池艙側抽式: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

  1. 開啟 Telegram,搜尋 @BotFather
  2. 輸入 /newbot,依指示命名(例:leak_alarm_bot)
  3. 複製 Bot Token(形如 123456789:AAH...)
  4. 搜尋你的 Bot,先發一句任意訊息(Bot 不能主動找你)
  5. 瀏覽器開 https://api.telegram.org/bot<TOKEN>/getUpdates
  6. 找 "chat":{"id":XXXXXXXXXX},複製這組數字當 TG_CHAT_ID

🏗️ 組裝步驟

📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
  1. 列印零件:機盒上下蓋 PLA、探棒固定座務必 PETG
  2. 焊電源閘與蜂鳴器電路:兩顆 S8050 + 兩顆 1kΩ 基極電阻,洞洞板 3×5 孔就夠
  3. 接感測模組:VCC 接電源閘輸出、DO→GPIO3、GND 共地;探棒兩線絞合後接 A/B
  4. 驗證觸水邏輯:燒錄前先開序列埠看 DO 極性——若你的模組是「觸水 HIGH」,把 probeIsWet() 的 == LOW 改成 == HIGH
  5. 固定主板與電池艙:雙面膠貼平,18650 側插入 TP4056 板
  6. 定位安裝:機盒鎖在櫥櫃側壁離地 5cm 以上,探棒固定座用螺絲鎖在地面最低點(可用水平儀或倒一杯水確認低點)
  7. 測試:探棒壓濕盤上 2 分鐘 → 蜂鳴器叫 + 手機收到推播 → 擦乾放回 → 收到恢復訊息,即完成

🔧 問題排除

問題原因解決方法
沒水也一直報警探棒線太長未絞合 / 模組電位器太靈敏探棒線絞合、远离電源適配器;用小起子把模組電位器逆時針轉 1/4 圈降靈敏
觸水完全沒反應DO 極性與預期相反序列埠確認極性後修改 probeIsWet() 比較式
電池兩週就沒電用了板載 LDO 且 Wi-Fi 每 30 秒重連確認程式是「只有要發訊息才開 Wi-Fi」;量待機電流應 < 0.1 mA
深度睡眠後狀態不見了狀態變數忘了加 RTC_DATA_ATTR跨睡眠變數必須放 RTC RAM,一般全域變數復位即歸零
Wi-Fi 連不上路由器只有 5GHz SSIDC2 只支援 2.4GHz;廚房路由器建議把 2.4G SSID 分開
半年後探棒鏽蝕長期通電電解 + 水垢本設計待機已斷電大幅延壽;探棒屬耗材,10 元備料 yearly 換

🎯 進階延伸

📦 檔案清單

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                    # 說明文件
📚 系列索引 下一篇:ESP32-C6 番茄鐘專注方塊 →