📋 專案概述

12 月的深夜四點半,我是被喉嚨乾醒的。冷氣無聲地抽了一整晚的水,書房濕度計顯示 32%——這個數字對木吉他、木窗框和我的鼻黏膜都是壞消息。上個月女兒一周流了兩次鼻血,醫生只說了一句「房間太乾」。

市面上的智能加濕器查了一圈,結論是這個品類的「智能」兩個字含水量比它噴出来的霧還高:

  1. 千元級的「智能」= 手機遠開關:App 上那個開關按鈕,和我蹲在機器前面按的那個開關,唯一的差別是要先解鎖手機。它依然不會看濕度——設定 8 小時定時,半夜濕度衝到 75% 照樣噴,枕頭邊床單摸起來是潮的。
  2. 濕度計 + 傳統加濕器 = 請自己半夜起來關機器:我試過買一個 300 元的加濕器配 200 元的濕度計,結果是設定鬧鐘起來關霧,連續一週後鬧鐘被我關了。
  3. 真正的痛是過濕,不是乾:冷霧本質是把純水打成微粒直吹,出霧速度永遠比房間擴散快。直吹桌面三小時,筆電鍵盤縫裡能長出鏽斑——會停的加濕器才叫智能,不會停的叫霧化事故。

這個專案不買新加濕器,而是把家樂福 300 元的傳統超聲波加濕器「換腦」:用 不到 400 元的零件 + 3D 列印水箱,做一台真正看得見濕度的機器:

成本效益算給你聽:千元級「智能」加濕器 1,299 元,功能清單裡沒有一項是本文第三、第四點;本專案 381 元,還多一個可以無限改韌體的 Wi-Fi 主控。真正的智能不是連網,是在正確濕度自己停下來。

為什麼選 ESP32-C2?

需求C2 的優勢
成本全系列最便宜的 Wi-Fi 板之一,SuperMini 單片 ~40 元,本專案預算大頭該留給電源和感測器
效能剛剛好閉環控制 1 秒一輪、感測器只有 I2C 兩顆——單核 C2 綽綽有餘,S3 的錢花在這裡會後悔
功耗餘裕深度睡眠 ~5µA,日後改鋰電池 + 太陽能不用換板重設計
體積25 × 18 mm,藏進上蓋控制艙完全不佔加水口

🧱 硬體清單

項目型號數量預估單價備註
主控板ESP32-C2 SuperMini1~40 元各廠 C2 SuperMini 皆可,注意腳位絲印
濕度感測AHT20(I2C)1~35 元±2%RH;不要用 DHT11——誤差 ±5% 且反應慢,閉環會震盪
霧化片24mm 超聲波霧化片 + 驅動板1~30 元25W peak;買含驅動板的模組,別買裸片
顯示SSD1306 0.96" OLED1~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 元

🔌 接線圖

ESP32-C2 SuperMini USB-C(供電+燒錄) 5V GND GPIO4/5 GPIO6/7 GPIO3 RH 48% SSD1306 OLED SDA SCL VCC GND AHT20 濕度計 0x38 · 裝乾燥艙! SDA SCL 3V3 GND 霧化片 25W 經 IRLZ44N 模組 IN←GPIO6 · VIN=5V 40mm 送風扇 經 IRLZ44N 模組 IN←GPIO7 · VIN=5V 浮球 NC → GND 藍線 ×2 GPIO4/5 → SDA/SCL(OLED+AHT20 共線) 黃線 GPIO6→霧化 IN、GPIO7→風扇 IN 綠線 GPIO3 → 浮球(內部上拉,有水=LOW) 紅線 5V / 黑線 GND → 各模組 VIN(霧化、風扇走 5V;感測走 3V3)
▲ 兩條 I2C 線掛兩個感測器;霧化片與風扇各自經 MOSFET 模組接 5V 大電流,ESP32 只吃控制訊號。

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 / GPIO5OLED + 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 / GNDOLED、AHT20感測與顯示走 3.3V,與 I2C 電位一致
⚠️ 霧化片驅動板與水同處一箱:驅動板裸露部分務必套熱縮管並固定在水面以上至少 4cm。5V 電壓碰水不會電死人,但會電鍍——兩週內把導線電解腐蝕斷,然後你就有一個泡在水裡的漏電源。

🖨️ 3D 列印零件

零件清單

零件數量材質建議顏色列印時間
水箱主體1PETG白色~2.5 hr
上蓋(含控制艙)1PETG白色~1 hr
霧化片浮盤1TPU(墊圈)+ PLA(盤)灰 / 白~40 min
L 型霧道導管1PETG透明~35 min
控制面板(OLED 窗)1PLA深綠~40 min

列印設定

設計要點

  1. AHT20 乾燥艙(本專案設計靈魂):上蓋右側一個與霧道完全隔離的小艙,感測器只從側向兩個 3mm 進氣孔聞房間的濕氣。感測器若湊近噴口,讀數永遠 90%+,閉環形同虛設——這是 DIY 加濕器讀數偏高的頭號原因
  2. 濕區 / 乾區物理分隔:上蓋左半是霧區(霧化片、水面),右半是電子艙(ESP32、MOS、OLED 背面),中間 8mm 隔牆,走線走隔牆預留孔 + 熱縮密封
  3. 霧化片浮盤:霧化片固定在 EVA 浮盤上,水位高低自動跟隨——水位下降時霧化片露出水面乾燒,是霧化片壽命的頭號殺手
  4. 霧道 L 型導管:霧化腔頂部 → 風扇入口,出口朝上 45° 不直吹桌面
  5. 底部走線孔 + raised 10mm 底腳:電子艙萬一凝露可排出,不泡自家水

💻 程式碼

📐 註解規範:區塊以框註分段;函式以框註說明用途/參數/回傳;行內註解一律解釋「為什麼」而非「做什麼」。全系列文章採同一規範。
📚 相依函式庫: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 組裝說明書
  1. 列印與防水測試:五個零件依設定列印;水箱裝滿水靜置 2 小時,層間滲水就補刷清漆
  2. 組霧化浮盤:霧化片振動面朝上裝浮盤(滴水通電應見水花立起,錯面水花軟趴趴),導線套熱縮管打矽膠密封
  3. 接線與鎖固:按接線圖接線;驅動板鎖上蓋電子艙內壁,線過隔牆孔後熱縮密封
  4. 裝浮球:浮球導線穿底部走線孔進電子艙,浮球掛在水箱導槽內可自由上下
  5. 燒錄與驗證:燒錄 → 開序列埠 → 對 AHT20 哈一口氣,濕度應 3 秒內衝上 80%+,鬆手後緩慢回落——跳不動就是裝在乾燥艙外或共地不良
  6. 試機定位:裝水加霧,出霧口朝上 45°,離桌面電子產品 1 公尺以上;24 小時後摸出霧口前方桌面,應完全乾燥

🔧 問題排除

問題原因解決方法
完全不出霧驅動板供電不足 / 霧化片面裝反 / 水位感應環路誤斷換 5V 3A 電源確認;滴水試霧檢查朝向;Serial 看 waterPresent 是否誤報缺水
濕度永遠讀 90%+、從不噴霧AHT20 太靠近霧道,吸的是純霧確認感測器真的鎖在乾燥艙、艙體密封條到位
開關機迴圈、Wi-Fi 斷斷續續霧化片啟動浪湧拉垮 5V電源升 3A;霧化片供電線加粗到 20AWG 並單獨回板 GND(星型接地)
缺水誤報水面波動讓浮球抖動本韌體已三次去抖;仍誤報就把水箱墊平,浮球導槽加阻尼(貼一圈毛氈)
桌面一層白粉自來水礦物質被霧化成「白粉病」改用純水/RO 水(冷霧加濕器通病,與本專案無關但必踩)
殼內水珠、黴味霧道凝露長期未吹乾確認停霧後風扇有續吹 30 秒;霧道改透明件便於檢查,每月擦洗霧化片水垢

🎯 進階延伸

📦 檔案清單

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                  # 說明文件
← 上一篇:ESP32-S3 桌面網路收音機 📚 系列索引 下一篇:ESP32-C3 夾式吉他調音器 →