📋 專案概述
七月的某個週二,我提著澆花壺站在水龍頭前,旁邊是三天前下過大雨、裝得滿開的 190 公升省水桶——它就在我看不見的位置,而我對它的認知停留在那聲「咚」的落水聲。水價戶務處級距跳到第三級那個月,我在浴室盯著帳單算了半小時:一家四口夏季每月多繳 400 多元水費,而陽台那桶「免費的水」我這個月一次都用不到——因為我從不知道它剩多少,也從想不起來它存在。省水設備最大的失敗不是漏水,是遺忘。
第二個痛點更噁心,而且是那種要翻開桶蓋才會遇到的:桶底那層放了六七天、晒到微溫的水,白底黑點的孑孓正在裡面集體早自习。斑蚊在 25°C 的水裡,從卵到化蛹只要 5~7 天——「放一周的雨水」和「蚊子育嬰房」是同一個東西的兩個名字。你會倒掉它嗎?不會,那是 190 公升「免費的水」。你會加蓋嗎?桶蓋就靠在牆邊,但每桶都要掀開掀回去,人類做不超過三次。
市售方案我也搜過一圈:
- 進口雨水桶水位計(透明浮球尺+刻度貼):上千台幣,本質是一根會卡垢的塑料尺,還得蹲下去讀。資訊即時性=你記得去蹲的那個瞬間。
- 智慧水錶類 App:量的是自來水,它只會提醒你「這個月超用了」,不會告訴你「明明有水你為什麼在用自來水」。
- 什麼都不裝:零成本,結果就是我上面兩段——繳級距水費+幫邻居養蚊子。
而這三個數字——水位、水溫、存水天數——其實一顆最便宜的 ESP32 就能全算。這台 ESP32-C2 陽台雨水智慧量表做的事:
- JSN-SR04T 防水超聲波測距:探頭倒裝桶蓋朝下,量探頭到水面的距離;每 30 分鐘醒來量 7 次取中位數,水位=桶高−距離,1cm 解析度、±1cm 精度,換算成「還剩幾桶花要澆」的直覺單位
- 水位三級警報:低於 20% 推「該開水龍頭補了」;回讀距離大於空桶值=探頭結藻失鎖,推「去擦探頭」(感測器壞了要自己承認,不能裝死)
- 蚊幼蟲倒數:水位上升事件=補水,重置計時器;水溫 >20°C 且存水 >3 天 → 「蚊幼蟲倒數中」推播。把「上次下雨」變成「上次換水」,這是整台機器的良心模組
- DS18B20 防水水溫:單線總線、不鏽鋼探針可以長期泡水;水溫同時是蚊蟲風險模型的一半,也是澑花前「這桶水冰不冰」的參考
- 省電架構:超聲波模組醒著就吃 3mA——用 NMOS 電源閘讓它「只在被量的 0.5 秒活著」;深睡 30 分鐘一醒,Wi-Fi+TLS 只在告警與每日日報時開啟,其餘時間連射頻都是關的
- 每日 21:00 日報:剩餘水量(公升+百分比)、存水天數、水溫、電池電壓;按鍵按下=立即索取即時狀態
帳目:進口浮球水位計 ~1,000 元+你蹲下去讀的勞務;本機 約 540 元。回收計算很簡單:水價第三級與第二級每度差約 6 元,省水桶每月多澑掉兩桶自來水(約 4 度)就回本一半——但真正的回收率是蚊子的帳:一隻在白天的陽台咬你的花斑蚊,不會因為你裝了水位計就餓死,讓它死在孑孓階段的,是你終於記得換水這件事。
為什麼選 ESP32-C2?
| 需求 | C2 的優勢(以及其他型號為什麼不選) |
|---|---|
| 每 30 分鐘醒 8 秒的待機帳單 | 深睡電流 5µA 級(datasheet 典型值)、全系列最便宜的 Wi-Fi 晶片;這台的日常是「裝死 29 分 52 秒,醒來量個水」,多一分錢的晶片都是浪費 |
| 功能匹配 | 沒有 BLE、沒有雙核、Flash 小——但一個澆水量表需要這些嗎?S3 的 PSRAM 在這裡每一 byte 都在繳笑聲稅;C6 的 Wi-Fi 6 對「一天連網 48 次、每次兩秒」的負載毫無意義。省下的 35 元拿去买更好的防水超聲波探頭 |
| 要付的代價(誠實欄) | 2MB Flash 塞完 Wi-Fi+mbedTLS+NVS 之後沒有 OTA 餘裕——定稿單 app 分割、關 OTA。陽台機器刷壞了爬一次陽台,比多一個遠端刷機攻擊面便宜。1MB 版模組請放棄本篇(TLS 緩衝會跟你搶 RAM/Flash) |
| 腳位限制 | C2 的 ADC1 只落在 GPIO0–4,電池分壓這種模擬腳要事先卡位;本篇 GPIO0 讀電壓、GPIO3–6 給數位,剛好夠且互不搶劫 |
🧱 硬體清單
| 項目 | 型號 | 數量 | 預估單價 | 備註 |
|---|---|---|---|---|
| 主控板 | ESP32-C2-DevKitM-1(2MB Flash 版) | 1 | ~75 元 | 買模組時確認是 2MB 版;1MB 版塞不下 TLS |
| 水位感測 | JSN-SR04T(防水探頭版) | 1 | ~120 元 | 一定要防水灌膠版,一般 HC-SR04 在桶蓋結露環境三個月就癌末 |
| 水溫感測 | DS18B20 防水不鏽鋼探針 | 1 | ~45 元 | 三線版(VDD/DQ/GND),不要買兩線寄生供電版——深睡斷電時會讀出 85°C 鬼碼 |
| 電源閘 | 2N7002 NMOS+100k/10kΩ | 1 組 | ~5 元 | 超聲波模組的 GND 開關;省電架構的本體 |
| 儲能 | 18650+保護板 | 1 | ~85 元 | 帶板版;陽台冬天低溫選容量衰減小的廠牌 |
| 發電 | 5V 1W 太陽能板 | 1 | ~70 元 | 本機平均電流預算 <0.15mA,1W 是給「連續陰雨+蚊蟲推播頻繁」的保險 |
| 充電 | TP4056+SS54 防逆二極體 | 1 | ~20 元 | RPROG 改 2kΩ 限充 0.5C 內 |
| 升壓 | MT3608(調 5.0V) | 1 | ~10 元 | 先調好再鎖膠,輸出飄壓會毒死 WS 元件與 ADC 基準 |
| 外殼 | 防水接線盒 12×8×6 | 1 | ~35 元 | IP65;裝桶蓋側面,探頭穿孔朝下 |
| 呼吸 | 透氣膜+防水膠條 | 1 組 | ~25 元 | 桶上空間日夜溫差大,不透氣=盒內下雨 |
| 人機 | 按鍵 | 1 | ~5 元 | 按下=立即回報(不用等 21:00) |
| 雜項 | 100kΩ×2、4.7kΩ、10kΩ、螺絲線材 | 1 組 | ~45 元 | 100k 分壓測電池;4.7k 是 DS18B20 單線上拉(少了它是最常見故障) |
| 合計 | ~540 元 | 無訂閱、無雲端;水桶資料全留自家 |
🔌 接線圖
CRC 失敗 或永遠的 85.0°C(上電預設值鬼碼)。② 超聲波探頭朝下安裝、穿孔處做滴水簷:探頭膜面朝上會積雨珠,然後你就得到一個永遠顯示「還有半桶水」的騙子。③ 本機全低壓(5V),但 18650 是能量密度很高的東西——保護板、防逆二極體、絕熱墊一個都不能省,電池倉不要用黑色 ASA 直晒太陽。⚙️ 3D 列印
| 零件 | 材質 | 列印設定 | 設計重點 |
|---|---|---|---|
| 桶蓋感測器座 | PETG(陰面)/ASA(日照面) | 0.2mm、3 壁、防水螺旋頂 | 探頭筆直朝下、座體邊緣做滴水簷;探頭到空桶水面的距離要 >18cm 死區——桶蓋太平就把座加高 |
| 防雨罩+透氣圈 | 同上 | 0.2mm、2 壁 | 罩住盒體與探頭接線,底部透氣槽朝下;桶上不密閉設計,出水時空氣要進得去(真空會卡水) |
| 太陽能板夾座 | ASA | 0.24mm、3 壁 | 夾欄桿式,30° 仰角朝北(陽台直射位);走線槽貼夾臂下行 |
| 探頭固定套 | TPU(軟膠) | 0.2mm、100% 填充邊 | 軟套減振:桶蓋被風吹得咚咚響時,震動噪音會混進回波——軟性緩衝是最便宜的濾波 |
總列印約 2.5 小時。桶身不用列印——賣場 190 公升儲水桶、或機車行免費要的舊油桶(洗淨)都行,打孔時開孔器比探頭直徑大 1mm、硅膠圈密封。
💻 Arduino 韌體
板子選 ESP32C2 Dev Module;Flash 2MB、Partition 選 Minimal(app 約 950KB,TLS+WiFi+NVS,關 OTA——2MB 的誠實之選)。DS18B20 用 OneWire 庫+自建 CRC8 解碼,不塞 DallasTemperature 這整頭大象進 2MB。
/* ============================================================
* rain_barrel_gauge.ino — ESP32-C2 陽台雨水智慧量表(ESP32 全系列 × 3D 列印 第 25 篇)
*
* 功能:JSN-SR04T 測桶內水位(7 次中位數)+ DS18B20 水溫 →
* 水位/存水天數/蚊蟲風險 → 低水位・感測失鎖・蚊幼蟲倒數三類告警
* + 每日 21:00 Telegram 日報;深睡 30 分鐘一醒、感測器電源閘、
* 只在回報時開 Wi-Fi+TLS
* 硬體:ESP32-C2-DevKitM-1(2MB)
* TRIG=GPIO4|ECHO=GPIO5|電源閘=GPIO3|1-Wire=GPIO6
* 按鍵=GPIO2|電池 ADC=GPIO0(ADC1 僅 GPIO0–4,選腳已卡位)
* 供電:太陽能 → TP4056+SS54 → 18650 → MT3608 5V → 板 5V(VBUS 不接)
* 相依:Arduino ESP32 core ≥ 3.1(板選 ESP32C2 Dev Module)
* OneWire(paulstoffregen);mbedTLS 走 core 內建 WiFiClientSecure
* ============================================================ */
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <OneWire.h>
#include <driver/gpio.h>
#include <nvs.h>
#include <esp_sleep.h>
#include <time.h>
/* --- 0. 使用者設定 --- */
const char* WIFI_SSID = "你家分享器";
const char* WIFI_PASS = "密碼";
const char* TG_TOKEN = "000000:換成你的_bot_token"; // @BotFather
const char* TG_CHAT = "換成你的_chat_id";
/* --- 1. 桶身幾何(換桶必改這三行)--- */
const float MOUNT_H = 55.0f; // cm:探頭到「空桶底」的距離=桶高+蓋高
const float BARREL_H = 45.0f; // cm:有效水深上限
const float AREA_CM2 = 2400.0f;// cm²:桶截面積(60×40 長方桶=2400)
const float DEAD_ZONE = 18.0f; // cm:JSN-SR04T 近端死區,小於它視為失鎖
/* 每 1cm 水位=2.4L */
/* --- 2. 腳位 --- */
#define PIN_TRIG 4
#define PIN_ECHO 5
#define PIN_GATE 3 // HIGH=超聲波模組通電(2N7002 閘極)
#define PIN_1W 6 // DS18B20 單線(4.7k 上拉 3.3V)
#define PIN_BTN 2 // 內部上拉;按下=立即回報
#define PIN_VBAT 0 // ADC1_CH0:100k×2 分壓(⚠ C2 ADC1 只有 GPIO0–4)
/* --- 3. 策略參數 --- */
const uint32_t SLEEP_MS = 30UL * 60UL * 1000UL; // 例行:每 30 分鐘醒一次
const uint8_t SAMPLE_N = 7; // 每次醒來量 7 筆取中位數(抗水面波浪)
const float LOW_PCT = 20.0f;// 低於 20% 推「補水」
const float REFILL_CM = 1.0f; // 水位上升 >1cm =補水事件(下雨或人工加水)
const uint8_t MOQUITO_D = 3; // 存水超過 3 天
const float MOQUITO_T = 20.0f;// 且水溫超過 20°C → 蚊幼蟲倒數
OneWire ow(PIN_1W);
/* --- 4. 深睡也活下來的狀態(RTC slow mem)--- */
RTC_DATA_ATTR float lastDepth = -1; // 上次水深(-1=開機首樣本)
RTC_DATA_ATTR uint32_t refillDay = 0; // 上次補水日(epoch 天)
RTC_DATA_ATTR uint32_t lowSentDay = 0; // 低水位警報去重:今天已發過?
RTC_DATA_ATTR uint32_t mosqSentDay = 0; // 蚊蟲警報去重
RTC_DATA_ATTR uint32_t lostSentDay = 0; // 感測失鎖去重
RTC_DATA_ATTR uint32_t reportDay = 0; // 21:00 日報去重(跨日鍵)
/* ============================================================
* 電源閘 + 超聲波測距
* ============================================================ */
/**
* 開/關超聲波模組電源(2N7002 電源閘)。
* 參數:on=true 通電
* 為什麼:JSN-SR04T 靜態電流 ~3mA,比整台 ESP32 深睡還兇 600 倍——
* 省電架構的本體就是這個開關,不是深睡本身。
*/
void sensorPower(bool on) {
digitalWrite(PIN_GATE, on ? HIGH : LOW);
delay(on ? 300 : 5); // 模組上電需約 300ms 穩定(datasheet 建議值)
}
/**
* 單次測距。
* 回傳:探頭到水面距離(cm);0=本次無回波
* 說明:TRIG 10µs 脈衝+ECHO 高電位 ×340m/s÷2;
* 13500µs 上限=2.25m,桶內絕對夠。pulseIn 逾時回 0,原樣丟給上層當失鎖訊號。
*/
float pingCm() {
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(5);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
uint32_t us = pulseIn(PIN_ECHO, HIGH, 13500);
return us * 0.0343f / 2.0f; // µs → cm(聲速除以來回)
}
/**
* 量一輪水位。
* 參數:n=取樣次數(建議 7)
* 回傳:距離中位數(cm);-1=感測失鎖(多數樣本為 0 或落入死區)
* 中位數而非平均:大雨時水面是波浪場,偶發的早回波/無回波會把平均數拉歪,
* 中位數天然免疫。
*/
float medianPing(uint8_t n) {
float v[9]; uint8_t good = 0;
for (uint8_t i = 0; i < n && i < 9; i++) {
float d = pingCm();
if (d > DEAD_ZONE && d < MOUNT_H + 15.0f) v[good++] = d; // 死區與超量程都丟掉
delay(60);
}
if (good < n / 2 + 1) return -1; // 過半失敗=失鎖(探頭髒了/結藻)
// 泡沫排序 7 個數:為了 7 個數引入 qsort 是對 2MB Flash 的挑釁
for (uint8_t i = 0; i < good - 1; i++)
for (uint8_t j = 0; j < good - 1 - i; j++)
if (v[j] > v[j + 1]) { float t = v[j]; v[j] = v[j + 1]; v[j] = t; }
return v[good / 2];
}
/* ============================================================
* DS18B20 水溫(單線+自建 CRC8,不帶大庫進 2MB)
* ============================================================ */
/**
* CRC8(Maxim 單線多項式 x^8+x^5+x^4+1,反射型)。
* 參數:dat=暫存器內容陣列、n=長度
* 回傳:true=CRC 正確
* 為什麼:單線總線在潮濕環境位翻轉很常見,沒 CRC 你會把雜訊讀成水溫。
*/
bool crc8ok(const uint8_t* dat, uint8_t n) {
uint8_t crc = 0;
for (uint8_t i = 0; i < n; i++) {
uint8_t b = dat[i];
for (uint8_t r = 0; r < 8; r++) {
uint8_t mix = (crc ^ b) & 0x01;
crc >>= 1;
if (mix) crc ^= 0x8C;
b >>= 1;
}
}
return crc == 0; // 含 CRC byte 在內整包同餘 0
}
/**
* 讀水溫。
* 回傳:攝氏溫度;-999=總線無回應/CRC 失敗
* 流程:Skip ROM→ConvertT(12bit, 750ms)→再 Skip→讀 9-byte scratchpad。
* 兩段之間不睡死:12-bit 轉換要 750ms,用 delay 換程式可讀性,
* 本機每個 wake 週期只讀一次,遲得起。
*/
float readWaterTemp() {
uint8_t sp[9];
ow.reset();
ow.skip();
ow.write(0x44, 1); // Convert T, 外部供電無需 parasite power
delay(760);
ow.reset();
ow.skip();
ow.write(0xBE, 0); // Read Scratchpad
for (uint8_t i = 0; i < 9; i++) sp[i] = ow.read();
if (!crc8ok(sp, 9)) return -999.0f;
int16_t raw = (int16_t)((sp[1] << 8) | sp[0]);
if (raw == 0x0750) return -999.0f; // 上電預設 85.0°C 鬼碼(剛上電沒轉換過)
return raw * 0.0625f;
}
/* ============================================================
* Telegram(只在必須說話時開 Wi-Fi)
* ============================================================ */
/**
* 發送 Telegram。
* 參數:msg=內文
* 回傳:true=送達
* 逾時重試 3 次、指數退避;setInsecure 見第 24 篇安全性框——
* 正式部署改 setCACert 或自家伺服器中轉;token 權限只開「發訊息」。
*/
bool sendTg(String msg) {
if (WiFi.status() != WL_CONNECTED) return false;
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.begin(client, String("https://api.telegram.org/bot") + TG_TOKEN + "/sendMessage");
http.addHeader("Content-Type", "application/json");
for (int t = 0; t < 3; t++) {
String body = "{\"chat_id\":\"" + String(TG_CHAT) + "\",\"text\":\"" + msg + "\"}";
int code = http.POST(body);
if (code == 200) { http.end(); return true; }
delay(500 << t);
}
http.end();
return false;
}
/** 連網→校時→發訊息→射頻關光光。回傳:是否送出 */
bool talk(String msg) {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
bool sent = false;
if (WiFi.waitForConnectResult() == WL_CONNECTED) {
configTime(8 * 3600, 0, "pool.ntp.org"); // 醒著才校時,深睡靠 RTC 走
getLocalTime(nullptr, 2000); // 把系統時鐘拉起來給 time() 用
sent = sendTg(msg);
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF); // 射頻是省電大頭,用完關
}
return sent;
}
/* ============================================================
* 主邏輯
* ============================================================ */
float vbat() { return analogReadMilliVolts(PIN_VBAT) * 2.0f / 1000.0f; }
void setup() {
Serial.begin(115200);
pinMode(PIN_GATE, OUTPUT); digitalWrite(PIN_GATE, LOW);
pinMode(PIN_TRIG, OUTPUT); digitalWrite(PIN_TRIG, LOW);
pinMode(PIN_ECHO, INPUT);
pinMode(PIN_BTN, INPUT_PULLUP);
// ECHO 在模組斷電時是浮動的——先拉低幾輪,防止深睡喚醒殘留脈衝誤判
for (uint8_t i = 0; i < 3; i++) { digitalWrite(PIN_TRIG, HIGH); delay(1);
digitalWrite(PIN_TRIG, LOW); delay(1); }
Serial.println(F("\n=== rain_barrel_gauge 雨水量表啟動 ==="));
}
void loop() {
uint32_t today = (uint32_t)(time(nullptr) / 86400); // epoch 天
bool btnNow = digitalRead(PIN_BTN) == LOW;
bool first = (lastDepth < 0);
/* --- 1. 量水位(電源閘開→中位數→關)--- */
sensorPower(true);
float dist = medianPing(SAMPLE_N);
sensorPower(false);
float depth = constrain(MOUNT_H - dist, 0.0f, BARREL_H);
float pct = depth / BARREL_H * 100.0f;
float litres = depth * AREA_CM2 / 1000.0f; // cm³→L
Serial.printf("[level] dist=%.1fcm depth=%.1fcm %.0f%% %.0fL\n",
dist, depth, pct, litres);
/* --- 2. 補水事件偵測:水位明顯上升=下雨或人工加水 → 重置存水計時 --- */
if (!first && depth > lastDepth + REFILL_CM) {
refillDay = today;
Serial.println(F("[refill] 補水事件:存水計時器歸零"));
}
lastDepth = depth;
/* --- 3. 量水溫(探針常電,不需要電源閘)--- */
float tempC = readWaterTemp();
uint32_t ageD = (today >= refillDay && refillDay > 0) ? (today - refillDay) : 0;
/* --- 4. 組裝訊息:三類告警各自去重(同一天同一類只轟一次)--- */
String msg;
bool urgent = false;
if (dist < 0 && lostSentDay != today) {
lostSentDay = today; urgent = true;
msg = "🧽 雨水量表:超聲波回讀失鎖——探頭可能結藻/沾水珠,請擦一下探頭。在那之前「水位」欄位是玄學。";
} else if (pct < LOW_PCT && lowSentDay != today) {
lowSentDay = today; urgent = true;
msg = "🚱 省水桶見底:" + String((int)pct) + "%(剩 " + String((int)litres)
+ "L)。要澆水的話,今天就開水龍頭吧,別讓花在等。";
} else if (ageD >= MOQUITO_D && tempC >= MOQUITO_T && mosqSentDay != today) {
mosqSentDay = today; urgent = true;
msg = "🦟 蚊幼蟲倒數:桶內水已 " + String(ageD) + " 天沒換、水溫 "
+ String(tempC, 1) + "°C——斑蚊的育嬰房正在量產中。澆完就把它用掉,或整桶換水。";
}
/* --- 5. 每日 21:00 日報(跨日鍵去重)--- */
struct tm t;
bool clockOK = getLocalTime(&t, 500);
bool daily = clockOK && t.tm_hour == 21 && reportDay != today;
/* --- 6. 要說話才連網(連網=本機最貴的動作)--- */
bool btnPress = btnNow;
if (btnPress) { while (digitalRead(PIN_BTN) == LOW) delay(10); } // 放鍵防連發
if (urgent || daily || btnPress) {
String rep = "💧 陽台省水桶:" + String((int)litres) + "L(" + String((int)pct)
+ "%)· 水溫 " + (tempC > -500 ? String(tempC, 1) + "°C" : "讀不到")
+ " · 存水 " + String(ageD) + " 天 · 電池 " + String(vbat(), 2) + "V";
bool ok = talk(msg.length() ? (msg + "\n" + rep) : rep);
Serial.printf("[tg] %s\n", ok ? "已送達" : "發送失敗(下次喚醒補發)");
if (daily) reportDay = today;
}
/* --- 7. 回家睡覺(30 分鐘;電壓過低拉長到 2 小時保命)--- */
Serial.flush(); delay(10);
esp_sleep_enable_timer_wakeup((vbat() < 3.40f ? 2UL * 3600UL : 1800UL) * 1000000ULL);
esp_deep_sleep_start();
}
Minimal(app 1.9MB+NVS+剩餘 storage),單一 app、OTA 關閉。編譯選項把 CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN 保持預設 16KB(縮到 4KB 會和 Telegram 的伺服器握手中斷——實測過,別問)。省體積的另一招是上面代碼裡「OneWire 手解 DS18B20」:DallasTemperature 庫連同字串格式化要吃 20 多 KB,對 2MB 而言是大象。setInsecure() 是社群常例,但它是「不驗證憑證」——bot token 會經過一條可能被中間人的路。正式做法:抓 CA 後 client.setCACert(ca_pem),或推播改走自家伺服器中轉。token 權限只開「發訊息」。🔧 組裝步驟
- 桌面通電測試:先不裝電源閘(GPIO3 直接接 3.3V 旁通),跑 Serial 看 ping 正常後,再接回 MOSFET——先分清「感測器壞」和「電源閘壞」兩種病。
- 電源閘驗證:GPIO3 拉低後量模組 VCC 應 <0.1V、整機待機電流 <0.2mA;拉高後 300ms 內可量到回波。待機電流超標先查 LED 與上拉電阻漏電。
- 水位校準:長尺量實際水深對 Serial 讀值,誤差 >1.5cm → 查探頭是否垂直水面、桶蓋是否傾斜。改
MOUNT_H前先懷疑物理安裝。 - 蚊蟲模式演練:Serial 指令把
refillDay灌成 5 天前(或直接把水溫探針含在手心加熱到 25°C)→ 推播觸發=良心模組上崗。 - 入盒與防水:電路進盒、探頭穿孔打硅膠、透氣膜貼最低點對角;線材出口做防滴彎。盒蓋鎖緊前再量一次水位(開蓋測過=沒測過)。
- 上架:桶蓋側面固定盒體、探頭懸在桶內中央(離桶壁 >15cm 防壁反射);太陽能板夾欄桿朝北 30°;按鍵拉到桶蓋正面。澆花壺沖淋 3 分鐘無滲水=正式上崗。
🧪 驗證
| 測試 | 動作 | 合格線 |
|---|---|---|
| 死區驗證 | 探頭下方 15cm 放平板 | 讀值必落入「失鎖」邏輯而不是亂讀小值 |
| 水位校準 | 長尺實測水深 vs Serial | 誤差 ≤1.5cm;連續 10 次讀值波動 ≤1cm(無波浪時) |
| 斷閘驗證 | 電流串入電池線,GPIO3 拉低 | 整機待機 <0.2mA;模組 VCC <0.1V |
| 蚊蟲模式 | 灌假 refillDay=5 天前+水溫 >20°C | Telegram 收到蚊幼蟲倒數,且同一天不重複轟炸 |
| 失鎖自白 | 探頭貼保鮮膜模擬結藻 | 觸發「擦探頭」推播,不是靜靜報假水位 |
| 防水 | 澆花壺沖淋桶蓋 3 分鐘 | 盒內乾燥、水位讀值不變 |
| 補水重置 | 人工加 5L 水 | 下個循環「存水 0 天」,蚊蟲倒數歸零 |
🆘 問題排除
| 症狀 | 最可能的原因 | 處方 |
|---|---|---|
| 讀值恆 0 或恆最大 | TRIG/ECHO 接反;電源閘沒開(模組沒電);探頭膜面積水珠 | 核對兩根訊號線;量模組 VCC;探頭朝下+滴水簷重裝 |
| 水位跳動 ±3cm | 大雨波浪(正常);桶內立柱/水管回音;探頭離壁太近 | 中位數已是解方;移動探頭至桶面中央、離壁 >15cm |
| DS18B20 讀 85.0 或 -999 | 4.7k 上拉漏接;VDD 接到電源閘後面;探針進水 | 補上拉;VDD 改常電 3.3V;探針要買灌封不鏽鋼版 |
| 深睡醒來第一筆怪值 | 喚醒瞬間 ECHO 殘留高位 | setup 先拉低 TRIG 數輪(程式已做);第一筆樣本丟掉可再加保險 |
| 日報沒發 | 21:00 判斷写在未連網分支;RTC 走時漂移 | 日報判斷放連網成功後(程式已做);每次連網重 configTime |
| 待機電流 1mA 起跳 | TP4056 反接漏電;升壓未鎖膠飄壓;按鍵一直壓著 | 逐級拔模組定位;升壓重調 5.0V;查按鍵卡死 |
| 冬天讀值縮水 | 鋰電池低溫內阻上升,電壓偵測誤判低壓降頻 | 正常保護行為;桶身包泡棉保电池也算保温 |
📁 專案檔案結構
rain-barrel-gauge/
├── firmware/
│ └── rain_barrel_gauge.ino # 上方完整韌體
├── stl/
│ ├── lid-sensor-mount.stl # 桶蓋感測器座(含滴水簷)
│ ├── rain-shroud.stl # 防雨罩+透氣圈
│ ├── solar-clip.stl # 太陽能板夾座
│ └── probe-bushing.stl # TPU 探頭軟套
├── f3d/
│ └── rain-barrel.f3d # Fusion 360 原始檔
├── manual/
│ └── assembly.html # 圖文組裝說明書
└── README.md # 說明文件
🎯 結語
這個專案最值錢的不是那顆 75 元的晶片,是「把遺忘外包出去」這個架構決定:水位計不會讓你變成更好的園丁,它只是把你忘記的那件事——桶裡還有沒有水、水放了幾天、蚊子開不開班——在正確的時間推到你手机上。省水帳要算一年才好看,但蚊子帳當天就回本:存水第 3 天那則「蚊幼蟲倒數中」的推播,是這台機器替你家陽台做的最好的一次公共衛生。而當水費級距下次跳級、你看著 App 裡自己給的「這個月省了 11 桶」的日報時,會想起這句話:最好的感測器,是幫你記得起的那一個。