📋 專案概述

去年秋天我打開浴室燈,天花板角落那一片蜘蛛網狀的黑黴第一次讓我認真計算一件事:台灣一間標準衛浴除黴+磁磚縫重新拷膠的行情是 3,000~8,000 元,而整件事的起因只是「排風扇有人記得開、沒人記得關」。洗澡後鏡面起霧只是表象——真正的傷害是牆面與矽利康在 75~95% 相對濕度裡泡的那兩三個鐘頭,黴變反應在这个區間跑得最歡。

市售方案我用了兩套,都翻車:

  1. 濕度感應排風模組(600~900 元):邏輯是「濕度超標→轉、到達→停」。問題在於它完全不知道人在不在——泡澡時濕度爬到最高點、也是你最需要排風的時刻,它常常剛好選擇停機休息;而且它只看當下濕度,回南天那種 68% 悶整天的「不超標但一直在發黴」狀態,它完全無感。
  2. 定時插座:洗澡時間從來不準時,本質上是賭博。

這個專案用 ESP32-S3 + 毫米波雷達 + 高精度濕度計,把排風扇交給三個互相兜底的判據:

成本帳:一台濕度感应排風模組 600~900 元、外加除黴工班 3,000 元起跳;本專案 613 元,而且韌體在你手上——滯回區間、後吹時間、日報文案,全是可以改一行的事。

為什麼選 ESP32-S3?

需求S3 的優勢
多外設同時跑環燈呼吸動畫(NeoPixel DMA 刷新)、I2C 取樣、HTTPS 推播同時進行——雙核心把動畫釘在 core1、Wi-Fi 協定堆疊獨佔 core0,環燈永遠不卡頓
恆供電裝置本機接浴室燈電源降 5V,µA 級低功耗用不上——要的是餘裕不是省電,同價位的 C2 在此就是白省
安裝環境原生 USB 直接從殼底检修孔燒錄;腳位餘裕大到把雷達、濕度、SSR、環燈全接完還剩一組 UART
延伸預留S3 是全系列唯一帶攝像頭併口的型號——日後加顆 OV2640 用視覺比對黴斑面積,主機不用換

🧱 硬體清單

項目型號數量預估單價備註
主控板ESP32-S3-DevKitC-1(N16R8)1~165 元N8R2 亦可;本專案不吃 PSRAM,選便宜的可再壓 30 元
存在感測海凌科 LD2410 毫米波雷達1~160 元買帶 OUT 腳的原版 LD2410;LD2410C 無 OUT 腳需改走 UART(見進階延伸)
濕度感測SHT40 模組(I2C)1~85 元±1.8%RH,比 DHT22 準一個等級且不吃單線時序——浴室潮氣環境線路要越簡單越好
風負載開關固態繼電器模組(SPC5821 過零型)1~65 元排風扇是電感性負載,過零 SSR 壽命是繼電器的 3~10 倍且無接點彈跳噪音
狀態指示WS2812B-49 環形燈板(直徑 60mm)1~45 元貼面蓋內側當「狀態瞳孔」,磨砂面蓋同時是擴散板
電源5V 1A USB 降壓模組(燈座取電)1~38 元從浴室燈燈座並下 5V;不動強電者改走天花板检修口拉 USB 線
外殼底基86 壁裝盒 + 3D 列印適配鎖套1~30 元利用既有開關孔位走線,不倒漆不鑽新孔
雜項端子台、防滴水面蓋、雙面膠、熱縮管1~25 元面蓋預留 SHT40 進氣縫與雷達窗口
合計~613 元

🔌 接線圖

ESP32-S3 DevKitC-1 · 倒裝於防滴殼 USB-C(检修孔) GPIO7 RADAR GPIO8/9 I2C GPIO15 SSR GPIO48 RGB 24GHz LD2410 毫米波 貼雷達窗、面對淋浴區 OUT → GPIO7 VCC→5V、GND→GND(圖面省略細線) SHT40(進氣縫上方) SDA/SCL → GPIO8/9 過零 SSR 模組 IN+ GPIO15 / IN- GND 110V 全封閉阻燃盒 GPIO15 → IN+ 火線(紅虛線) 中性線(黑虛線) 排風扇(110V) 火線串 SSR 的 ~ ~ 端,不得繞過 WS2812 環燈(面蓋內側) DIN → GPIO48(5V 供電、GND 共地)
▲ 全機唯一 110V 接點就是 SSR 那顆,其餘全部待在同個 5V 低壓域裡——強電封在阻燃盒、低壓掛在防滴殼,兩域只透過 SSR 的光耦對話。

ASCII 對照表

ESP32-S3 DevKitC-1 LD2410 / SHT40 / SSR / 環燈 ───────────────── ───────────────────────── 5V ──── 紅線 ──── LD2410 VCC、環燈 5V GND ──── 黑線 ──── LD2410 GND、SHT40 GND、SSR IN-、環燈 GND GPIO7 ──── 黃線 ──── LD2410 OUT(有人=HIGH) GPIO8 ──── 藍線 ──── SHT40 SDA GPIO9 ──── 藍線 ──── SHT40 SCL GPIO15 ──── 綠線 ──── SSR IN+(HIGH=風扇轉) GPIO48 ──── 橘線 ──── WS2812 環燈 DIN (LD2410 TX/RX 主線路不接,出廠 OUT 即為「移動=HIGH」;要微動靈敏度再並 UART 調參) ※ SHT40 用 3V3 供電,讀值直接對 3.3V 域;SSR 輸入側內部已有限流電阻,GPIO 直推 ※ 110V 側:插座火線 → SSR「~」,SSR「~」→ 風扇火線;中性線直通不進 SSR

詳細接線

ESP32-S3 腳位連接說明
GPIO7LD2410 OUTS3 任何 GPIO 皆可中斷喚醒與讀取;此腳只有雷達在用,預留上拉即可免焊 10k
GPIO8 / GPIO9SHT40 SDA / SCLS3 的 Arduino 預設 I2C 腳,免 setPins;模組自带 10k 上拉,線長 30cm 內免外加
GPIO15SSR IN+過零型 SSR 輸入 3–32VDC,3.3V 邏輯直推沒問題;IN- 回 GND(低壓域共地)
GPIO48WS2812 DIN環燈 5V 供電但資料是 3.3V 位準——S3 GPIO 直接安全;上電瞬間環燈會閃一下屬正常,程式首幀先全滅
5V / GND燈座降壓模組降壓模組鎖在燈座天花板側,USB 線穿检修口下來——浴室裡不出現任何裸露 110V
⚠️ 強電三步鐵律:①切斷浴室燈斷路器後再用驗電筆確認才動線;②SSR 兩端 110V 全部鎖進阻燃盒、鎖好才通電;③第一次通電測風扇要用不含人的遠端測試(插馬桶沖水開關之類去碰開關都別笑——溼手碰強電才是本專案唯一會死人的劇本)。

🖨️ 3D 列印零件

零件清單

零件數量材質建議顏色列印時間
防滴主殼(倒扣式)1PLA白~3 hr
透波面蓋(雷達窗+進氣縫)1PLA(0° 圓盤紋)半透白~45 min
環燈導光圈1PLA(磨砂面朝內)白~20 min
86 盒適配鎖套2PETG(鎖孔受力件)白~15 min×2
SSR 阻燃盒適配蓋1PETG黑~25 min

列印設定

設計要點

  1. 雷達窗正對淋浴區:面蓋上無開孔(防水),24GHz 直接穿 1.6mm PLA;LD2410 天線面貼窗內側,外殼-metal 件(螺絲)離天線 10mm 以上
  2. 散熱欄柵朝下+滴水彎道:殼體進出氣走 L 形迷宮,淋浴水霧進不來、SHT40 讀的是被動擴散的室溫濕度而不是淋到的水
  3. 電路板豎插導槽:DevKitC-1 長邊插進導槽再鎖兩顆 M3,检修時整片「抽屜式」抽出,不用拆殼翻線
  4. 環燈導光圈懸浮卡裝:燈環貼圈內側、圈卡進面蓋凹槽——狀態燈從整個面蓋透出,看不出有任何 LED 顆粒

💻 程式碼

📐 註解規範:區塊以框註分段;函式以框註說明用途/參數/回傳;行內註解一律解釋「為什麼」而非「做什麼」。全系列文章採同一規範。
📚 相依:板子選「ESP32S3 Dev Module」,USB CDC On Boot: Enabled;Lib Manager 裝 Adafruit SHT4X Library + Adafruit NeoPixel。主迴路跑在 core1、Wi-Fi 堆疊在 core0(ESP32 Arduino 預設配置),環燈動畫不吃中斷延遲。
/* ============================================================
 *  ESP32-S3 浴室潮氣哨兵  |  Bathroom Moisture Guard
 *  ------------------------------------------------------------
 *  功能:毫米波人在即排風 + SHT40 濕度滯回 + 後吹乾燥 12 分
 *        + 潮浸積分日報(Telegram)+ WS2812 狀態環燈
 *  硬體:ESP32-S3 DevKitC-1 + LD2410(OUT=GPIO7) + SHT40(GPIO8/9)
 *        + 過零SSR(GPIO15) + WS2812環燈(GPIO48, 49顆)
 *  供電:燈座降壓 5V → USB-C(恆供電,無深睡)
 *  相依:Arduino core for ESP32 v3.x / Adafruit_SHT4X / Adafruit_NeoPixel
 *  設計核心:發黴的充分條件是「濕度停留時間」,不是瞬間峰值——
 *        所以本機同时管「現在」(三判據排風)與「昨天」(潮浸積分)。
 *  ============================================================ */

#include <WiFi.h>
#include <HTTPClient.h>
#include <time.h>
#include <Wire.h>
#include <Adafruit_SHT4X.h>
#include <Adafruit_NeoPixel.h>

/* ------------------------------------------------------------
 * 1. 使用者設定區
 * ------------------------------------------------------------ */
const char* WIFI_SSID     = "YOUR_WIFI_SSID";       // 僅 2.4GHz
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 uint8_t PIN_RADAR   = 7;    // LD2410 OUT(移動=HIGH)
const uint8_t PIN_SSR     = 15;   // 高=風扇轉(過零 SSR)
const uint8_t PIN_RING    = 48;   // WS2812 DIN
const uint8_t RING_LEDS   = 49;

/* ------------------------------------------------------------
 * 3. 控制參數(全部經一個雨季實測調過)
 *    滯回上下限:75 轉 / 60 停。停在 70 是發黴甜蜜區,要停就停穿它
 * ------------------------------------------------------------ */
const float    RH_FORCE_ON   = 75.0;    // 絕對濕度門檻(有人與否都轉)
const float    RH_STOP       = 60.0;    // 降到此值才允許停機
const float    SHOWER_RATE   = 4.0;     // 10 分鐘升 4%RH =「有人在洗澡」
const float    SOAK_RH       = 70.0;    // 潮浸積分計時門檻
const uint16_t SOAK_ALERT_MIN = 90;     // 昨日潮浸超此分鐘數 → 建議除濕
const uint32_t HOLD_MS     = 8UL*60000; // 雷達最後動作後的「還在裡」推定
const uint32_t PURGE_MS    = 12UL*60000;// 人走後的後吹乾燥
const uint32_t MIN_ON_MS   = 5UL*60000; // 最短運轉(保護電機、排透牆面)
const uint32_t MIN_OFF_MS  = 10UL*60000;// 最短停機(防頻繁啟停)
const uint8_t  DARK_START  = 22, DARK_END = 7; // 環燈夜間滅燈時段

/* ------------------------------------------------------------
 * 4. 全域物件與狀態
 * ------------------------------------------------------------ */
Adafruit_SHT4X sht40 = Adafruit_SHT4X();
Adafruit_NeoPixel ring(RING_LEDS, PIN_RING, NEO_GRB + NEO_KHZ800);

float    rhNow = 0, tNow = 0;
uint32_t lastMotionMs = 0, lastShowerMs = 0;
bool     fanOn = false;
uint32_t fanSinceMs = 0, lastFanToggleMs = 0, purgeUntilMs = 0;
uint32_t soakSecToday = 0;              // 今日 RH>70% 累計秒
float    rhMinDay = 100, rhMaxDay = 0;
uint16_t fanMinToday = 0;               // 今日排風分鐘(整分累加)
bool     soakAlerted = false;           // 今日已提醒過(每日只洗版一次)
uint32_t lastSecMs = 0, lastMinuteMs = 0, lastSampleMs = 0;
bool     yesterdayDone = true;          // 跨日後待發日報旗標
char     dayStats[220] = "";

/* 10 分鐘濕度趨勢環形緩衝:30 秒一格 × 20 格
 * 為什麼要趨勢:雷達偶尔漏掉「淋浴中靜止」的人,濕度陡升不會說謊 */
float    humRing[20]; uint8_t humIdx = 0; bool humRingFull = false;

/* ------------------------------------------------------------
 * sendTelegram —— Telegram 推播(失敗不重試:日报错过比洗版討厭,
 *   但推播失敗代表斷網,下小時狀態循環會自己補上重要性更高的警訊)
 *   參數:msg 訊息本文;回傳:HTTP 200 與否
 * ------------------------------------------------------------ */
bool sendTelegram(const String& msg) {
  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 +
                "\",\"text\":\"" + msg + "\"}";
  int st = http.POST(json);
  http.end();                            // HTTPS 資源必釋(避免堆積)
  return st == 200;
}

/* ------------------------------------------------------------
 * readSensors —— 讀 SHT40 並更新潮積日統計
 *   SHT40 I2C 讀取 <1ms,每 2 秒一讀對匯流排毫無壓力
 * ------------------------------------------------------------ */
void readSensors() {
  sensors_event_t hum, temp;
  if (!sht40.getEvent(&hum, &temp)) return;   // 感測器失聯:保留舊值繼續控制
  rhNow = hum.relative_humidity;
  tNow  = temp.temperature;
  if (rhNow < rhMinDay) rhMinDay = rhNow;
  if (rhNow > rhMaxDay) rhMaxDay = rhNow;
}

/* ------------------------------------------------------------
 * trendTick —— 每 30 秒存一格濕度,並偵測「洗澡斜率」
 *   回傳:是否刚侦測到洗澡事件(10 分鐘升 >4%RH)
 * ------------------------------------------------------------ */
bool trendTick() {
  bool shower = false;
  float old = humRing[humIdx];             // 寫入前先取「10 分鐘前」那格
  humRing[humIdx] = rhNow;
  humIdx = (humIdx + 1) % 20;
  if (humIdx == 0) humRingFull = true;
  if (humRingFull && old > 0 && (rhNow - old) >= SHOWER_RATE && rhNow > 60)
    shower = true;                          // 陡升且已進入潮區=熱水正在流
  return shower;
}

/* ------------------------------------------------------------
 * wantFan —— 三判據決定「想不想轉」(不考慮最短鎖)
 *   A 雷達推定在場 B 洗澡斜率推定 C 絕對濕度
 *   停機則用滯回:轉起來就要一路降到 RH_STOP 才罷休
 * ------------------------------------------------------------ */
bool wantFan() {
  bool presence = (millis() - lastMotionMs) < HOLD_MS;
  bool shower   = (millis() - lastShowerMs)  < HOLD_MS;
  bool force    = rhNow >= RH_FORCE_ON;
  if (presence || shower || force) return true;
  return fanOn && rhNow > RH_STOP;          // 停機滯回(轉著未達乾目標不松手)
}

/* ------------------------------------------------------------
 * fanControl —— 帶最短啟停鎖與後吹乾燥的輸出級
 *   為什麼要 purge:人離開≠牆面乾了,淋浴水霧在磁磚縫的毛細水
 *   還要再蒸發十幾分鐘,這才是黴能吃到的飯
 * ------------------------------------------------------------ */
void fanControl(bool want) {
  uint32_t now = millis();
  if (want && !fanOn) {
    if (now - lastFanToggleMs >= MIN_OFF_MS) {   // 停機鎖未滿:再等一下
      fanOn = true; fanSinceMs = now; lastFanToggleMs = now;
      digitalWrite(PIN_SSR, HIGH);
      Serial.println("[FAN] ON");
    }
  } else if (!want && fanOn) {
    if (now - fanSinceMs < PURGE_MS) return;     // 後吹乾燥未滿:轉著
    if (now - lastFanToggleMs >= MIN_ON_MS) {    // 運轉鎖未滿:轉著
      fanOn = false; lastFanToggleMs = now;
      purgeUntilMs = now;
      digitalWrite(PIN_SSR, LOW);
      Serial.println("[FAN] OFF");
    }
  }
}

/* ------------------------------------------------------------
 * lightEffect —— 環燈語言(20ms 刷新)
 *   青呼吸=除濕中 綠=有人在 藍慢脈衝=乾燥 紅=潮浸超標
 *   夜間時段全滅:浴室半夜不該有燈,感測器不靠看
 * ------------------------------------------------------------ */
void lightEffect() {
  struct tm t; bool dark = false;
  if (getLocalTime(&t, 10))
    dark = (t.tm_hour >= DARK_START || t.tm_hour < DARK_END);
  if (dark) { ring.clear(); ring.show(); return; }

  bool presence = (millis() - lastMotionMs) < HOLD_MS;
  uint8_t breath = 90 + 80 * sinf(millis() / 700.0f);
  uint32_t c;
  if (!soakAlerted && presence)        c = ring.Color(0, breath, 0);
  else if (fanOn)                      c = ring.Color(0, breath, breath);
  else if (soakAlerted || rhNow > 80)  c = ring.Color(breath, 0, 0);
  else                                 c = ring.Color(0, 0, breath / 2 + 30);
  ring.fill(c);
  ring.show();
}

/* ------------------------------------------------------------
 * dayTick —— 每日 00:01 結算昨日、07:50 發日報
 *   沒有 NTP 同步前不結算(沒時間就沒有「昨天」)
 * ------------------------------------------------------------ */
void dayTick() {
  static uint8_t lastHour = 99;
  struct tm t;
  if (!getLocalTime(&t, 10)) return;
  if (lastHour == 23 && t.tm_hour == 0) {          // 跨日結算
    snprintf(dayStats, sizeof(dayStats),
      "📅 浴室日報|濕度 %.0f~%.0f%%|潮浸 %lu 分|排風 %u 分",
      rhMinDay, rhMaxDay, soakSecToday / 60, fanMinToday);
    yesterdayDone = false;
    if (soakSecToday / 60 > SOAK_ALERT_MIN) soakAlerted = true;  // 今天亮紅燈
    rhMinDay = 100; rhMaxDay = 0; soakSecToday = 0; fanMinToday = 0;
  }
  lastHour = t.tm_hour;
  if (!yesterdayDone && t.tm_hour == 7 && t.tm_min >= 50) {
    sendTelegram(String(dayStats) +
      (soakAlerted ? "\n⚠️ 潮浸超標,建議開除濕機或開窗通風半日" : "\n✅ 狀態良好,繼續保持"));
    yesterdayDone = true;
    soakAlerted = false;                            // 新的一天重新觀察
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(PIN_RADAR, INPUT_PULLUP);      // LD2410 OUT 推挽輸出,上拉無害防悬空
  pinMode(PIN_SSR, OUTPUT);
  digitalWrite(PIN_SSR, LOW);            // 開機預設停機(安全側)
  ring.begin(); ring.clear(); ring.show();

  Wire.begin();
  if (!sht40.begin()) Serial.println("[SHT40] 未回應,以預設值續跑");
  sht40.setSpeed(SHT4X_HIGH_SPEED);
  sht40.setResolution(SHT4X_HIGH_RES);

  configTzTime("CST-8", "pool.ntp.org", "time.google.com"); // POSIX 的時區寫法:CST-8
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("連線 Wi-Fi");
  for (int i = 0; i < 60 && WiFi.status() != WL_CONNECTED; i++) { delay(500); Serial.print("."); }
  Serial.println(WiFi.status() == WL_CONNECTED ? " OK" : " 失敗,將持續重試");
}

void loop() {
  delay(20);
  uint32_t now = millis();

  /* --- 1. 秒級:雷達 + 控制 --- */
  if (now - lastSecMs >= 1000) {
    lastSecMs = now;
    if (digitalRead(PIN_RADAR) == HIGH) lastMotionMs = now;
    if (WiFi.status() != WL_CONNECTED && now % 30000 == 0) WiFi.reconnect();
    fanControl(wantFan());
    if (fanOn) { /* 分鐘統計在下面 lastMinuteMs */ }
  }

  /* --- 2. 兩秒:濕度取樣 --- */
  if (now - lastSampleMs >= 2000) {
    lastSampleMs = now;
    readSensors();
    if (rhNow > SOAK_RH) soakSecToday++;   // 潮積秒計(1Hz 與取樣同週期,誤差 <2s)
    Serial.printf("RH %.1f%% T %.1fC fan=%d presence=%d\r\n",
      rhNow, tNow, fanOn, digitalRead(PIN_RADAR));
  }

  /* --- 3. 30 秒:趨勢環 + 洗澡偵測;60 秒:排風分鐘累計 --- */
  if (now - lastSecMs >= 1000 && (now / 1000) % 30 == 0) {
    if (trendTick()) { lastShowerMs = now; Serial.println("[SHOWER] 濕度斜率判定了一个人"); }
  }
  if (now - lastMinuteMs >= 60000) { lastMinuteMs = now; if (fanOn) fanMinToday++; }

  /* --- 4. 環燈與日報 --- */
  lightEffect();
  static uint32_t lastDayMs = 0;
  if (now - lastDayMs >= 60000) { lastDayMs = now; dayTick(); }
}

🏗️ 組裝步驟

📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
  1. 先驗雷達再封殼:tmp 版韌體先跑「雷達 LED 直读」——LD2410 板載指示燈閃爍對應 OUT,站在預裝位置模仿洗澡小動作,確認 3 公尺內穩定觸發才鑽孔
  2. 強電交給安全流程:斷電→驗電→SSR 串火線→阻燃盒鎖閉→通電→用電燈測試開關。風扇接回前,SSR 端絕對絕緣膠布暫封
  3. 感測器位置:雷達窗對淋浴區中心(別對著窗戶,窗外樹枝搖曳會誤報);SHT40 進氣縫朝下、離門縫對流路徑 30cm 以內,讀的是「浴室的濕度」而不是「殼裡的濕度」
  4. 封殼前整機測試:熱水放 5 分鐘對照手機濕度計(偏差 <5% 合格)、人進出看環燈與 Telegram、把 RH_FORCE_ON 暫時改成 40 驗證 SSR 開關與風扇
  5. 上線觀察 48 小時:看日報數字與你對浴室的直覺是否吻合,再微調滯回參數重燒

🔧 問題排除

問題原因解決方法
風扇都不會轉SSR 輸入接反(IN+/IN-)或低壓域未共地萬用表二極體檔確認 IN+ 對 IN- 有 LED 壓降;GPIO15 手动寫 HIGH 聽…SSR 沒有聲音——安靜是-normal 的,用輸出端導通判斷
半夜環燈誤觸「有人在」雷達看到浴簾飄動或窗外樹影調整安裝角度讓雷達視窗避開窗;LD2410 用藍牙 APP 調低靈敏度档位
濕度永遠比手機低很多進氣縫被列印餘料堵住、或殼內積水形成小氣候針清進氣縫;殼體倒裝確認(本殼設計就是為了殼內不積水)
日報潮浸時間誇張地高感測器長期貼高濕環境產生滯後漂移SHT40 會漂但很小;把殼拆下放室內穩定一小時後對照參考值,必要時以 SOAK_RH 加補正值
S3 一直重啟燈座降壓模組電流不足(環燈全亮瞬間 3A 衝擊)環燈預設亮度 60;環燈顆數減半或改 5V 2A 電源
洗澡都要等到很濕才轉雷達漏抓靜止的人且斜率門檻太高SHOWER_RATE 由 4.0 降到 3.0;HOLD_MS 拉長;雷達距離螺絲微調近距離檔

🎯 進階延伸

📦 檔案清單

bathroom-moisture-guard/
├── src/
│   └── moisture-guard.ino     # ESP32-S3 程式碼(本頁完整程式)
├── stl/
│   ├── guard-shell.stl        # 防滴主殼(PLA)
│   ├── face-cover.stl         # 透波面蓋(圓盤紋)
│   ├── ring-diffuser.stl      # 環燈導光圈
│   ├── wall-clip.stl          # 86 盒適配鎖套(PETG)
│   └── ssr-cover.stl          # SSR 阻燃盒適配蓋(PETG)
├── f3d/
│   └── moisture-guard.f3d     # Fusion 360 原始檔
├── manual/
│   └── assembly.html          # 圖文組裝說明書
└── README.md                  # 說明文件
← 上一篇:ESP32-C3 夾式吉他調音器 📚 系列索引 下一篇:ESP32-C6 機車充電安全哨兵 →