📋 專案概述
12 月的深夜四點半,我是被喉嚨乾醒的。冷氣無聲地抽了一整晚的水,書房濕度計顯示 32%——這個數字對木吉他、木窗框和我的鼻黏膜都是壞消息。上個月女兒一周流了兩次鼻血,醫生只說了一句「房間太乾」。
市面上的智能加濕器查了一圈,結論是這個品類的「智能」兩個字含水量比它噴出来的霧還高:
- 千元級的「智能」= 手機遠開關:App 上那個開關按鈕,和我蹲在機器前面按的那個開關,唯一的差別是要先解鎖手機。它依然不會看濕度——設定 8 小時定時,半夜濕度衝到 75% 照樣噴,枕頭邊床單摸起來是潮的。
- 濕度計 + 傳統加濕器 = 請自己半夜起來關機器:我試過買一個 300 元的加濕器配 200 元的濕度計,結果是設定鬧鐘起來關霧,連續一週後鬧鐘被我關了。
- 真正的痛是過濕,不是乾:冷霧本質是把純水打成微粒直吹,出霧速度永遠比房間擴散快。直吹桌面三小時,筆電鍵盤縫裡能長出鏽斑——會停的加濕器才叫智能,不會停的叫霧化事故。
這個專案不買新加濕器,而是把家樂福 300 元的傳統超聲波加濕器「換腦」:用 不到 400 元的零件 + 3D 列印水箱,做一台真正看得見濕度的機器:
- 濕度閉環:目標 50%RH,掉到 47% 開噴、回到 52% 停(遲滯帶 5%,不會在閾值邊界反覆跳啟停)
- 缺水即斷 + Telegram 推播:浮球開關驅動 MOSFET 物理斷電,推播「💧 缺水了」,補水後自動推「✅ 已恢復」
- 40 分鐘強制休息:小房間連噴 40 分鐘必定休息 20 分鐘,閉環失靈也不會把房間變雲霧紗
- 餘霧吹乾:停止出霧後風扇延遲運轉 30 秒,把殼內殘霧吹出去——殘霧在殼裡凝結發霉是 DIY 加濕器的第一殺手
- OLED 即時濕度直方圖:一眼看出這三小時房間是越来越乾還是剛補過水
成本效益算給你聽:千元級「智能」加濕器 1,299 元,功能清單裡沒有一項是本文第三、第四點;本專案 381 元,還多一個可以無限改韌體的 Wi-Fi 主控。真正的智能不是連網,是在正確濕度自己停下來。
為什麼選 ESP32-C2?
| 需求 | C2 的優勢 |
|---|---|
| 成本 | 全系列最便宜的 Wi-Fi 板之一,SuperMini 單片 ~40 元,本專案預算大頭該留給電源和感測器 |
| 效能剛剛好 | 閉環控制 1 秒一輪、感測器只有 I2C 兩顆——單核 C2 綽綽有餘,S3 的錢花在這裡會後悔 |
| 功耗餘裕 | 深度睡眠 ~5µA,日後改鋰電池 + 太陽能不用換板重設計 |
| 體積 | 25 × 18 mm,藏進上蓋控制艙完全不佔加水口 |
🧱 硬體清單
| 項目 | 型號 | 數量 | 預估單價 | 備註 |
|---|---|---|---|---|
| 主控板 | ESP32-C2 SuperMini | 1 | ~40 元 | 各廠 C2 SuperMini 皆可,注意腳位絲印 |
| 濕度感測 | AHT20(I2C) | 1 | ~35 元 | ±2%RH;不要用 DHT11——誤差 ±5% 且反應慢,閉環會震盪 |
| 霧化片 | 24mm 超聲波霧化片 + 驅動板 | 1 | ~30 元 | 25W peak;買含驅動板的模組,別買裸片 |
| 顯示 | SSD1306 0.96" OLED | 1 | ~75 元 | I2C 0x3C,與 AHT20 共總線 |
| 送風 | 4010 5V 風扇 | 1 | ~45 元 | 把霧吹出霧道 + 冷卻霧化片 |
| 功率開關 | IRLZ44N MOSFET 模組 | 2 | ~18 元 | 霧化片、風扇各一;模組含柵極下拉電阻 |
| 缺水偵測 | MC-110 浮球開關(NC) | 1 | ~15 元 | NC 常閉:有水=閉合,與 ESP32 共地 |
| 電源 | 5V 3A USB 電源 | 1 | ~75 元 | 霧化片 25W peak,1A 的舊手機頭帶不動會重啟迴圈 |
| 雜項 | 矽膠管、熱縮管、M2/M3 螺絲、雙面膠 | 1 | ~30 元 | 杜邦線黑紅黃各 3 |
| 合計 | ~381 元 |
🔌 接線圖
ASCII 對照表
ESP32-C2 SuperMini
5V ○── 紅線 ──┬── 霧化片 MOS 模組 VIN
GND ○── 黑線 ──┼── 風扇 MOS 模組 VIN(黑線與模組 GND 共地)
GPIO4 ○── 藍線 ──┬── OLED SDA ──┐
GPIO5 ○── 藍線 ──┤ ├── AHT20 SDA / SCL(I2C 共總線)
GPIO6 ○── 黃線 ──── 霧化片 MOS 模組 IN
GPIO7 ○── 黃線 ──── 風扇 MOS 模組 IN
GPIO3 ○── 綠線 ──── 浮球開關 NC ── GND(有水閉合拉低)
3V3 ○── 紅線 ──── OLED VCC、AHT20 VCC
※ 避開 GPIO8/9(strapping 開機腳)與 GPIO18/19(USB)
詳細接線
| ESP32-C2 腳位 | 連接 | 說明 |
|---|---|---|
| GPIO4 / GPIO5 | OLED + AHT20 的 SDA / SCL | 兩顆 I2C 裝置地址不同(0x3C / 0x38)可共線;總線已內建上拉勿再加電阻 |
| GPIO6 | 霧化片 MOSFET 模組 IN | 模組內含柵極下拉:MCU 重啟瞬間霧化片不會誤開 |
| GPIO7 | 風扇 MOSFET 模組 IN | 同上;風扇啟停由「霧化 + 30 秒延遲」邏輯控制 |
| GPIO3 | 浮球開關(NC)另一端接 GND | 啟用內部上拉;有水=閉合=LOW。浮球不進控制迴路,只做偵測,斷水由韌體切 MOSFET——邏輯集中、除錯只查一處 |
| 5V / GND | 兩個 MOS 模組 VIN / GND | 霧化片 25W 大電流走 5V 直供,不經過板子 3.3V LDO |
| 3V3 / GND | OLED、AHT20 | 感測與顯示走 3.3V,與 I2C 電位一致 |
⚠️ 霧化片驅動板與水同處一箱:驅動板裸露部分務必套熱縮管並固定在水面以上至少 4cm。5V 電壓碰水不會電死人,但會電鍍——兩週內把導線電解腐蝕斷,然後你就有一個泡在水裡的漏電源。
🖨️ 3D 列印零件
零件清單
| 零件 | 數量 | 材質建議 | 顏色 | 列印時間 |
|---|---|---|---|---|
| 水箱主體 | 1 | PETG | 白色 | ~2.5 hr |
| 上蓋(含控制艙) | 1 | PETG | 白色 | ~1 hr |
| 霧化片浮盤 | 1 | TPU(墊圈)+ PLA(盤) | 灰 / 白 | ~40 min |
| L 型霧道導管 | 1 | PETG | 透明 | ~35 min |
| 控制面板(OLED 窗) | 1 | PLA | 深綠 | ~40 min |
列印設定
- 材質:水箱相關一律 PETG——PLA 長期泡水解體,三個月後微裂滲水;控制面板離水遠,PLA 美觀即可
- 壁厚:水箱 4 層(1.6mm)、上蓋 3 層;填滿 100% + 頂/底實心 4 層,層間就是滲水路徑
- 層厚:水箱 0.16mm(比標準 0.2 更密,防水第一)
- 防水後處理:內壁刷一層水性聚氨酯清漆(水族用),比環氧便宜且不黃
設計要點
- AHT20 乾燥艙(本專案設計靈魂):上蓋右側一個與霧道完全隔離的小艙,感測器只從側向兩個 3mm 進氣孔聞房間的濕氣。感測器若湊近噴口,讀數永遠 90%+,閉環形同虛設——這是 DIY 加濕器讀數偏高的頭號原因
- 濕區 / 乾區物理分隔:上蓋左半是霧區(霧化片、水面),右半是電子艙(ESP32、MOS、OLED 背面),中間 8mm 隔牆,走線走隔牆預留孔 + 熱縮密封
- 霧化片浮盤:霧化片固定在 EVA 浮盤上,水位高低自動跟隨——水位下降時霧化片露出水面乾燒,是霧化片壽命的頭號殺手
- 霧道 L 型導管:霧化腔頂部 → 風扇入口,出口朝上 45° 不直吹桌面
- 底部走線孔 + raised 10mm 底腳:電子艙萬一凝露可排出,不泡自家水
💻 程式碼
📐 註解規範:區塊以框註分段;函式以框註說明用途/參數/回傳;行內註解一律解釋「為什麼」而非「做什麼」。全系列文章採同一規範。
📚 相依函式庫:Arduino IDE 開發板管理員選「esp32 by Espressif(v3.x,C2 支援)」;Lib Manager 裝
📚 相依函式庫:Arduino IDE 開發板管理員選「esp32 by Espressif(v3.x,C2 支援)」;Lib Manager 裝
Adafruit SSD1306 + Adafruit GFX Library。AHT20 直接手刻 I2C,免裝庫。/* ============================================================
* ESP32-C2 桌面智能加濕器 | Smart Humidifier
* ------------------------------------------------------------
* 功能:AHT20 濕度閉環控制超聲波霧化片(47/52% 遲滯)、
* 缺水自動斷霧 + Telegram 推播、餘霧吹乾防霉
* 硬體:ESP32-C2 SuperMini + AHT20(0x38) + SSD1306(0x3C)
* + 霧化片 MOS(GPIO6) + 風扇 MOS(GPIO7) + 浮球(GPIO3)
* 供電:USB 5V 3A(霧化片 25W peak,勿省電源錢)
* 相依:Arduino core for ESP32 v3.x / Adafruit_SSD1306 /
* Adafruit_GFX / Wire / WiFi / HTTPClient
* ============================================================ */
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <HTTPClient.h>
/* ------------------------------------------------------------
* 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"; // @BotFather 發行
const char* TG_CHAT_ID = "YOUR_CHAT_ID"; // 純數字 chat id
/* ------------------------------------------------------------
* 2. 腳位與閾值
* ------------------------------------------------------------ */
const uint8_t PIN_SDA = 4;
const uint8_t PIN_SCL = 5;
const uint8_t PIN_ATOM = 6; // 霧化片 MOS IN
const uint8_t PIN_FAN = 7; // 風扇 MOS IN
const uint8_t PIN_WATER = 3; // 浮球 NC→GND:有水=LOW(啟用內部上拉)
const float RH_START = 47.0; // 低於 → 開噴
const float RH_STOP = 52.0; // 高於 → 停噴(遲滯帶 5%,防邊界振盪)
const uint32_t ATOM_MIN_ON_MS = 10UL * 1000; // 最短連續出霧:防「剛開就停」短迴路
const uint32_t ATOM_MAX_ON_MS = 40UL * 60 * 1000; // 最長連續出霧:小房間防濕度過衝
const uint32_t ATOM_COOLDOWN_MS = 20UL * 60 * 1000; // 達上限後強制休息 20 分鐘
const uint32_t FAN_DRY_RUN_MS = 30UL * 1000; // 停霧後風扇續吹 30 秒排殼內殘霧
const uint8_t WATER_DEBOUNCE_N = 3; // 浮球取樣 N 次一致才翻態(水面波動防誤報)
/* ------------------------------------------------------------
* 3. 全域狀態
* ------------------------------------------------------------ */
Adafruit_SSD1306 display(128, 64, &Wire, -1);
bool atomOn = false; // 目前霧化片狀態
bool fanOn = false;
bool waterPresent = true; // 去抖後的浮球狀態
bool waterAlertSent = false; // 已推播「缺水」旗標:防洗版
uint32_t atomOnMsAt = 0; // 本輪出霧開始時間
uint32_t atomOffMsAt = 0; // 上次停霧時間(風扇延遲判斷)
uint32_t cooldownUntil = 0; // 強制休息結束時刻(millis 時基)
float rhSmooth = 50.0; // 顯示/控制用濕度
uint8_t sensorErrCnt = 0; // AHT20 連續讀取失敗計數
/* ------------------------------------------------------------
* aht20Read —— 讀取 AHT20 濕度(手刻 I2C,免第三方庫)
* 回傳:true=成功,rh 填入 0~100 濕度
* 做法:0xAC 0x33 觸發 → 輪詢 busy 位(上限 200ms)→ 讀 6 bytes
* 為什麼手刻:AHT20 協議只有 10 行,多裝一個庫不如自己寫清楚
* ------------------------------------------------------------ */
bool aht20Read(float& rh) {
Wire.beginTransmission(0x38);
Wire.write(0xAC); Wire.write(0x33); Wire.write(0x00); // 觸發測溫濕
if (Wire.endTransmission() != 0) return false;
uint8_t st;
uint32_t t0 = millis();
do { // busy 輪詢,200ms 逾時
Wire.requestFrom((uint8_t)0x38, (uint8_t)1);
st = Wire.read();
if (millis() - t0 > 200) return false; // 感測器卡死 → 上層軟復位
delay(5);
} while (st & 0x80);
Wire.requestFrom((uint8_t)0x38, (uint8_t)6);
if (Wire.available() < 6) return false;
uint32_t r20 = ((uint32_t)Wire.read() << 12) |
((uint32_t)Wire.read() << 8) |
((uint32_t)Wire.read() << 4);
Wire.read(); Wire.read(); Wire.read(); // 溫度欄本專案不用
rh = (float)r20 / 1048576.0f * 100.0f;
return (rh > 0.0f && rh < 101.0f); // 全零/全 F 視為無效
}
/* ------------------------------------------------------------
* readHumidity —— 中位數濾波讀濕度
* 回傳:0~100 RH;連續失敗時自動軟復位(0xBA)重試一次
* 為什麼中位數:風扇啟停的電源紋波會讓 ADC 端偶發突波,
* 平均數會被單點拉偏,中位數不會
* ------------------------------------------------------------ */
float readHumidity() {
float v[3]; uint8_t ok = 0;
for (int i = 0; i < 3; i++) {
if (aht20Read(v[ok])) ok++;
delay(80); // AHT20 兩讀之間留恢復時間
}
if (ok == 0) { // 全部失敗:軟復位再掙扎一次
Wire.beginTransmission(0x38);
Wire.write(0xBA); // soft reset
Wire.endTransmission();
delay(20);
if (!aht20Read(v[0])) return -1; // 放棄,回 -1 由上層計錯
ok = 1;
}
// 排序取中位數(3 個元素,氣泡排序足矣)
for (int i = 0; i < ok - 1; i++)
for (int j = 0; j < ok - 1 - i; j++)
if (v[j] > v[j + 1]) { float t = v[j]; v[j] = v[j + 1]; v[j + 1] = t; }
return v[ok / 2];
}
/* ------------------------------------------------------------
* readWater —— 浮球開關去抖
* 回傳:true=有水。100ms × 3 次取樣全一致才翻態
* 為什麼:水面晃動時浮球可能在臨界點抖動,單次讀取會
* 「缺水→報警→有水→恢復」連環誤報
* ------------------------------------------------------------ */
bool readWater() {
static bool last = true;
uint8_t high = 0;
for (int i = 0; i < WATER_DEBOUNCE_N; i++) {
if (digitalRead(PIN_WATER) == HIGH) high++; // HIGH = NC 斷開 = 缺水
delay(100);
}
bool now = (high == 0); // 全部 LOW 才判定有水
if (now == last) return last;
delay(500); // 邊緣態多等一輪確認
if (now == last) return last;
last = now;
return last;
}
/* ------------------------------------------------------------
* sendTelegram —— 推播訊息到 Telegram Bot
* 參數:message 文字;回傳:是否 HTTP 200
* ------------------------------------------------------------ */
bool sendTelegram(const String& message) {
if (WiFi.status() != WL_CONNECTED) return false; // 離線直接放棄,下輪再試
HTTPClient http;
String url = String("https://api.telegram.org/bot") + TG_BOT_TOKEN + "/sendMessage";
http.begin(url);
http.addHeader("Content-Type", "application/json");
String json = String("{\"chat_id\":\"") + TG_CHAT_ID +
"\",\"parse_mode\":\"Markdown\",\"text\":\"" + message + "\"}";
int code = http.POST(json);
bool ok = (code == 200);
if (!ok) Serial.printf("[TG] HTTP %d\n", code);
http.end(); // C2 RAM 小,務必釋放
return ok;
}
/* ------------------------------------------------------------
* setAtom / setFan —— 功率輸出的唯一出入口
* 為什麼包函式:所有狀態時間戳在這裡統一更新,
* 主迴路就不會到處散落 digitalWrite 造成時間戳不一致
* ------------------------------------------------------------ */
void setAtom(bool on) {
if (on == atomOn) return;
atomOn = on;
digitalWrite(PIN_ATOM, on ? HIGH : LOW);
if (on) atomOnMsAt = millis(); else atomOffMsAt = millis();
Serial.printf("[%lus] ATOM %s (RH=%.1f)\n", millis() / 1000, on ? "ON" : "OFF", rhSmooth);
}
void setFan(bool on) {
if (on == fanOn) return;
fanOn = on;
digitalWrite(PIN_FAN, on ? HIGH : LOW);
}
/* ------------------------------------------------------------
* uiDraw —— OLED 繪製狀態頁
* 左半:大字号濕度;右半:濕度直方圖(近 24 根柱 = 48 分鐘)
* 底列:狀態字(RUN/IDLE/DRY/ERR)+ Wi-Fi 圖點
* ------------------------------------------------------------ */
uint8_t hist[24]; uint8_t histIdx = 0; uint32_t histAt = 0;
void uiDraw(const char* state) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(2);
if (rhSmooth >= 0) display.printf("%2.0f%%", rhSmooth);
else display.print("ERR");
display.setTextSize(1);
display.setCursor(0, 22);
display.printf("TARGET 47-52%%");
display.setCursor(0, 34);
display.printf("%s WS:%s", state, waterPresent ? "OK" : "DRY");
display.setCursor(0, 46);
display.printf("WIFI:%s", WiFi.status() == WL_CONNECTED ? "ON " : "OFF");
// 右側直方圖:30 秒一根柱,可視範圍 30~80%RH
for (int i = 0; i < 24; i++) {
uint8_t v = hist[(histIdx + i) % 24];
uint8_t h = map(v, 30, 80, 0, 40);
if (h) display.drawFastVLine(78 + i * 2, 63 - h, h, SSD1306_WHITE);
}
display.display();
}
void setup() {
Serial.begin(115200);
delay(1000); // 等 USB-CDC 就緒
pinMode(PIN_ATOM, OUTPUT); digitalWrite(PIN_ATOM, LOW);
pinMode(PIN_FAN, OUTPUT); digitalWrite(PIN_FAN, LOW);
pinMode(PIN_WATER, INPUT_PULLUP); // NC 浮球:有水拉低
Wire.begin(PIN_SDA, PIN_SCL);
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
display.display();
Wire.beginTransmission(0x38); // AHT20 上電校準指令
Wire.write(0x21);
Wire.endTransmission();
delay(20);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
for (int i = 0; i < 60 && WiFi.status() != WL_CONNECTED; i++) {
delay(500); // 30 秒逾時,之後背景重試
Serial.print(".");
}
Serial.println("\n加濕器控制迴路上線");
}
/* ------------------------------------------------------------
* loop —— 1 秒一輪的控制狀態機
* 優先序:感測錯誤 > 缺水 > 強制休息 > 遲滯閉環
* ------------------------------------------------------------ */
void loop() {
/* --- 1. 感測:讀濕度,連續 5 輪失敗才升級為錯誤 --- */
float rh = readHumidity();
if (rh < 0) {
if (++sensorErrCnt >= 5) { uiDraw("ERR "); delay(4000); return; } // 感測掛了先休 4 秒再試
} else {
sensorErrCnt = 0;
rhSmooth = rhSmooth * 0.7f + rh * 0.3f; // EMA 平滑:顯示不閃但控制反應仍夠快
}
/* --- 2. 缺水:最高優先級,立即斷霧 + 吹乾 + 推播 --- */
waterPresent = readWater();
if (!waterPresent) {
setAtom(false);
if (millis() - atomOffMsAt < 60UL * 1000) setFan(true); // 缺水也吹殘霧 60 秒
else setFan(false);
if (!waterAlertSent && sendTelegram("💧 **加濕器缺水了**\n請補水,補水後自動恢復。"))
waterAlertSent = true;
uiDraw("DRY ");
delay(900);
return;
}
if (waterAlertSent) { // 補回水了:推恢復、重置旗標
if (sendTelegram("✅ **加濕器已補水,恢復運行。**")) waterAlertSent = false;
}
/* --- 3. 時間比例上限:連噴滿 40 分鐘強制休息 20 分鐘 --- */
uint32_t now = millis();
if (atomOn && now - atomOnMsAt > ATOM_MAX_ON_MS) {
setAtom(false);
cooldownUntil = now + ATOM_COOLDOWN_MS;
}
bool inCooldown = (now < cooldownUntil);
/* --- 4. 遲滯閉環:47 開 / 52 停 + 最短運行時間 --- */
if (!atomOn && !inCooldown && rhSmooth <= RH_START && sensorErrCnt == 0)
setAtom(true);
if (atomOn) {
bool longEnough = (now - atomOnMsAt >= ATOM_MIN_ON_MS);
if (longEnough && (rhSmooth >= RH_STOP || sensorErrCnt > 0))
setAtom(false); // 感測失效時預設停噴(濕度不可信=寧可不加濕)
}
/* --- 5. 風扇:出霧即吹;停霧後延遲 30 秒吹乾殘霧 --- */
setFan(atomOn || (now - atomOffMsAt < FAN_DRY_RUN_MS));
/* --- 6. UI:每 30 秒存一根直方圖柱,每輪刷畫面 --- */
if (now - histAt >= 30000) {
histAt = now;
hist[histIdx] = (uint8_t)constrain(rhSmooth, 0, 100);
histIdx = (histIdx + 1) % 24;
}
uiDraw(atomOn ? "RUN " : (inCooldown ? "REST" : "IDLE"));
delay(950); // ~1 秒控制週期,讓出 CPU 給 Wi-Fi 堆疊
}
🏗️ 組裝步驟
📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
- 列印與防水測試:五個零件依設定列印;水箱裝滿水靜置 2 小時,層間滲水就補刷清漆
- 組霧化浮盤:霧化片振動面朝上裝浮盤(滴水通電應見水花立起,錯面水花軟趴趴),導線套熱縮管打矽膠密封
- 接線與鎖固:按接線圖接線;驅動板鎖上蓋電子艙內壁,線過隔牆孔後熱縮密封
- 裝浮球:浮球導線穿底部走線孔進電子艙,浮球掛在水箱導槽內可自由上下
- 燒錄與驗證:燒錄 → 開序列埠 → 對 AHT20 哈一口氣,濕度應 3 秒內衝上 80%+,鬆手後緩慢回落——跳不動就是裝在乾燥艙外或共地不良
- 試機定位:裝水加霧,出霧口朝上 45°,離桌面電子產品 1 公尺以上;24 小時後摸出霧口前方桌面,應完全乾燥
🔧 問題排除
| 問題 | 原因 | 解決方法 |
|---|---|---|
| 完全不出霧 | 驅動板供電不足 / 霧化片面裝反 / 水位感應環路誤斷 | 換 5V 3A 電源確認;滴水試霧檢查朝向;Serial 看 waterPresent 是否誤報缺水 |
| 濕度永遠讀 90%+、從不噴霧 | AHT20 太靠近霧道,吸的是純霧 | 確認感測器真的鎖在乾燥艙、艙體密封條到位 |
| 開關機迴圈、Wi-Fi 斷斷續續 | 霧化片啟動浪湧拉垮 5V | 電源升 3A;霧化片供電線加粗到 20AWG 並單獨回板 GND(星型接地) |
| 缺水誤報 | 水面波動讓浮球抖動 | 本韌體已三次去抖;仍誤報就把水箱墊平,浮球導槽加阻尼(貼一圈毛氈) |
| 桌面一層白粉 | 自來水礦物質被霧化成「白粉病」 | 改用純水/RO 水(冷霧加濕器通病,與本專案無關但必踩) |
| 殼內水珠、黴味 | 霧道凝露長期未吹乾 | 確認停霧後風扇有續吹 30 秒;霧道改透明件便於檢查,每月擦洗霧化片水垢 |
🎯 進階延伸
- Home Assistant:把
sendTelegram()換成 MQTT publish(Wi-Fi 現成),HA 端就能把濕度曲線接進自動化(例如濕度低於 40% 順手關窗型除濕機) - 電池版:18650 + TP4056 + 升壓板,loop 改 light sleep + GPIO 喚醒,平均電流可壓到 3mA 以下(Wi-Fi 只在推播時開)
- 多房联控:每房一顆 C2 當純感測節點(深睡 30 分鐘醒一次報 MQTT),主機統一開關——感測點離人近、控制點離水近
- 溫補償顯示:AHT20 本體就有溫度欄,OLED 加顯示露點溫度——「離牆壁凝水還剩幾度」比濕度百分比直观得多
📦 檔案清單
smart-humidifier/
├── src/
│ └── smart-humidifier.ino # ESP32-C2 程式碼(本頁完整程式)
├── stl/
│ ├── tank-body.stl # 水箱主體(PETG)
│ ├── top-lid.stl # 上蓋含乾燥艙(PETG)
│ ├── fog-guide.stl # L 型霧道(PETG 透明)
│ ├── float-plate.stl # 霧化片浮盤(PLA+TPU 墊)
│ └── panel.stl # 控制面板
├── f3d/
│ └── smart-humidifier.f3d # Fusion 360 原始檔
├── manual/
│ └── assembly.html # 圖文組裝說明書
└── README.md # 說明文件