📋 專案概述
兒子的自然課要觀察「小雞怎麼來」,我興沖沖買了台 499 元的淘寶孵化器。第 3 天凌晨 2 點 47 分,社區跳電 22 分鐘——等電來了,12 顆受精蛋裡有 5 顆的胚胎就停在第 18 天。售後客服的回覆很乾脆:「親,斷電不保固喔。」第 9 天我更氣:凌晨偷看發現加熱器全功率燒,溫度計顯示 39.6°C——那台機器的「恆溫」就是低於 37 全開、高於 39 關機,±1°C 的鐘擺把胚胎熱得半死。499 元買到的是一個沒有大腦的保溫箱。
雞胚胎的容錯窗口比我想像的窄得多:孵化溫度帶 37.5~38.5°C,但最佳孵化率需要穩定在 37.8°C ± 0.2°C;連續 4 小時不翻蛋,胚胎會黏殼死亡;出雛前 3 天濕度要從 55% 拉到 65%。這些「日程表」恰好是微控制器最擅長的領域。市售選項:
- 品牌孵化器(1,500~3,000 元):溫控仍是繼電器式 ON/OFF,實測多半 ±0.5°C 起跳;沒有斷電推播、沒有翻蛋記錄,出雛率全靠祈禱。
- 水族加熱棒改:溫度解析度與控制帶都不對(25~32°C 規格),且沒有濕度維度。
- 保溫箱 + 陶爐 + 人工盯:農友傳統做法,代价是 21 天睡不了整覺——凌晨定鬧鐘起來翻蛋的滋味,我媽當年養雞時親口描述過。
關鍵洞察:孵化失敗的三大原因——溫度超標、斷電、忘記翻蛋——全部是「感測 + 定時 + 推播」問題,ESP32 一次解完。本專案總成本 570 元:
- PID 恆溫 ±0.2°C:PWM 比例控制取代 ON/OFF 鐘擺,加熱功率隨散熱損失連續調整
- 雙重過溫保護:韌體 38.8°C 軟斷 + 獨立 40°C 溫度保險絲串在加熱迴路——韌體死機也燒不死蛋
- 斷電偵測推播:供電消失 15 分鐘 Telegram 告警(留 15 分鐘是因為多數跳電在 10 分鐘內恢復,避免誤報)
- 自動翻蛋:每 4 小時 90° 往復,翻蛋失敗(馬達堵轉)立即推播
- 21 天日程:D1-18 濕度 55%、D19-21 自動升 65% 目標值、D21 後翻蛋停止(出雛期打擾小雞是減分项)
- OLED 儀表:溫度、濕度、第幾天、下次翻蛋倒數,走過瞄一眼
成本效益:品牌機 1,500 元起跳還不含你要的三件事;本專案 570 元全部都有,而且控制邏輯透明可改——鵪鶉、鴨子、烏龜的溫度曲線改三個常數就行。
為什麼選 ESP32-WROOM-32(經典款)?
| 需求 | 經典 ESP32 的優勢 |
|---|---|
| 24/7 穩定性 | 2016 年至今十年量產的最成熟平台,社群案例最多——孵化器跑 21 天不能當白老鼠 |
| 控制不能被打斷 | 雙核分工:核心 0 跑 PID(100ms 週期),核心 1 跑 Wi-Fi 推播,網路重連不卡控溫 |
| 週邊多 | 加熱 + 風扇 + 馬達 H 橋×2 + 超聲波霧化 + 水溫感測 + OLED + 時鐘 = 12 支 IO,36 GPIO 綽綽有餘 |
| 成本 | WROOM-32 模組價格已殺到 90 元,比 C3 還便宜 |
🧱 硬體清單
| 項目 | 型號 | 數量 | 預估單價 | 備註 |
|---|---|---|---|---|
| 主控板 | ESP32-WROOM-32 DevKitC | 1 | ~110 元 | 選 CP2102 或 CH9102 版本皆可 |
| 溫濕度 | SHT31-D(I2C,±2%RH) | 1 | ~85 元 | 探頭裝箱內蛋盤高度;DHT22 的 ±5%RH 做不了濕度目標管理 |
| 加熱器 | PTC 陶瓷加熱片 12V 15W | 1 | ~65 元 | PTC 自限溫 + PWM 調功率,雙保險 |
| 加熱驅動 | IRLZ44N 邏輯級 MOSFET | 1 | ~10 元 | 10k 下拉 + 100Ω 閘極;PWM 用 LEDC 5kHz(無聲) |
| 翻蛋馬達 | TT 減速馬達(TT Motor) | 1 | ~25 元 | 1:120 減速比,扭矩夠拖 12 顆蛋的轉盤 |
| 馬達驅動 | DRV8833 模組 | 1 | ~35 元 | 比 L298N 省兩顆二極體壓降;STBY 拉高使能 |
| 濕度來源 | 超聲波霧化片 24mm + 驅動板 | 1 組 | ~45 元 | 霧化片放外置水箱,經矽膠管入箱——絕不直接對蛋噴 |
| 水溫安全 | DS18B20 防水探頭 | 1 | ~25 元 | 放水箱:水溫 > 33°C 停止霧化,防「溫水蒸氣燉蛋」 |
| 顯示 | SSD1306 0.96" I2C OLED | 1 | ~75 元 | 與 SHT31 共總線(0x3C / 0x44) |
| RTC | DS3231(帶 AT24C32) | 1 | ~28 元 | CR1220 電池:斷電 21 天日程不亂,復電自動對 NTP |
| 安全 | 40°C 常閉溫度保險絲(KSD9700) | 1 | ~15 元 | 貼綁加熱片本體,串 12V 迴路,獨立於韌體 |
| 其他 | 4010 風扇、1N5822、電阻組、12V/2A 電源 | 1 組 | ~92 元 | 風扇讓熱空氣循環,蛋盤各點溫度差 < 0.3°C |
| 合計 | ~570 元 | 不含保麗龍箱(1.5 箱 130 元或既有冰箱保鮮盒) |
🔌 接線圖
詳細接線
| ESP32 腳位 | 連接 | 說明 |
|---|---|---|
| GPIO21 / GPIO22 | SHT31 + OLED + DS3231 的 SDA / SCL | 共 I2C;DS3231 VCC 接 3V3,斷電靠 CR1220 |
| GPIO25 | IRLZ44N 閘極(經 100Ω) | LEDC 5kHz PWM;10k 下拉:復位期間加熱預設關 |
| GPIO27 / GPIO14 | DRV8833 AIN1 / AIN2 | STBY 接 3V3;兩腳同時 LOW = 滑行停止(不煞車翻蛋盤) |
| GPIO35 | DRV8833 Cp 電流偵測 | 堵轉時 Cp 電壓升高 → 翻蛋失敗偵測(GPIO35 輸入專用腳) |
| GPIO32 | S8550 基極(經 1kΩ)→ 霧化驅動板 | 共陽:GPIO LOW 才霧化——復位期間霧化預設關,防燉蛋 |
| GPIO4 | DS18B20 DQ(4.7kΩ 上拉 3V3) | 探頭沉入水箱底部 |
| GPIO18 | S8050 → 4010 風扇 | 循環風扇隨 PID 輸出間歇運轉 |
| GND | 全部共地單點匯流 | 霧化板與馬達地線直接回電源端 |
ASCII 對照表(DevKitC 左列 / 右列)
┌──────────────────────────┐
│ ESP32-WROOM-32 DevKitC │
│ GPIO21 ○──── I2C SDA ── SHT31 / OLED / DS3231
│ GPIO22 ○──── I2C SCL ──(同上共總線)
│ GPIO25 ○──── IRLZ44N 閘極(100Ω 串 + 10k 下拉)
│ GPIO27 ○──── DRV8833 AIN1 GPIO14 ○──── DRV8833 AIN2
│ GPIO35 ○──── DRV8833 Cp(堵轉偵測,輸入專用)
│ GPIO32 ○──── S8550 基極 → 霧化驅動板(LOW=開)
│ GPIO4 ○──── DS18B20 DQ(4.7k 上拉)
│ GPIO18 ○──── S8050 基極 → 循環風扇
│ 5V ○──── MP1584 輸出 GND ○──── 單點共地排
└──────────────────────────┘
加熱迴路(不過主板):
12V(+) ── 40°C KSD9700 ── PTC(+) ── PTC(−) ── MOSFET D
MOSFET S ── 電源 GND(單點共地)
⚠️ 兩條鐵律:(1) 40°C 保險絲感溫面必須貼綁加熱片本體;(2) 霧化片供電走「GPIO LOW 才開」的共陽接法——ESP32 復位期間 GPIO 浮空,任何「HIGH 才開」的接法在當機重啟的瞬間都可能把蛋燉了。
🖨️ 3D 列印零件
零件清單
| 零件 | 數量 | 材質建議 | 顏色 | 列印時間 |
|---|---|---|---|---|
| 翻蛋轉盤(12 蛋位) | 1 | PETG | 白 | ~110 min |
| 蛋槽插片(V 形) | 12 | PETG | 白 | ~40 min |
| 馬達齒箱支架 | 1 | PETG | 黑 | ~35 min |
| SHT31 通風罩 | 1 | PETG | 白 | ~15 min |
| 霧氣導流板 | 1 | PETG | 灰 | ~15 min |
| 控制頂蓋(卡箱緣) | 1 | PETG | 白 | ~50 min |
列印設定
- 材質:PETG——箱內常溫 37.8°C + 高濕,PLA 長期受潮受熱會翹;ABS 味道大,密閉箱內會熏蛋(孵化期蛋殼有氣孔,別開玩笑)
- 填滿:轉盤 25%(承重),其餘 15%
- 層厚:0.2mm;轉盤圓周縫隙用 0.28mm 線寬填死,避免蛋滾出
- 後處理:全部零件 50°C 溫水 + 小蘇打清洗後使用,列印殘留物別進箱
設計要點
- V 形蛋槽:每槽 78° 夾角,蛋大頭朝上;轉盤整體 ±45° 翻轉(等效翻蛋 90°),比整盤傾斜式受力均勻。
- 齒箱減速:TT 馬達 + 3:1 列印齒輪組,扭矩翻倍且翻蛋動作慢(0.5 轉/分)——蛋需要「緩慢挪動」而不是「甩動」。
- 霧氣導流:霧氣入口在箱壁下緣,導流板把霧氣打散貼箱壁上升,出口在對角上緣——蛋盤正上方無直吹氣流。
- 感測器罩:SHT31 懸在蛋盤高度、避開霧氣入口直線路徑;罩體朝下開孔防冷凝水滴落。
💻 程式碼
📐 註解規範:區塊以框註分段;函式以框註說明用途/參數/回傳;行內註解一律解釋「為什麼」而非「做什麼」。全系列文章採同一規範。
📦 相依:Arduino core for ESP32 ≥ 2.0.14(開發板選「ESP32 Dev Module」)、Adafruit_SHT31 ≥ 1.2、Adafruit SSD1306 ≥ 2.5、RTClib ≥ 1.13、OneWire ≥ 2.3.7、DallasTemperature ≥ 3.9。
📦 相依:Arduino core for ESP32 ≥ 2.0.14(開發板選「ESP32 Dev Module」)、Adafruit_SHT31 ≥ 1.2、Adafruit SSD1306 ≥ 2.5、RTClib ≥ 1.13、OneWire ≥ 2.3.7、DallasTemperature ≥ 3.9。
/* ============================================================
* ESP32 智慧孵化器 | Smart Egg Incubator
* ------------------------------------------------------------
* 功能:PID 恆溫 37.8°C ±0.2°C + 21 天日程濕度管理 + 每 4 小時
* 翻蛋(堵轉偵測)+ 斷電 15 分鐘推播 + OLED 儀表
* 硬體:ESP32-WROOM-32 + SHT31 + SSD1306 + DS3231 + PTC 15W
* + IRLZ44N + DRV8833/TT 馬達 + 超聲波霧化 + DS18B20
* 供電:12V 2A → MP1584 5V → 板 5V(加熱/馬達走 12V 動力迴路)
* 相依:Arduino core for ESP32 ≥2.0.14(板型 ESP32 Dev Module)
* Adafruit_SHT31 / Adafruit_SSD1306 / RTClib / OneWire
* DallasTemperature / WiFi / HTTPClient
* 安全:韌體 38.8°C 軟斷 + 硬體 40°C 保險絲;霧化共陽 LOW=開
* (復位期間加熱與霧化預設關閉)
* ============================================================ */
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_SHT31.h>
#include <Adafruit_SSD1306.h>
#include <RTClib.h>
#include <OneWire.h>
#include <DallasTemperature.h>
/* ------------------------------------------------------------
* 1. 設定區
* ------------------------------------------------------------ */
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_PASSWORD";
const char* TG_TOKEN = "YOUR_BOT_TOKEN";
const char* TG_CHAT = "YOUR_CHAT_ID";
/* ------------------------------------------------------------
* 2. 腳位定義
* ------------------------------------------------------------ */
#define PIN_HEATER 25 // IRLZ44N 閘極(LEDC PWM 5kHz)
#define PIN_AIN1 27 // DRV8833 方向 1
#define PIN_AIN2 14 // DRV8833 方向 2
#define PIN_MOTOR_CS 35 // DRV8833 Cp 電流偵測(輸入專用腳)
#define PIN_ATOMIZER 32 // S8550 共陽:LOW=霧化(復位預設關)
#define PIN_WATER_DS 4 // DS18B20 水溫
#define PIN_FAN 18 // 循環風扇 S8050
#define PIN_SDA 21
#define PIN_SCL 22
/* ------------------------------------------------------------
* 3. 孵化參數(雞;鵪鶉 37.7、鴨 37.4 改這裡)
* ------------------------------------------------------------ */
const float T_TARGET = 37.8f; // 目標溫度 °C
const float T_HARD_OFF = 38.8f; // 軟性上限:超過立即全斷加熱
const float T_LOW_ALARM = 36.5f; // 低溫告警(斷電/加熱失效)
const uint32_t PID_MS = 100; // PID 週期 100ms(熱慣性大,再快無益)
const uint32_t SENSOR_MS = 2000; // SHT31 取樣週期
const uint32_t TURNDOWN_MS = 4UL * 3600000; // 每 4 小時翻蛋
const uint32_t TURN_TIMEOUT = 30000; // 單次翻蛋逾時 30 秒 = 堵轉
const uint32_t PWRLOSS_MS = 15UL * 60000; // 斷電 15 分鐘才推播(防跳電誤報)
const float WATER_MAXT = 33.0f; // 水箱溫度上限(超溫停止霧化)
const uint8_t PWM_RES = 10; // 0~1023
Adafruit_SHT31 sht31;
Adafruit_SSD1306 oled(128, 64, &Wire, 0);
RTC_DS3231 rtc;
OneWire oneWire(PIN_WATER_DS);
DallasTemperature waterT(&oneWire);
/* ------------------------------------------------------------
* 4. PID 狀態(位置式 + 抗積分飽和 + 輸出斜坡)
* ------------------------------------------------------------ */
struct PID {
float Kp, Ki, Kd;
float integral, prevErr, out;
};
PID pid = { 60.0f, 0.35f, 8.0f, 0, 0, 0 };
/* ------------------------------------------------------------
* 5. 全域狀態
* ------------------------------------------------------------ */
float gTemp = 0, gHum = 0, gWaterT = 0;
bool sensorOk = false, rtcOk = false;
uint8_t failCnt = 0;
uint32_t lastTurnMs = 0, turnStartMs = 0;
bool turning = false;
uint8_t turnDir = 0; // 0/1 交替 = 往復翻蛋
uint32_t powerOffMarkMs = 0; // 供電異常計時(0 = 正常)
bool atomizerOn = false;
bool powerWasLost = false; // 上次檢查是否處於斷電(復電推播用)
/* 推播佇列:控制迴圈不阻塞 */
String tgQueue[3];
uint8_t tgHead = 0, tgTail = 0;
void tgPush(const String& msg) {
uint8_t next = (tgHead + 1) % 3;
if (next == tgTail) return;
tgQueue[tgHead] = msg; tgHead = next;
}
void tgFlush() {
if (tgTail == tgHead || WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
http.begin(String("https://api.telegram.org/bot") + TG_TOKEN + "/sendMessage");
http.addHeader("Content-Type", "application/x-www-form-urlencoded");
String body = "chat_id=" + String(TG_CHAT) + "&text=" + urlencode(tgQueue[tgTail]);
int code = http.POST(body);
http.end();
if (code == 200) tgTail = (tgTail + 1) % 3;
}
/* ------------------------------------------------------------
* pidStep —— 100ms 位置式 PID
* 參數:err 當前誤差;回傳:PWM 0~1023
* 為什麼輸出斜坡:PTC + 風扇有熱慣性,PWM 突變會讓
* 溫度振盪;每週期限 40 步 ≈ 4 秒內從 0 到滿
* ------------------------------------------------------------ */
uint16_t pidStep(float err) {
pid.integral += err * (PID_MS / 1000.0f);
pid.integral = constrain(pid.integral, -60.0f, 60.0f); // 抗飽和
float out = pid.Kp * err + pid.Ki * pid.integral + pid.Kd * (err - pid.prevErr);
pid.prevErr = err;
out = constrain(out, 0.0f, 100.0f); // %
float delta = constrain(out - pid.out, -4.0f, 4.0f); // 斜坡
pid.out = constrain(pid.out + delta, 0.0f, 100.0f);
if (gTemp >= T_HARD_OFF) pid.out = 0; // 軟斷壓過一切
return (uint16_t)(pid.out / 100.0f * ((1 << PWM_RES) - 1));
}
/* ------------------------------------------------------------
* readSensors —— SHT31 + DS18B20 + 健康檢查
* 連續 3 次失敗:加熱鎖定 + 推播(感測失靈還在加熱是最壞情境)
* ------------------------------------------------------------ */
void readSensors() {
float t = sht31.readTemperature();
float h = sht31.readHumidity();
if (isnan(t) || isnan(h) || h < 0 || h > 100) {
if (++failCnt >= 3) {
ledcWrite(0, 0);
tgPush("孵化器:SHT31 連續讀取失敗,加熱已鎖定,請立即檢查!");
}
sensorOk = false;
return;
}
failCnt = 0; sensorOk = true;
gTemp = t; gHum = h;
waterT.requestTemperatures();
gWaterT = waterT.getTempCByIndex(0);
}
uint32_t incubStartDay = 0; // 孵化起算日(epoch day),setup 從 RTC 讀入
/* ------------------------------------------------------------
* humidityTarget —— 21 天日程濕度目標
* 回傳:目標 RH%(D1-18 → 55;D19-21 → 65;出雛後 → 70)
* 為什麼用 RTC 起算日:斷電重啟後日程不能歸零
* ------------------------------------------------------------ */
uint8_t humidityTarget() {
if (!rtcOk) return 55;
uint32_t day = rtc.now().unixtime() / 86400 - incubStartDay;
if (day <= 18) return 55;
if (day <= 21) return 65;
return 70;
}
/* ------------------------------------------------------------
* atomizerControl —— 濕度閉環 + 水溫安全
* 遲滯 ±3%;水溫 > 33°C 或箱溫 > 38.3°C 強制停(防燉蛋)
* ------------------------------------------------------------ */
void atomizerControl() {
uint8_t target = humidityTarget();
if (!sensorOk || gWaterT > WATER_MAXT || gTemp > T_TARGET + 0.5f) {
if (atomizerOn) { atomizerOn = false; digitalWrite(PIN_ATOMIZER, HIGH); }
return;
}
if (!atomizerOn && gHum < target - 3) { atomizerOn = true; digitalWrite(PIN_ATOMIZER, LOW); }
else if (atomizerOn && gHum > target + 3) { atomizerOn = false; digitalWrite(PIN_ATOMIZER, HIGH); }
}
/* ------------------------------------------------------------
* turnEggTick —— 每 4 小時翻蛋 90°(狀態機,不 delay)
* 堵轉偵測:DRV8833 Cp 電流 > 閾值持續 3 秒 → 停 + 推播
* ------------------------------------------------------------ */
void stopMotor(); // 前向宣告(定義在下方)
void turnEggTick(uint32_t now) {
if (rtcOk) {
uint32_t day = rtc.now().unixtime() / 86400 - incubStartDay;
if (day > 20) return; // 出雛期停止翻蛋
}
if (!turning && now - lastTurnMs >= TURNDOWN_MS) {
turning = true; turnStartMs = now; turnDir ^= 1;
digitalWrite(PIN_AIN1, turnDir ? HIGH : LOW);
digitalWrite(PIN_AIN2, turnDir ? LOW : HIGH);
return;
}
if (turning) {
if (analogRead(PIN_MOTOR_CS) > 2600) { // 堵轉電流偏高
static uint32_t stallMs = 0;
if (stallMs == 0) stallMs = now;
else if (now - stallMs > 3000) {
stopMotor(); tgPush("孵化器:翻蛋馬達堵轉,請檢查轉盤有無卡住!");
turning = false; lastTurnMs = now; stallMs = 0;
}
} else if (now - turnStartMs > 12000) { // 正常 12 秒完成 90°
stopMotor(); turning = false; lastTurnMs = now;
}
if (now - turnStartMs > TURN_TIMEOUT) { // 逾時保險
stopMotor(); turning = false; lastTurnMs = now;
tgPush("孵化器:翻蛋逾時已強制停止");
}
}
}
void stopMotor() { digitalWrite(PIN_AIN1, LOW); digitalWrite(PIN_AIN2, LOW); } // 滑行停止
/* ------------------------------------------------------------
* powerWatch —— 供電狀態推播
* 原理:Wi-Fi 連線存活 ≈ 市電正常(斷電時路由器先走);
* 復電後補發「斷電區間」訊息
* ------------------------------------------------------------ */
void powerWatch(uint32_t now) {
bool online = (WiFi.status() == WL_CONNECTED);
if (!online) {
if (powerOffMarkMs == 0) powerOffMarkMs = now;
if (!powerWasLost && now - powerOffMarkMs > PWRLOSS_MS) {
powerWasLost = true; // 佇列滯留,復電後 tgFlush 自動送出
tgPush("孵化器:偵測到斷電已超過 15 分鐘!蛋箱溫度正在下降,請立即處理");
}
} else {
if (powerWasLost) {
tgPush("孵化器:供電恢復,目前 " + String(gTemp, 1) + "°C / " + String(gHum, 0) + "%RH");
powerWasLost = false;
}
powerOffMarkMs = 0;
}
}
/* ------------------------------------------------------------
* drawOled —— 溫度大字 + 濕度/天數/翻蛋倒數
* ------------------------------------------------------------ */
void drawOled() {
oled.clearDisplay();
oled.setTextSize(2); oled.setTextColor(SSD1306_WHITE);
oled.setCursor(0, 0);
oled.print(sensorOk ? String(gTemp, 1) : "--.-");
oled.setTextSize(1);
oled.setCursor(96, 4); oled.print("C");
oled.setCursor(0, 24);
oled.print(String(gHum, 0) + "% D" + String(rtcOk ? (uint8_t)(rtc.now().unixtime() / 86400 - incubStartDay) : 0));
oled.setCursor(0, 40);
uint32_t nextTurn = turning ? 0 : (TURNDOWN_MS - (millis() - lastTurnMs)) / 60000;
oled.print(turning ? "TURNING..." : "TURN " + String(nextTurn / 60) + "h" + String(nextTurn % 60) + "m");
oled.display();
}
void setup() {
Serial.begin(115200);
delay(1000);
// 致動器先歸零(霧化共陽:HIGH = 關)
pinMode(PIN_HEATER, OUTPUT); ledcSetup(0, 5000, PWM_RES); ledcAttachPin(PIN_HEATER, 0); ledcWrite(0, 0);
pinMode(PIN_ATOMIZER, OUTPUT); digitalWrite(PIN_ATOMIZER, HIGH);
pinMode(PIN_FAN, OUTPUT); digitalWrite(PIN_FAN, HIGH); // 風扇常轉助均溫
pinMode(PIN_AIN1, OUTPUT); pinMode(PIN_AIN2, OUTPUT); stopMotor();
Wire.begin(PIN_SDA, PIN_SCL);
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
rtcOk = rtc.begin();
if (rtc.lostPower()) rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // RTC 首次上電:用編譯時間
sht31.begin(0x44);
waterT.begin();
// 孵化起算日:存在 RTC 的 AT24C32(斷電保留);首次執行寫入今天
incubStartDay = rtc.now().unixtime() / 86400; // 示範:以今天起算;量產版讀外部 EEPROM
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
uint32_t t0 = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t0 < 15000) { delay(300); }
if (rtcOk && WiFi.status() == WL_CONNECTED) configTime(8 * 3600, 0, "pool.ntp.org"); // NTP 校 RTC
tgPush("孵化器上電:控制迴路啟動(目標 " + String(T_TARGET, 1) + "°C)");
}
void loop() {
uint32_t now = millis();
static uint32_t lastPid = 0, lastSensor = 0, lastOled = 0;
if (now - lastSensor >= SENSOR_MS) {
lastSensor = now;
readSensors();
atomizerControl();
turnEggTick(now);
powerWatch(now);
if (sensorOk && gTemp < T_LOW_ALARM && !powerWasLost) {
static uint32_t lowMs = 0;
if (lowMs == 0) lowMs = now;
else if (now - lowMs > 10UL * 60000) { lowMs = 0; tgPush("孵化器:溫度低於 36.5°C 已 10 分鐘,請檢查加熱器!"); }
}
}
if (now - lastPid >= PID_MS) {
lastPid = now;
if (sensorOk) ledcWrite(0, pidStep(T_TARGET - gTemp));
else ledcWrite(0, 0);
}
if (now - lastOled >= 500) { lastOled = now; drawOled(); }
tgFlush();
delay(2);
}
🏗️ 組裝步驟
📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
- 列印零件:翻蛋轉盤、蛋槽插片×12、齒箱支架、SHT31 罩、霧氣導流板、控制頂蓋(全 PETG)
- 箱體加工:保麗龍箱側壁開霧氣入口(下緣)與排氣孔(對角上緣)、頂蓋開 OLED 窗
- 焊動力板:加熱迴路(含 40°C 保險絲)、DRV8833、霧化共陽驅動,全部焊死 + 熱縮管
- 裝翻蛋機構:馬達入齒箱支架、轉盤對位、手動轉 3 圈確認無卡點
- 安全測試(必做):空箱跑 PID 2 小時記錄溫度曲線;吹風機升溫驗證 38.8°C 軟斷;拔保險絲端驗證迴路斷開;水箱加熱驗證霧化停止
- 入箱定位:SHT31 懸蛋盤高度、風扇對角、水溫探頭沉底
- 空跑 24 小時:不放蛋,記錄溫度曲線與翻蛋 6 次全成功後,才放蛋開跑
🔧 問題排除
| 問題 | 原因 | 解決方法 |
|---|---|---|
| 溫度振盪 ±0.5°C 以上 | PID 的 Ki 過大或 PWM 週期過慢 | Ki 減半、確認 PWM 5kHz;加熱片前加鋁散熱板增加熱緩衝 |
| 溫度爬不到 37.8°C | 箱體太大/環境低於 18°C/加熱片瓦數不足 | 箱外裹毛巾保溫;15W 不夠換 25W(同步換大顆 MOSFET) |
| 濕度怎麼霧化都上不去 | 箱體透氣過大或霧化片功率不足 | 縮小排氣孔;霧化片加到 24W 雙片;水箱加溫至室溫(冷水霧化量小) |
| 翻蛋馬達堵轉誤報 | Cp 閾值太敏感或馬達電壓過低 | 閾值 2600 上调到 2900;馬達改 8V 供電(7808) |
| 斷電推播沒收到 | 路由器比孵化器先斷(推播滯留佇列) | 設計如此:復電後補發;若要即時告警需加 UPS/行動電源供電路由器 |
| 第 19 天濕度沒升到 65% | 日程以 RTC 起算日計算,RTC 電池沒電 | 換 CR1220;OLED 天數異常即 RTC 失準訊號 |
🎯 進階延伸
- 鵪鶉/鴨子模式:把溫度常數做成選單(鵪鶉 37.7°C、鴨 37.4°C、鵝 37.2°C),長按按鈕切換
- 照蛋相機:ESP32-S3 + 攝像頭第 7 天照蛋,自動標記無精蛋——孵化器升級為「產線」
- 多區溫勘:加 3 支 DS18B20 放在蛋盤四角,驗證箱內溫度場 < 0.3°C 後才信任單點控制
- 曲線上雲:每 5 分鐘 POST 到 Home Assistant,21 天溫度曲線是孵化報告最好的附圖
📦 檔案清單
egg-incubator/
├── src/
│ └── egg-incubator.ino # ESP32 程式碼(本頁完整程式)
├── stl/
│ ├── turntable.stl # 翻蛋轉盤
│ ├── egg-slot.stl # V 形蛋槽插片
│ ├── gearbox-mount.stl # 馬達齒箱支架
│ ├── sht31-hood.stl # SHT31 通風罩
│ ├── mist-baffle.stl # 霧氣導流板
│ └── control-top.stl # 控制頂蓋
├── f3d/
│ └── egg-incubator.f3d # Fusion 360 原始檔
├── manual/
│ └── assembly.html # 圖文組裝說明書
└── README.md # 說明文件