📋 專案概述

樹脂印到第三天,你一定會遇見終點站難題:清洗完的軟趴趴印件,拿 UV 檯燈照 10 分鐘、翻面再 10 分鐘——人守著、眼睛不敢直視、總有一面照不勻;黏手放一夜,隔天摸起來還是像拔絲的起司。你開始理解為什麼树脂玩家的置物架上,總有一台「固化機」的位置。

市售固化機兩條燈管一個轉台,NT$1,500 起跳,功能本質上是「定時器+轉」。真正咬人的四個坑它一個都沒填:

本機 NT$325:28BYJ-48 轉台+405nm 燈板+乾簧管門磁+DHT22,把四個坑全部填進韌體:

算盤:最便宜的市售固化機 NT$1,500 上下、無門磁無補償;本機 325 元,材料貴在螢幕和 DHT22——把「定時器+轉」升級成「配方機+補償機+安全機」,多花的钱全部花在感測器上。而對樹脂玩家真正的成本是失敗的印件:一顆 90 分鐘的雕像因為固化不足在後處理時斷手,樹脂錢是小、時間是大。

🩺 誠實聲明:本機是「時間×功率×溫度」三變數的工程估算機,不是固化品質儀——树脂的光引發體系各廠不同,每種新樹脂仍要跑一次爐內測試片校準基準時間(組裝說明書步驟 5)。本機保證的是:基準時間一旦校好,之後每一爐都打得中同一個標準。

為什麼選 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 SuperMini1~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 的主迴路
溫濕度DHT221~40 元腔內溫度=補償模型的變數,裝在燈板對角
編碼器KY-040 旋轉編碼器帶按鈕1~12 元選單+啟動+閃檢,本機唯一操作件
螢幕SSD1306 0.91" I2C(128×32)1~65 元面朝操作者,運作中顯示剩餘秒
蜂鳴器有源蜂鳴器帶板 5V1~10 元倒數、完工、異常三種節拍
列印料PLA 60g+PETG 20g—~20 元腔體長期微熱,講究可全 PETG
合計~327 元市售固化機的 1/4

🔌 接線圖

ESP32-C2 SuperMini 5V(MP1584 降壓) GPIO1/3/4/5 ● GPIO0 (UV PWM) ● GPIO2 (門磁) ● GPIO10 (DHT) ● 5V / GND ● ● GPIO6/7/8 編碼器 ● GPIO9 蜂鳴 ● GPIO15/16 I2C ULN2003 IN1-4 ← GPIO1/3/4/5 28BYJ-48 UV 燈板 405nm 12V・3W・鋁基板貼殼 IRLZ44N 12V 高側回路的低側開關 GPIO0 → 100Ω → 閘極(10k 下拉) Drain→燈板− 乾簧管(蓋側磁鐵) 間隙 ≤8mm DHT22(燈板對角) KY-040 旋鈕 STANDARD 2:00 OLED 0.91" 12V→MP1584→5.1V
▲ 三條紀律:① UV 燈走 12V 獨立迴路,PWM 只開關 IRLZ44N,燈板電流不過主板;② 門磁採「韌體 10ms 輪詢+UV 只認門磁不認狀態機」雙保險;③ 步進馬達 5V 直供自降壓輸出,與 C2 共地單點。

ASCII 對照表

ESP32-C2 SuperMini 周邊 ┌──────────────┐ │ GPIO1 ●─────┼── ULN2003 IN1 ─┐ │ GPIO3 ●─────┼── ULN2003 IN2 ├─ 28BYJ-48(半步表) │ GPIO4 ●─────┼── ULN2003 IN3 │ │ GPIO5 ●─────┼── ULN2003 IN4 ─┘ │ GPIO0 ●─────┼── 100Ω → IRLZ44N 閘極(10k 下拉到 GND) │ GPIO2 ●─────┼── 乾簧管 → GND(內部上拉;閉合=蓋好) │ GPIO10 ●─────┼── DHT22 DATA(4.7kΩ 上拉 3V3) │ GPIO6/7/8 ●──┼── 編碼器 A / B / SW(內部上拉) │ GPIO9 ●─────┼── 蜂鳴器(有源) │ GPIO15/16 ●──┼── OLED SDA / SCL │ 5V ●─────┼── MP1584 輸出(先空載調 5.1V!) │ GND ●─────┼── 單點共地(ULN2003 地線先回主板接點) └──────────────┘ UV 燈板:12V(+) → 燈板(+);燈板(−) → IRLZ44N Drain;Source → 12V(−) ※ 避開 GPIO18/19(USB)與 GPIO20(strapping)
⚠️ MP1584 調壓紀律:降壓模組出廠預設可能是 9V+,直接接 C2 就是秒殺。組裝第一步「空載調壓 5.1V → 萬用表複測 → 再接板」,這顆多圈電位器逆時針降壓——三步寫進組裝說明書步驟 1,跳過的人都付過學費。
👁️ 405nm 光安全:UV-A 無痛感(你看著覺得「只是偏紫的光」),但視網膜曝露是累計的。本機三層防護:金屬/PLA 全遮光腔體(蓋無視窗)、乾簧管開蓋即斷、閃檢模式 0.3 秒低功率。任何時候腔體有縫可窺光,停止使用直到修好——光安全這事,機械遮斷永遠優先於軟體遮斷。

🖨️ 3D 列印零件

零件清單

零件數量材質建議顏色列印時間
固化腔體+鉸鏈蓋(120×90×60mm,蓋無視窗)1PETG(耐微熱)黑~2 hr
UV 反光罩(45° 錐面,出爐貼鋁箔膠帶)1PLA白~25 min
旋轉平台(Ø110mm,底面十字肋+矽膠防滑圈)1PLA任意~35 min
馬達固定座(28BYJ 耳朵兩螺+軸聯軸器)1PETG黑~20 min
編碼器旋鈕(滾花)1PLA任意~12 min
門磁固定座(蓋側磁鐵槽+腔側套管)1PLA黑~10 min

列印設定與光安全要點

💻 程式碼

📐 註解規範:區塊以框註分段;函式附框註(用途/參數/回傳);行內註解解釋「為什麼」。DHT22 單線時序手撕(core 無內建驅動);步進走半步表 3ms;UV 只認門磁的物理真相。外部相依僅 Adafruit SSD1306/GFX。
/* ============================================================
 *  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,反向操作會在接電瞬間全功率閃燈。

🏗️ 組裝步驟

📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作桌 → 開啟 HTML 組裝說明書
  1. 降壓先調再接:MP1584 空載調 5.1V → 萬用表複測 → 才接 C2;12V 迴路先不帶燈板測通斷
  2. UV 迴路組裝:IRLZ44N 鎖散熱片(3W 全功率 TO-222 會燙);燈板鋁基面貼腔壁金屬面導熱
  3. 轉台機械:馬達鎖固定座→聯軸器→平台試轉,偏擺 <1mm 才收工;防滑圈最後粘
  4. 門磁對位:蓋合狀態乾簧管與磁鐵 ≤8mm,側標線——之後每次挪機都複查這條線
  5. 首爐校準:每種新樹脂印一片 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 未連上就 finishRuntgSend 有失敗靜默設計;看 OLED wifi 狀態或 tg 指令手動測

🎯 進階延伸

📦 檔案清單

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 世界成立——而那正是「工作站」三個字應該負責的事。

← 上一篇:ESP32-C3 輻射蓋格計數器 📚 系列索引 下一篇:ESP32-S3 家庭對講機 →