📋 專案概述

上個月你把 3W 雷雕機升級成切 3mm 椴木的「接單小炮」,第一張椪糖造型吊飾單就做焦了八片:切面焦黑、板邊被煙燻出一圈黃漬,客人傳訊問「這是燒過的嗎」。買家不知道,你的雷雕機根本沒有空氣輔助。於是你買了台氣泵接上——新問題接踵而來:這種泵是 92dB 的怒吼型選手,雕刻一個 40 分鐘的 job,氣泵要全程嗷嗷叫;更扯的是它跟雷射完全是兩個世界,開機要記得開氣、暫停要記得關氣。上週五晚上你趕單到凌晨兩點,忘記開氣跑了 40 分鐘——今天打開機殼,focus 鏡片鍍膜燒出一個直徑 0.5mm 的白點。鏡片 NT$450 事小,「氣壞了」这件事,你的機器一點都不知道:氣管早就從接頭崩開,雷射照樣噴了半小時。

市售商品:

本模組把「氣」和「光」綁成硬連鎖(hard interlock):用兩顆光耦合器從雷射主板隔空偷看 FIRE(點火)與 EN(使能)兩條訊號——不共地、不碰主板邏輯——然後管理一台幫浦和一顆電磁閥的完整氣序:

💰 成本效益:全機 ~NT$705。一片 focus 鏡片 NT$450、一次焦壞的客戶訂單 NT$1,200+心情。氣assist 把切口焦黑率直接砍半(氧被吹走=燃燒變缺氧碳化),接單品質的提升是立即的。

為什麼選 ESP32-C3?

需求C3 的優勢
反應要快單核 RISC-V 160MHz+GPIO 中斷延遲 <2µs——斷氣保護從電流異常到蜂鳴器響,全鏈路 300ms 內完成,綽綽有餘
ADC 量電流ADC1 掛 GPIO0-4,ACS712 的模擬輸出直接吃;C3 的 ADC1 與 Wi-Fi 共電源的噪訊問題用中位數濾波解決
模組比咖啡便宜NT$120 就有 Wi-Fi+藍牙+中斷能力;這台的功能本就不需要雙核,把钱花在幫浦上
貼著機器裝單板低功耗、可直接固定在雷雕機電源上蓋內;C3 溫度範圍也撐得住機殼裡夏天 55°C

🧱 硬體清單

項目型號數量預估單價備註
主控板ESP32-C3 SuperMini / DevKitM-11~120 元ADC1 於 GPIO0-4
幫浦12V 無油隔膜幫浦(60L/min 級)1~280 元附減震腳;92dB 是常態,靠軟啟動減驚嚇
電磁閥12V 二通常閉電磁閥(銅閥座)1~90 元N/C:斷電=關氣,安全デフォルト
電流感測ACS712-05A 模組1~60 元串在幫浦負端;100mV/A
光耦PC817C 光耦合器模組2~10 元FIRE/EN 各一顆,隔離 <5ms 延遲不影響
MOSFETIRLZ44N(邏輯閘極)+10kΩ 下拉2~40 元Q1 幫浦(PWM)/Q2 閥(開關)
顯示SSD1306 0.96" I2C1~90 元狀態機一目了然
蜂鳴器源式有源蜂鳴器 5V1~15 元斷氣警報,比推播快
電源沿用雷雕機 12V(或獨立 12V 3A)1~0 元ACS712 需 5V→從 C3 板取
列印+氣材PETG 列印件+4/6mm 軟管+快速接頭—~100 元分流塊、泵架、控制盒
合計~705 元原厂高階機的 5%;一片鏡片的 1.6 倍

🔌 接線圖

ESP32-C3 GPIO2←FIRE(光耦) GPIO3←EN(光耦) GPIO0←ACS712 GPIO6→Q1 泵 GPIO5→Q2 閥 GPIO8/9 OLED 雷雕機主板(僅訊號) FIRE 觸發線 ─┐ EN 使能線  ─┤ +12V via 1k PC817 ×2 隔離,不共地 ACS712-05A(串泵負端) 12V− → IP+ IP− → 泵− OUT → GPIO0 (1k+3V3鉗位) 12V 幫浦 泵+ ←12V 泵− ←ACS712→Q1→GND 12V 電磁閥(N/C) 斷電=關氣 續流二極體必焊! SSD1306 SDA/SCL←8/9 12V 電源與控制板共地;PC817 兩側完全隔離 Q1/Q2 源極→GND、閘極 10kΩ 下拉——上電瞬間幫浦不亂噴
▲ 光耦是整個設計的良心:雷射主板與控制板之間只有光,沒有銅——你的保固和炸機風險都被隔在另一邊。

詳細接線

ESP32-C3 腳位連接說明
GPIO2PC817#1 輸出端FIRE 點火訊號(經 1kΩ 驅動內側 LED,輸入側並接主板觸發線)
GPIO3PC817#2 輸出端EN 使能訊號(主板 L/ON 線)
GPIO0ACS712 OUTADC1;串於幫浦電源負端量總電流
GPIO6Q1 IRLZ44N 閘極幫浦 PWM 軟啟動(1kHz);10kΩ 閘極下拉
GPIO5Q2 IRLZ44N 閘極電磁閥開關;閥線圈並 1N4007 續流
GPIO8 / GPIO9OLED SDA / SCL狀態機與即時電流顯示
GPIO1按鈕(對 GND)短按重設統計、長按強制通氣(除塵用)
雷雕機主板 本模組(PC817 隔離) FIRE ──┬─ 主板側        C3 側:輸出的 C-E 並 10kΩ 上拉 3V3 │ 1kΩ→PC817 LED→GND └──→ GPIO2(火脈衝=低電位) EN ──┴─(同 topology,第二顆)└──→ GPIO3 ┌──────────────────────────────┐ 12V+ ──┬─幫浦+─┘ 幫浦− → ACS712 IP+ / IP− ─→ Q1 D│S→GND (G←GPIO6 PWM) │ OUT→1k→GPIO0 │ └─電磁閥+ ── 閥− → Q2 D│S→GND (G←GPIO5) │ 閥線圈 ∥ 1N4007 OLED ←GPIO8/9 按鈕 GPIO1→GND 蜂鳴器 ←5V源式
⚠️ ACS712 供電 5V、輸出零點 2.5V:進 C3 的 3.3V ADC 前串 1kΩ 並對 3.3V 接蕭特基鉗位二極體——幫浦卡死瞬間輸出可能衝過 3.3V,這顆二極體是你 ADC 的保險絲。
⚠️ 電磁閥是感性負載:1N4007 續流二極體沒焊,Q2 下個 job 就走。幫浦同理(MOSFET 端已有體二極體,但建議再加一顆 TVS/RC -snubber)。

🖨️ 3D 列印設計

零件材質/設定數量備註
manifold 分流塊PETG/0.2mm/3 壁/40%(氣壓要受)1一進三出、三噴嘴同心夾持 360° 環繞光軸
nozzle-ring 噴嘴適配環PETG/0.2mm/3 壁/50%2內徑對應 12/14mm 雷射頭外徑,M3 鎖固
pump-mount 泵架PETG/0.28mm/2 壁/25%1四角穿矽膠減震柱,泵體懸吊固定
duct-clip 軟管夾組PETG/0.24mm/2 壁/30%6X 臂隨行鏈走線,留 8cm 應變環
box-control 控制盒PETG/0.2mm/2 壁/25%1鎖在機殼電源上蓋,OLED 觀察窗

雷雕機台附近有鏡片反射的紫外與高溫,PETG 是底線、PLA 會蠕變;分流塊噴嘴角度設計為下傾 15°,氣簾正好壓住切口的上升煙柱。

💻 Arduino 程式碼

Arduino IDE 2.x+espressif 3.x core(開發板 ESP32C3 Dev Module);函式庫:Adafruit SSD1306、Adafruit GFX。

/* ============================================================
 *  雷雕同步空氣輔助  laser-airassist.ino
 *  ------------------------------------------------------------
 *  功能:氣光硬連鎖(預吹/跟火/後吹/排煙四段氣序)、
 *        幫浦電流斷氣保護、軟啟動、統計推 NAS
 *  硬體:ESP32-C3|GPIO2←FIRE、GPIO3←EN(PC817,低有效)
 *        GPIO6→泵 PWM、GPIO5→閥、GPIO0←ACS712、
 *        GPIO8/9 I2C、GPIO1 按鈕、GPIO10 蜂鳴器
 *  供電:12V 共用電源、GND 共點;ACS712 供 5V
 *  相依:Adafruit_SSD1306、Adafruit_GFX(core 3.x)
 * ============================================================ */
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <Preferences.h>

/* --- 0. 配置 --- */
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASS";
const char* NAS_URL   = "http://192.168.8.60:1880/airassist";
const char* NAS_TOKEN = "change-me";

#define PIN_FIRE 2      // PC817 低有效:脈衝=LOW
#define PIN_EN   3
#define PIN_CUR  0      // ADC1!GPIO6/7 是 ADC2,Wi-Fi 開著會死
#define PIN_PUMP 6
#define PIN_VALVE 5
#define PIN_BTN  1
#define PIN_BUZ  10

const uint32_t PRE_BLOW_MS   = 500;    // 預吹
const uint32_t POST_BLOW_MS  = 800;    // 後吹
const uint32_t SCAVENGE_MS   = 15000;  // job 結束排煙吹
const uint32_t ARM_TIMEOUT_MS= 180000; // 3 分鐘無火自動離崗
const float    I_MIN = 0.40f;          // 斷氣(失壓)門檻 A
const float    I_MAX = 2.20f;          // 塞死門檻 A
const uint32_t I_MAX_HOLD = 10000;     // 塞死持續判定
const float    ACS_ZERO = 2.50f, ACS_K = 0.100f;  // 零點 V、V/A

/* --- 1. 狀態機 --- */
enum St { IDLE, ARM, CUT, BLOW, SCAV, ALARM } st = IDLE;
uint32_t stEnter = 0, lastFire = 0, enSince = 0;
float amps = 0; uint32_t pumpDuty = 0;  // 0..1000
uint32_t firePulses = 0, fireUs = 0; float pumpSecs = 0, filterSecs = 0;
uint32_t jobs = 0, alarmSince = 0; uint8_t alarmCode = 0; // 1=斷氣 2=塞死
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
Preferences prefs;

/* --- 2. FIRE 中斷:記錄與計數 ---
 * 參數:無|回傳:無
 * 為什麼放在 ISR:火脈衝可短至幾十 µs,主迴圈輪詢必漏拍。
 * millis() 在 ESP32 ISR 内是安全讀取(僅讀計數變數) */
volatile uint32_t firePulseCnt = 0;
volatile uint32_t fireStampMs = 0;
void IRAM_ATTR fireISR() {
  if (digitalRead(PIN_FIRE) == LOW) {     // 只看有效邊
    firePulseCnt++;
    fireStampMs = millis();
  }
}

/* --- 3. 電流讀取:中位數濾波 ---
 * 參數:無|回傳:幫浦電流 A(零點誤差由全域 curOffset 扣除)
 * 為什麼中位數:C3 ADC1 與 Wi-Fi PA 共享電源路徑,偶發毛刺用均值扛不住 */
float readAmps() {
  uint16_t s[9];
  for (int i = 0; i < 9; i++) { s[i] = analogRead(PIN_CUR); delayMicroseconds(200); }
  for (int i = 0; i < 8; i++) for (int j = i + 1; j < 9; j++)
    if (s[j] < s[i]) { uint16_t t = s[i]; s[i] = s[j]; s[j] = t; }
  float v = s[4] * 3.3f / 4095.0f;            // 11dB 衰減檔:滿量程約 3.3V
  return constrain((v - ACS_ZERO) / ACS_K, 0, 5);   // ACS712@5V:零點 2.5V
}
float curOffset = 0;   // 零點校準值(A),setup 裡量

/* --- 4. 氣序狀態機:本模組的靈魂 ---
 * 參數:dtMs 本拍毫秒|回傳:無
 * IDLE→ARM(預吹)→CUT(跟火)→BLOW(後吹)→SCAV(排煙)→IDLE */
void sequencer(uint32_t nowMs, uint32_t dtMs) {
  bool enActive = digitalRead(PIN_EN) == LOW;
  bool firing   = (nowMs - fireStampMs) < 50;   // 50ms 內有火
  if (firing) lastFire = nowMs;                 // 後吹倒數從最後一次火算起
  switch (st) {
    case IDLE:
      /* EN 優先;無 EN 訊號的機型由首個 FIRE 直接進 CUT(fallback) */
      if (enActive) { st = ARM; stEnter = nowMs; jobs++; valve(false); pumpRamp(0); }
      else if (firing) { st = CUT; stEnter = nowMs; jobs++; }
      break;
    case ARM:                                  // 預吹:幫浦全速+開閥
      pumpRamp(1000); valve(true);
      /* 預吹滿 0.5s 才承認火——雷射早點幾毫秒會少吹一點,但狀態不被抢跑 */
      if (firing && nowMs - stEnter >= PRE_BLOW_MS) { st = CUT; stEnter = nowMs; }
      else if (nowMs - stEnter > PRE_BLOW_MS + ARM_TIMEOUT_MS) st = IDLE;
      break;
    case CUT:
      pumpRamp(1000); valve(true);
      if (nowMs - lastFire > POST_BLOW_MS) { st = SCAV; stEnter = nowMs; }
      break;
    case SCAV:                                 // 排煙:先全吹再軟停
      if (nowMs - stEnter < SCAVENGE_MS) pumpRamp(1000);
      else if (nowMs - stEnter < SCAVENGE_MS + 1500) pumpRamp(300);
      else { pumpRamp(0); valve(false); st = IDLE; stEnter = nowMs; }
      break;
    case ALARM:                                // 斷氣/塞死:關閥停泵+蜂鳴
      valve(false); pumpRamp(0); digitalWrite(PIN_BUZ, (nowMs / 120) % 2);
      break;
  }
  if (st != IDLE && st != ALARM) { pumpSecs += dtMs / 1000.0f; filterSecs += dtMs / 1000.0f; }
  if (st != ALARM && firing) fireUs += 5;      // 估算點火時數(脈衝×解析度)
}

/* --- 5. 幫浦軟啟動斜坡:1.5s 線性 --- */
void pumpRamp(uint32_t target) {              // target 0..1000
  if (pumpDuty < target) pumpDuty = min(target, pumpDuty + 10);   // 每 15ms 拍
  else if (pumpDuty > target) pumpDuty = max(target, (uint32_t)((int32_t)pumpDuty - 10));
  ledcWrite(PIN_PUMP, pumpDuty);
}
void valve(bool on) { digitalWrite(PIN_VALVE, on ? HIGH : LOW); }

/* --- 6. 斷氣保護:雙向電流判讀 ---
 * 參數:dtMs|回傳:無
 * 氣管脫落→出口失壓→電流掉;濾芯塞死→負載升→電流漲 */
void watchdog(uint32_t nowMs, uint32_t dtMs) {
  bool running = (st == CUT || st == ARM || st == SCAV) && pumpDuty > 800;
  if (!running) { alarmSince = 0; return; }
  bool low  = amps < I_MIN;
  bool high = amps > I_MAX;
  if (high) { if (alarmSince == 0) alarmSince = nowMs;
              else if (nowMs - alarmSince > I_MAX_HOLD) trip(2); }
  else if (low) { if (nowMs - stEnter > 300) trip(1); }   // 升速後 300ms 仍沒電流=沒氣
  else alarmSince = 0;
}
void trip(uint8_t code) {
  alarmCode = code; st = ALARM; stEnter = millis();
  alarmSince = millis();   // ALARM 30 秒後自動復歸 IDLE(人工排除後重試)
}

/* --- 7. NAS 推送:統計+警報 --- */
void pushStats() {
  if (WiFi.status() != WL_CONNECTED) return;
  HTTPClient http; http.begin(NAS_URL);
  http.addHeader("Content-Type", "application/json");
  http.addHeader("X-Device-Token", NAS_TOKEN);
  String body = "{\"jobs\":" + String(jobs) + ",\"pulses\":" + String(firePulses) +
    ",\"fire_s\":" + String(fireUs / 1e6, 1) + ",\"pump_h\":" + String(pumpSecs / 3600, 2) +
    ",\"filter_h\":" + String(filterSecs / 3600, 1) + ",\"st\":" + String((int)st) +
    ",\"amps\":" + String(amps, 2) + ",\"alarm\":" + String(alarmCode) + "}";
  http.POST(body); http.end();
}

/* --- 8. 面板 --- */
void drawPanel() {
  const char* names[] = {"IDLE", "PRE-BLOW", "CUTTING", "BLOW-OFF", "SCAVENGE", "ALARM"};
  oled.clearDisplay(); oled.setTextSize(1); oled.setCursor(0, 0);
  oled.printf("%-9s %4.2fA\n", names[st], amps);
  oled.printf("jobs %lu plse %lu\n", (unsigned long)jobs, (unsigned long)firePulses);
  oled.printf("pump %2.1fh filt %3.0fh\n", pumpSecs / 3600, filterSecs / 3600);
  if (alarmCode) oled.printf("! %s\n", alarmCode == 1 ? "NO AIR!" : "BLOCKED!");
  oled.display();
}

/* --- 9. setup --- */
void setup() {
  Serial.begin(115200);
  pinMode(PIN_FIRE, INPUT_PULLUP); pinMode(PIN_EN, INPUT_PULLUP);
  pinMode(PIN_BTN, INPUT_PULLUP);  pinMode(PIN_BUZ, OUTPUT);
  pinMode(PIN_VALVE, OUTPUT); digitalWrite(PIN_VALVE, LOW);
  ledcAttach(PIN_PUMP, 1000, 10);              // 1kHz/10bit 軟啟動
  prefs.begin("airassist");
  jobs = prefs.getUInt("jobs", 0); filterSecs = prefs.getFloat("filt", 0);
  Wire.begin(8, 9); oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  oled.setTextColor(SSD1306_WHITE);
  for (int i = 0; i < 5; i++) curOffset += analogRead(PIN_CUR);  // 零點校準
  curOffset = curOffset / 5 * 3.3f / 4095.0f / ACS_K;
  attachInterrupt(digitalPinToInterrupt(PIN_FIRE), fireISR, FALLING);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  analogSetPinAttenuation(PIN_CUR, ADC_11db);
}

/* --- 10. loop:15ms 控制拍 --- */
void loop() {
  static uint32_t tCtl = 0, tNet = 0, tOled = 0, btnHold = 0, btnLast = HIGH;
  uint32_t m = millis();
  if (m - tCtl >= 15) {
    tCtl = m;
    firePulses += firePulseCnt; firePulseCnt = 0;
    amps = readAmps() - curOffset;
    sequencer(m, 15);
    watchdog(m, 15);
    if (st == ALARM && m - stEnter > 30000) { st = IDLE; alarmCode = 0; }
  }
  if (m - tOled >= 500) { tOled = m; drawPanel(); }
  if (m - tNet >= 60000) {
    tNet = m; pushStats();
    prefs.putUInt("jobs", jobs); prefs.putFloat("filt", filterSecs);
  }
  /* 按鈕:短按重設濾芯計時;長按 2s 強制通氣(除塵吹灰) */
  uint8_t b = digitalRead(PIN_BTN);
  if (b == LOW && btnLast == HIGH) btnHold = m;
  if (b == HIGH && btnLast == LOW) {
    if (m - btnHold > 2000) { forceBlow(); }
    else { filterSecs = 0; }
  }
  btnLast = b;
}
void forceBlow() {                   // 除塵模式:閥開泵轉 5 秒後自動關
  valve(true); for (uint32_t t = 0; t < 333; t++) { pumpRamp(1000); delay(15); }
  pumpRamp(0); valve(false);
}
⚠️ ADC 腳位必選 GPIO0-4(ADC1):GPIO6/7 是 ADC2,Wi-Fi 啟用時讀值亂跳——本設計 PWM 輸出佔了 GPIO6,電流輸入固定在 GPIO0,就是為了把 ADC2 讓出去避免踩雷。
📦 NAS 端規則(Node-RED):①alarm==1 → Telegram 電話級「雷雕機斷氣!幫浦還在轉但電流只有 0.2A——氣管檢查」;②filter_h>100 → 每日輕提醒「濾芯該拆下來拍一拍了」;③每週彙報:fire_s/pump_h 比值就是「這週氣泵白叫了幾小時」的羞愧指數(本模組通常把它壓到 0.6 以下)。

🏗️ 組裝步驟

📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
  1. 列印後處理:分流塊噴嘴孔用 2mm 鑽頭通一遍、適配環內徑試套雷射頭(緊配但不強壓)
  2. 氣路組裝:泵→軟管→電磁閥→分流塊→三噴嘴環繞光軸;所有接頭上紮帶,運轉後拉拽測試
  3. 光耦接訊號:FIRE/EN 各一顆 PC817,輸入側 1kΩ 串聯並接主板訊號線——先斷機電、用三用表確認接錯不會短路再上電
  4. 功率級焊接:Q1 泵/Q2 閥,閘極 10kΩ 下拉、閥線圈 1N4007;ACS712 串幫浦負端
  5. 零點校準:幫浦未運轉時 Serial 讀 ADC,把漂移量寫進 curOffset(出廠零點有 ±50mV 誤差)
  6. 靜態驗證:手動短接 PC817 輸入側模擬 EN/FIRE 序列,驗證 IDLE→預吹→跟火→後吹→排煙全流程與時序
  7. 斷氣演練:切割模擬中拔開氣管快速接頭,300ms 內蜂鳴+ALARM 顯示——演練通過才準真實雕刻
  8. 隨機固定與試雕:控制盒鎖機殼、軟管隨行鏈留 8cm 應變環;先雕廢料角料確認無焦邊、鏡片 30 分鐘後無霧化

🔧 問題排除

問題原因解決方法
完全無反應(永遠 IDLE)EN 極性接反(该机高有效)看 OLED 的 EN 原始值確認極性,韌體把 == LOW 改 == HIGH;光耦接線不動
誤觸發:按面板鍵氣泵就轉FIRE 線上感應毛刺/共地迴路PC817 輸入側本來就隔離,檢查是不是把 5V 電源線誤接成訊號線
電流讀值亂跳ADC 腳用到 ADC2(GPIO6/7)或 Wi-Fi PA 毛刺電流輸入固定 GPIO0-4;中位數濾波已內建,仍跳則 OUT 對 GND 加 100nF
幫浦 MMM 叫但不轉(啟動時)軟啟動斜坡起點電壓不足以啟動馬達斜坡起點改 350(先給啟動扭矩再爬升),或換 100%→PWM 降頻式軟啟動
斷氣誤報(SCAV 段常響)I_MIN 高於幫浦空載電流Serial 記錄實測空載 A 值(本泵 0.6A),門檻設空載的 60%
切邊仍然发黄噴嘴角度太淺,氣流從切口上方掠過噴嘴下傾改 25°、距切面 4-6mm;流量不是萬能,角度才是
鏡片還是霧了預吹不足:EN 訊號機型沒有(只有 FIRE)無 EN 機接法:ARM 段由首個 FIRE 觸發、預吹改「後吹加倍+定期鏡片檢查」;或從電源板取激光致能點

🎯 進階延伸

📦 檔案清單

laser-airassist/
├── src/
│   └── laser-airassist.ino     # ESP32 韌體(本頁完整程式)
├── nas/
│   └── node-red-flow.json      # 警報/統計/濾芯週期
├── stl/
│   ├── manifold.stl            # 一進三出分流塊
│   ├── nozzle-ring-12.stl      # 噴嘴適配環 12mm
│   ├── nozzle-ring-14.stl      # 噴嘴適配環 14mm
│   ├── pump-mount.stl          # 減震泵架
│   ├── duct-clip.stl           # 隨行軟管夾
│   └── box-control.stl         # 控制盒
├── f3d/
│   └── airassist-design.f3d    # Fusion 360 原始檔
├── test/
│   └── seq-simulator.ino       # 短接模擬 EN/FIRE 的台架測試
└── manual/
    └── assembly.html           # 圖文組裝說明書
← 上一篇:ESP32-C6 伺服器機櫃智能散熱除塵風扇控制器 📚 系列索引 下一篇:ESP32-S3 白噪音睡眠聲機 →