📋 專案概述
樹脂印到第三天,你一定會遇見終點站難題:清洗完的軟趴趴印件,拿 UV 檯燈照 10 分鐘、翻面再 10 分鐘——人守著、眼睛不敢直視、總有一面照不勻;黏手放一夜,隔天摸起來還是像拔絲的起司。你開始理解為什麼树脂玩家的置物架上,總有一台「固化機」的位置。
市售固化機兩條燈管一個轉台,NT$1,500 起跳,功能本質上是「定時器+轉」。真正咬人的四個坑它一個都沒填:
- 開蓋不遮斷:405nm 對視網膜是秒級傷害,「定時器」不會在你低頭偷看的那一刻保護你的眼睛——市售機多數照燈不滅
- 沒有配方管理:標準樹脂 2–4 分鐘、韌性樹脂 6–10 分鐘、水洗樹脂要加長、澆灌件更是另一個世界——一台機器一個按鍵,全部靠腦子記
- 沒有溫度補償:冬天表面發黏你以為時間不夠,其實是樹脂在 12°C 根本懶得交聯——再加時只是烤乾表面、裡面還是生蛋
- 沒有燈衰補償:UV-LED 每 1,000 小時衰 10–20%,半年後「同一個設定」的固化度悄悄掉兩成,你換樹脂批次背鍋
本機 NT$325:28BYJ-48 轉台+405nm 燈板+乾簧管門磁+DHT22,把四個坑全部填進韌體:
- 四種樹脂配方:標準/韌性/水洗/澆灌,旋鈕一選即走,各自時間與功率——新樹脂試出最佳參數後用序列埠存進 NVS,配方跟著機器走,不跟著腦子走
- 溫度補償:腔溫 15–20°C 自動加時 ×1.4、12–15°C ×1.8;低於 12°C 拒絕啟動並顯示「先回溫」——低溫強烤是生蛋的近路
- 燈衰補償:LED 累計點亮時數存 NVS,每滿 100 小時功率係數 −5%,固化時間自動按比例加長——出料品質不隨燈老化漂移
- 安全三連:開蓋 50ms 內斷 UV(乾簧管主迴路輪詢,不靠軟體狀態機攔)、腔溫 >45°C 暫停、啟動前三聲嗶+3 秒倒數
- 閃檢模式:待機時短按——0.3 秒低功率 UV 閃光+轉台轉一圈,不開蓋就知道固化到位了沒有(市售機要開蓋偷看的那三秒,眼睛自己負責)
- 完工推 Telegram:深夜排程、早上收貨;每爐記錄實際時數自動餵給燈衰模型
算盤:最便宜的市售固化機 NT$1,500 上下、無門磁無補償;本機 325 元,材料貴在螢幕和 DHT22——把「定時器+轉」升級成「配方機+補償機+安全機」,多花的钱全部花在感測器上。而對樹脂玩家真正的成本是失敗的印件:一顆 90 分鐘的雕像因為固化不足在後處理時斷手,樹脂錢是小、時間是大。
為什麼選 ESP32-C2?
| 需求 | C2 的對應 |
|---|---|
| 成本 | SuperMini ~40 元,全家族最低——工作站預算留給燈板、電源和感測器,主控揀最省的 |
| 運算量 | 步進半步+UV PWM+I2C 面板+Wi-Fi 推播並行,單核 RISC-V 120MHz 負載不到 5% |
| GPIO 預算 | 步進 4+UV 1+編碼器 3+門磁 1+DHT 1+蜂鳴 1+I2C 2=13 支,C2 可用腳正好綽綽有餘 |
| 不需要的(PSRAM/BLE/雙核/攝像頭) | 本機的本質是「一台會定時、會看溫度、怕你傷眼的電機」——S3 的才華在這裡無處安放 |
環境:arduino-esp32 core ≥ 3.0,開發板選 ESP32C2 Dev Module,USB CDC On Boot: Enabled。⚠️ GPIO18/19 是 USB、GPIO20 是 strapping,本韌體全部避開。圖形庫需 Adafruit SSD1306 ≥ 2.5.x;DHT22 採單線手撕時序(無外部函式庫)。
🧱 硬體清單
| 項目 | 型號 | 數量 | 預估單價 | 備註 |
|---|---|---|---|---|
| 主控板 | ESP32-C2 SuperMini | 1 | ~40 元 | 全家族最便宜的主控 |
| 轉台馬達 | 28BYJ-48 5V + ULN2003 驅動板 | 1 | ~30 元 | 半步驅動;1/64 減速 torque 夠載 200g 平台 |
| UV 燈板 | 395–405nm UV-LED 鋁基板(12V 3W) | 1 | ~60 元 | 選 405nm 為主頻的樹脂固化級燈珠 |
| UV 開關 | IRLZ44N(TO-222)+10k 下拉+100Ω | 1 | ~8 元 | 低閘極邏輯級,3.3V 柵極直推 |
| 電源 | 12V/2A 適配器+MP1584 降壓模組 | 1 | ~30 元 | 降壓先空載調到 5.1V 再接 C2 |
| 門磁 | 乾簧管(MC-21)+磁鐵 | 1 | ~12 元 | 開蓋 <50ms 遮斷 UV 的主迴路 |
| 溫濕度 | DHT22 | 1 | ~40 元 | 腔內溫度=補償模型的變數,裝在燈板對角 |
| 編碼器 | KY-040 旋轉編碼器帶按鈕 | 1 | ~12 元 | 選單+啟動+閃檢,本機唯一操作件 |
| 螢幕 | SSD1306 0.91" I2C(128×32) | 1 | ~65 元 | 面朝操作者,運作中顯示剩餘秒 |
| 蜂鳴器 | 有源蜂鳴器帶板 5V | 1 | ~10 元 | 倒數、完工、異常三種節拍 |
| 列印料 | PLA 60g+PETG 20g | — | ~20 元 | 腔體長期微熱,講究可全 PETG |
| 合計 | ~327 元 | 市售固化機的 1/4 |
🔌 接線圖
ASCII 對照表
🖨️ 3D 列印零件
零件清單
| 零件 | 數量 | 材質建議 | 顏色 | 列印時間 |
|---|---|---|---|---|
| 固化腔體+鉸鏈蓋(120×90×60mm,蓋無視窗) | 1 | PETG(耐微熱) | 黑 | ~2 hr |
| UV 反光罩(45° 錐面,出爐貼鋁箔膠帶) | 1 | PLA | 白 | ~25 min |
| 旋轉平台(Ø110mm,底面十字肋+矽膠防滑圈) | 1 | PLA | 任意 | ~35 min |
| 馬達固定座(28BYJ 耳朵兩螺+軸聯軸器) | 1 | PETG | 黑 | ~20 min |
| 編碼器旋鈕(滾花) | 1 | PLA | 任意 | ~12 min |
| 門磁固定座(蓋側磁鐵槽+腔側套管) | 1 | PLA | 黑 | ~10 min |
列印設定與光安全要點
- 腔體 4 層牆+蓋 30% 填充:目標是暗室等級遮光——蓋好後把手機開閃光燈塞進去,四周縫隙不得見光;4 層牆不夠就內貼鋁箔膠帶(同時解決反光均勻度)
- 蓋上嚴禁透明視窗:405nm 穿透透明壓克力/PLA 如同可見光,「能看見裡面」與「擋得住」不可兼得——看固化狀態的正確姿勢是閃檢模式,不是開蓋偷看
- 反光罩印白 PLA、出爐全內貼鋁箔膠帶:燈板 3W 的功率要全部用在印件上,貼與不貼固化時間差 30%
- 旋轉平台底面開十字減重肋但保持 ≥2mm 平整度:偏擺 >1mm 會在固化中讓細件「拜拜」拍倒
- DHT22 裝燈板對角、離牆 10mm:貼牆讀到的是壁溫、貼燈讀的是燈溫,都不是樹脂溫度
💻 程式碼
/* ============================================================
* ESP32-C2 樹脂後固化轉台 | Resin Cure Turntable C2
* ------------------------------------------------------------
* 功能:四樹脂配方(NVS 可存校準值);溫度+燈衰雙補償;
* 開蓋 50ms 遮斷 UV;>45°C 暫停;閃檢模式;
* 倒數三嗶啟動;完工推 Telegram。
* 硬體:ESP32-C2 SuperMini + 28BYJ-48/ULN2003 + 405nm 燈板
* (IRLZ44N) + 乾簧管 + DHT22 + KY-040 + SSD1306 + 蜂鳴器
* 供電:12V/2A → MP1584 降 5.1V(UV 燈走 12V 獨立迴路)
* 相依:arduino-esp32 core >= 3.0;Adafruit_SSD1306 >= 2.5.x
* ============================================================ */
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Preferences.h>
#include <time.h>
#include <math.h>
/* ------------------------------------------------------------
* 1. 使用者設定區
* ------------------------------------------------------------ */
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* TG_BOT_TOKEN = "YOUR_BOT_TOKEN"; // 不需要推播可留空字串
const char* TG_CHAT_ID = "YOUR_CHAT_ID";
/* 引腳(避開 GPIO18/19 USB 與 GPIO20 strapping) */
const uint8_t PIN_STP[4] = {1, 3, 4, 5}; // ULN2003 IN1..IN4
const uint8_t PIN_UV = 0; // LEDC → IRLZ44N
const uint8_t PIN_REED = 2; // 乾簧管(閉合=蓋好=GND)
const uint8_t PIN_DHT = 10;
const uint8_t PIN_ENC_A = 6, PIN_ENC_B = 7, PIN_ENC_SW = 8;
const uint8_t PIN_BUZZ = 9;
const uint8_t PIN_SDA = 15, PIN_SCL = 16;
Adafruit_SSD1306 oled(128, 32, &Wire, -1);
Preferences nvs;
/* ------------------------------------------------------------
* 2. 配方表與雙補償模型
* ------------------------------------------------------------ */
struct Recipe { const char* name; uint16_t baseSec; uint8_t pwmPct; };
Recipe recipes[4] = {
{"STANDARD", 150, 80}, // 標準樹脂 2.5 分 @80%
{"TOUGH", 420, 90}, // 韌性 7 分 @90%
{"WASHABLE", 240, 70}, // 水洗 4 分 @70%(過固化發脆)
{"CASTING", 600, 60}, // 澆灌 10 分 @60%(低功率防開裂)
};
uint8_t selRecipe = 0;
/* 溫度補償:樹脂光交聯在低溫下反應速率指數下降,
係數取樹脂資料表常見量級(12°C 以下直接拒絕,強烤=外熟內生)*/
float tempFactor(float t) {
if (t < 12.0f) return -1.0f; // 負值=拒絕啟動
if (t < 15.0f) return 1.8f;
if (t < 20.0f) return 1.4f;
return 1.0f;
}
/* 燈衰補償:累計時數每 100 小時功率係數 -5%,時間按比例加長。
為什麼調時間不調功率:PWM 滿區間失真的風險比加 15% 時間高。
為什麼存「秒」不存「小時×10」:一爐才 2–10 分鐘,整數小時制
除法直接歸零,燈衰永遠補不到——單位要配得上事件尺度 */
uint32_t ledSecTotal = 0; // UV 累計點亮秒數
float ledFactor() { return constrain(100.0f /
(100.0f - floor(ledSecTotal / 360000.0f) * 5.0f), 1.0f, 1.6f); }
/* ------------------------------------------------------------
* 3. DHT22 手撕單線時序
* ------------------------------------------------------------ */
/* 用途:讀腔溫與濕度。參數:t/hum 輸出指針/回傳:true=成功
為什麼手撕:core 3.x 無內建 DHT 驅動,而本機只用溫度一路 */
bool readDHT22(float& t, float& hum) {
uint8_t data[5] = {0};
noInterrupts();
pinMode(PIN_DHT, OUTPUT); digitalWrite(PIN_DHT, LOW);
delayMicroseconds(1100); // 起始信號 >1ms 低
pinMode(PIN_DHT, INPUT_PULLUP);
uint32_t t0 = micros();
interrupts();
if (digitalRead(PIN_DHT) == HIGH) return false; // 線沒接/沒反應
for (uint8_t bit = 0; bit < 40; bit++) {
uint32_t ts[2];
for (uint8_t e = 0; e < 2; e++) { // 等兩個邊沿
uint8_t lvl = digitalRead(PIN_DHT); uint32_t s = micros();
while (digitalRead(PIN_DHT) == lvl) {
if (micros() - s > 100) { interrupts(); return false; } // 超時
}
ts[e] = micros();
}
uint8_t byteIdx = bit / 8, bitIdx = 7 - (bit % 8);
if (ts[1] - ts[0] > 40) data[byteIdx] |= (1 << bitIdx); // 高電位>40µs=1
}
interrupts();
if ((uint8_t)((data[0]+data[1]+data[2]+data[3]) & 0xFF) != data[4]) return false;
hum = ((data[0] << 8) | data[1]) / 10.0f;
t = ((data[2] & 0x7F) << 8 | data[3]) / 10.0f * ((data[2] & 0x80) ? -1 : 1);
return true;
}
/* ------------------------------------------------------------
* 4. 步進(半步表)
* ------------------------------------------------------------ */
const uint8_t HALFSTEP[8] = {1,3,2,6,4,12,8,9};
uint8_t stepIdx = 0; uint32_t stepMs = 0;
bool motorOn = false;
const uint16_t STEP_MS = 3; // 3ms/半步 ≈ 8 秒/轉
void stepTick() {
if (!motorOn) return;
if (millis() - stepMs < STEP_MS) return;
stepMs = millis();
stepIdx = (stepIdx + 1) & 7;
for (uint8_t i = 0; i < 4; i++)
digitalWrite(PIN_STP[i], (HALFSTEP[stepIdx] >> i) & 1);
}
void motorStop() { // 暫停/完工都斷電:28BYJ 通電鎖軸會發熱
for (uint8_t i = 0; i < 4; i++) digitalWrite(PIN_STP[i], LOW);
motorOn = false;
}
/* ------------------------------------------------------------
* 5. 主控狀態機
* ------------------------------------------------------------ */
enum S : uint8_t { ST_IDLE, ST_COUNT, ST_RUN, ST_PAUSE, ST_FLASH };
S state = ST_IDLE;
uint32_t runTotalS = 0, runLeftS = 0;
uint32_t stateT0 = 0, lastBeatS = 0, pauseT = 0; // pauseT:暫停計時用
float chamberT = 25.0f; uint32_t lastDhtMs = 0;
bool doorClosed() { return digitalRead(PIN_REED) == LOW; }
void uvWrite(uint8_t pct) { ledcWrite(0, map(pct, 0, 100, 0, 1023)); }
void beep(uint8_t n, uint16_t ms = 90) {
for (uint8_t i = 0; i < n; i++) {
digitalWrite(PIN_BUZZ, HIGH); delay(ms);
digitalWrite(PIN_BUZZ, LOW); delay(ms);
}
}
/* 開始一爐:補償計算在此一次性凍結,中途開關門不重算——
否則每次開蓋散熱都會讓時間縮短,品質隨開蓋次數漂移 */
void startRun() {
if (!doorClosed()) { beep(3, 60); return; } // 蓋好才能啟動
if (tempFactor(chamberT) < 0) { beep(3, 60); return; } // 太低溫
runTotalS = runLeftS = recipes[selRecipe].baseSec
* tempFactor(chamberT) * ledFactor();
state = ST_COUNT; stateT0 = millis(); beep(1); // 倒數三聲
}
void loopRun() {
if (!doorClosed() && state == ST_RUN) { // 主迴路:門磁>狀態機
uvWrite(0); motorStop(); state = ST_PAUSE; pauseT = millis(); beep(1, 180);
}
if (state == ST_PAUSE) { // 暫停逾 10 分鐘自動終止:
if (millis() - pauseT > 600000UL) { // 門開著散熱越久補償越假,
uvWrite(0); motorStop(); state = ST_IDLE; // 不如中止這爐重來
beep(2, 180);
tgSend("Cure aborted: door open >10min");
} else if (doorClosed() && chamberT < 45.0f) {
state = ST_RUN; beep(1);
}
}
if (chamberT > 45.0f && state == ST_RUN) { // 過溫暫停
uvWrite(0); motorStop(); state = ST_PAUSE; pauseT = millis();
}
if (state == ST_COUNT && millis() - stateT0 > 3000) {
state = ST_RUN; uvWrite(recipes[selRecipe].pwmPct); motorOn = true;
}
if (state == ST_RUN) {
uint32_t now = millis() / 1000;
if (now != lastBeatS) { // 每秒倒數
lastBeatS = now;
if (runLeftS > 0) runLeftS--;
if (runLeftS % 30 == 0 && runLeftS > 0) beep(1, 25); // 30 秒 heartbeat
if (runLeftS == 0) finishRun();
}
}
}
void finishRun() {
uvWrite(0); motorStop(); state = ST_IDLE;
ledSecTotal += runTotalS; // 計入燈衰模型
nvs.putUInt("ledsec", ledSecTotal);
beep(3, 160);
tgSend(String("Cure done: ") + recipes[selRecipe].name + " " +
String(runTotalS) + "s @" + String(chamberT, 1) + "C");
}
/* 閃檢:0.3 秒 15% UV + 平台轉一圈。看表面反光的「濕潤鏡面」
是否消失——鏡面仍在=沒乾,霧面=到位。不開蓋的驗收法 */
void flashCheck() {
if (!doorClosed()) { beep(2, 60); return; }
state = ST_FLASH; motorOn = true; uvWrite(15);
delay(8200); // ≈ 一圈
uvWrite(0); motorStop(); state = ST_IDLE;
}
/* ------------------------------------------------------------
* 6. Telegram
* ------------------------------------------------------------ */
String urlencode(const String& s) {
String o;
for (unsigned int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (isalnum(c) || c == '-' || c == '.' || c == ' ') o += c;
else { char b[4]; sprintf(b, "%%%02X", (unsigned char)c); o += b; }
}
return o;
}
void tgSend(const String& msg) {
if (String(TG_BOT_TOKEN).length() < 10) return; // 未設定就靜默跳過
HTTPClient h;
h.begin(String("https://api.telegram.org/bot") + TG_BOT_TOKEN + "/sendMessage");
h.addHeader("Content-Type", "application/x-www-form-urlencoded");
h.POST("chat_id=" + String(TG_CHAT_ID) + "&text=" + urlencode(msg));
h.end();
}
/* ------------------------------------------------------------
* 7. 編碼器(2ms 輪詢 FSM)+顯示
* ------------------------------------------------------------ */
int8_t encLastA; int32_t encAcc = 0; bool swLast = true; uint32_t swT0 = 0;
bool swLongFired = false;
void encRead() {
uint8_t a = digitalRead(PIN_ENC_A), b = digitalRead(PIN_ENC_B);
if (a != encLastA) encAcc += (a == b) ? 1 : -1; // A 沿判方向
encLastA = a;
if (abs(encAcc) >= 2) { // 一格=2 個 A 沿
if (state == ST_IDLE) selRecipe = (selRecipe + 4 + (encAcc > 0 ? 1 : 3)) % 4;
else if (state == ST_RUN) runLeftS += (encAcc > 0) ? 30 : (runLeftS > 30 ? -30 : 0);
encAcc = 0;
}
bool sw = digitalRead(PIN_ENC_SW);
if (sw == LOW && swLast) { swT0 = millis(); swLongFired = false; }
if (sw == LOW && !swLongFired && millis() - swT0 > 1200) { // 長按=啟停
swLongFired = true;
if (state == ST_IDLE) startRun();
else if (state == ST_RUN || state == ST_PAUSE) { uvWrite(0); motorStop(); state = ST_IDLE; }
}
if (sw == HIGH && !swLast && millis() - swT0 < 600 && !swLongFired) {
if (state == ST_IDLE) flashCheck(); // 短按=閃檢
}
swLast = sw;
}
void draw() {
oled.clearDisplay();
oled.setTextSize(1); oled.setTextColor(SSD1306_WHITE);
if (state == ST_IDLE) {
oled.printf("%s %lum%02lus led%%%.0f\n", recipes[selRecipe].name,
recipes[selRecipe].baseSec / 60, recipes[selRecipe].baseSec % 60,
100.0f / ledFactor());
oled.printf("T%.1fC %s hold:start tap:chk", chamberT,
doorClosed() ? "shut" : "OPEN!");
} else if (state == ST_RUN || state == ST_PAUSE) {
oled.setTextSize(2); oled.setCursor(0, 0);
oled.printf("%2lu:%02lu", runLeftS / 60, runLeftS % 60);
oled.setTextSize(1);
oled.printf(" %s T%.1f %s", recipes[selRecipe].name, chamberT,
state == ST_PAUSE ? "PAUSE" : "");
} else if (state == ST_COUNT) {
oled.printf("3..2..1 %s\n", recipes[selRecipe].name);
oled.printf("%lu%% %lus", recipes[selRecipe].pwmPct, runTotalS);
}
oled.display();
}
/* ------------------------------------------------------------
* 8. setup / loop
* ------------------------------------------------------------ */
void setup() {
Serial.begin(115200);
nvs.begin("cure");
ledSecTotal = nvs.getUInt("ledsec", 0);
for (uint8_t i = 0; i < 4; i++) { pinMode(PIN_STP[i], OUTPUT); digitalWrite(PIN_STP[i], LOW); }
pinMode(PIN_BUZZ, OUTPUT); pinMode(PIN_REED, INPUT_PULLUP);
pinMode(PIN_ENC_A, INPUT_PULLUP); pinMode(PIN_ENC_B, INPUT_PULLUP);
pinMode(PIN_ENC_SW, INPUT_PULLUP);
ledcSetup(0, 5000, 10); ledcAttachPin(PIN_UV, 0);
uvWrite(0); // 上電第一件事:UV 滅
Wire.begin(PIN_SDA, PIN_SCL);
oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
encLastA = digitalRead(PIN_ENC_A);
if (String(WIFI_SSID).length() > 2) {
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
configTime(8 * 3600, 0, "pool.ntp.org");
}
Serial.println("Cure station ready. cmd: uvcal=rst / dbg / tg");
}
void loop() {
stepTick();
encRead();
if (millis() - lastDhtMs > 2000) { // DHT22 最慢 2 秒一讀
lastDhtMs = millis();
float h; readDHT22(chamberT, h); // 讀取失敗沿用舊值
}
/* 門磁真相寫在 loopRun 第一行:UV 的開與關只認物理門磁,
狀態機「以為在跑」不構成繼續開燈的理由 */
loopRun();
/* 序列指令 */
if (Serial.available()) {
static String buf; char ch = Serial.read();
if (ch == '\n') {
buf.trim();
if (buf == "uvcal=rst") { ledSecTotal = 0; nvs.putUInt("ledsec", 0); Serial.println("led hours reset"); }
else if (buf == "dbg") Serial.printf("T%.1f door%d ledh%.1f run%lu\n", chamberT, doorClosed(), ledSecTotal / 3600.0f, runLeftS);
else if (buf == "tg") tgSend("cure station test");
buf = "";
} else buf += ch;
}
static uint32_t drT = 0;
if (millis() - drT > 250) { drT = millis(); draw(); }
}
uvWrite(0) 在 setup 第一行——無論韌體怎麼重啟,UV 一定先滅再受門磁約束。測試 IRLZ44N 迴路時先裝燈板、後接 12V,反向操作會在接電瞬間全功率閃燈。🏗️ 組裝步驟
- 降壓先調再接:MP1584 空載調 5.1V → 萬用表複測 → 才接 C2;12V 迴路先不帶燈板測通斷
- UV 迴路組裝:IRLZ44N 鎖散熱片(3W 全功率 TO-222 會燙);燈板鋁基面貼腔壁金屬面導熱
- 轉台機械:馬達鎖固定座→聯軸器→平台試轉,偏擺 <1mm 才收工;防滑圈最後粘
- 門磁對位:蓋合狀態乾簧管與磁鐵 ≤8mm,側標線——之後每次挪機都複查這條線
- 首爐校準:每種新樹脂印一片 20×20×4mm 測試片,從配方基準時間起 ±30 秒遞減搜尋「指推不黏、指甲無痕」的最短時間,回填 NVS
🔧 問題排除
| 問題 | 原因 | 解決方法 |
|---|---|---|
| 表面仍黏手 | 時間/燈衰/低溫三補償沒吃滿,或樹脂批次異常 | dbg 看腔溫是否 <20°C;新樹脂重跑步驟 5 校準;查燈板有無單顆燈珠死掉(放大鏡看螢光粉層) |
| 馬達抖動不轉 | 5V 從 UV 電源串供、步進電流壓降 | 馬達 5V 直取自 MP1584 輸出端(粗線),與 C2 共點不串接 |
| 開蓋 UV 不滅 | 磁鐵間隙 >8mm 或乾簧管方向錯 | 用 dbg 的 door 欄位對準;磁鐵面朝簧片側(不是側面) |
| 印件冒泡/霧化 | 腔溫過高或功率過大 | >45°C 暫停已內建;把配方功率下調一档、時間加長——固化品質換功率永遠是錯的方程 |
| DHT 讀值 -127 或不動 | 單線時序供電不足 | DHT22 供電別與步進共地線回路的尾段;上拉改 4.7k(舊 10k 邊緣) |
| 完工推播沒收到 | Wi-Fi 未連上就 finishRun | tgSend 有失敗靜默設計;看 OLED wifi 狀態或 tg 指令手動測 |
🎯 進階延伸
- 清洗攪拌槽前置站:同一顆 C2 加一路继電器控氣缸氣攪 IPA,「洗 90 秒→瀝乾風淋 60 秒→自動進本機固化」三段流水——本系列下一顆需求炸彈
- UV 劑量片閉環:市售 UV 指示貼紙進腔,出爐拍照比色,把手機 App 的比色結果回填 NVS——從「時間模型」升級「劑量模型」
- 夜排程:接第 05 篇 AQI 站的精神——把固化排進離峰電價時段,Telegram 下單「tonight standard ×4」自動開爐
- N₂ 防氧抑制介面:澆灌玩家的最愛——腔體預留 4mm 氣嘴,繼電器控制電磁閥,注氮 30 秒再點燈,氧阻層消失
- 燈珠健康自檢:腔底貼一片螢光紙,每 100 爐閃檢+光敏電阻讀回輝光,低于閾值提醒換燈板
📦 檔案清單
resin-cure-station/
├── src/
│ └── resin-cure-station.ino # 本頁完整韌體
├── stl/
│ ├── chamber.stl # 固化腔體+鉸鏈蓋(PETG、無視窗)
│ ├── reflector.stl # UV 反光罩(出爐貼鋁箔)
│ ├── turntable.stl # 旋轉平台(Ø110、十字肋)
│ ├── motor-mount.stl # 馬達固定座
│ ├── knob.stl # 編碼器旋鈕
│ └── reed-holder.stl # 門磁固定座
├── f3d/
│ └── resin-cure-station.f3d # Fusion 360 原始檔
├── manual/
│ └── assembly.html # 圖文組裝說明書
└── README.md
🎯 結語
這台機器最得意的功能不是轉台也不是推播,是那個不起眼的閃檢模式:裝機第三天起,我再也沒有「開蓋偷看一眼」的衝動——0.3 秒低功率一閃,鏡面還在就在爐裡再等一下。後來我想明白,它治的不是固化不足,是樹脂玩家的強迫症輪詢:你以為你在檢查作品,其實作品早就在檢查你的耐心。三補齊(配方、溫度、燈衰)之後,「同一台機器、同一個設定、同一個品質」這句話第一次在 resin 世界成立——而那正是「工作站」三個字應該負責的事。