The acceptance catalog gains the test behind forgectrl 5a31d66: an exhaust floor at the legal maximum must trip AIRFLOW after the grace and three ticks (hold, fire blocked, no resume, the exhaust gate TRIPPED and named in the reason); a purge current floor at the ADC rail must trip the same way; an exhaust floor of zero must read off in gates_off and trip nothing; restored, the next session must run OK with every fan at or above its floor. Three host cases against a scripted engine that models the grace and the readings. The covers map names src/airflow.*. COOLING.md gains 3a (the airflow gates) and the five settings with their ranges and bands; SERVICES.md the gate rule, the rows, the AIRFLOW verdict and fan_gates; SAFETY.md names the floors among the verdict's inputs; BRINGUP item 19 records the gates as in with provisional floors and the catalog at 44. Pins: forgectrl 5a31d66, forgefirm-app 81027ff (0.1.15+git); fetch-verified.
20 KiB
Laser safety: how the machine is kept from firing
This document describes the laser-safing design of a stock Glowforge running ForgeFIRM: the discrete hardware chain on the factory control board, what the i.MX6 can see and drive, and the software layers ForgeFIRM stacks on top. The hardware chain is the safety boundary; software only ever adds gates in front of it and never bypasses it.
Signal names follow the factory board's nets. GPIOx_yy is the i.MX6 GPIO;
the Linux name in parentheses is how ForgeFIRM exposes it (device-tree
gpio-keys switch, or glowforge.ko /sys/glowforge/cnc attribute).
1. Principle
lid closed (both switches) ─┐
SoC alive (charge pump) ──┴─▶ HV_ENABLE ─────────────────▶ PSU: HV supply may run
│
FIRE (per-tick stream bit) ─┐ ▼
button latch cleared ──┼─▶ LASER_ON ──▶ PSU: tube fires only when BOTH are true
interlock latch cleared ──┘
Two independent hardware outputs go to the laser power supply on J1:
- HV_ENABLE (J1_16) — high only while the lid is closed and the SoC is actively retriggering a hardware one-shot ("charge pump"). A hung SoC, a stuck GPIO or an open lid drops it in hardware.
- LASER_ON (J1_12) — the SoC's per-tick FIRE request, AND-gated behind two hardware latches: the button latch (lid state + SoC lock, cleared only by a physical button press) and the interlock latch (set by the SoC, cleared only while the remote-interlock loop is closed — see §5 for what that means in ForgeFIRM today).
The SoC cannot fire the tube by driving one pin. It has to keep the one-shot alive, release its own lock, wait for a human to press the button while the lid is closed, and then stream FIRE bits — and any of those conditions going away kills emission in hardware, not in software.
2. The hardware chain
2.1 Parts on the control board
| Ref | Part | Role |
|---|---|---|
| U1 | SN74AHC123A dual retriggerable monostable, R ≈ 499 kΩ / C ≈ 1 µF (t_w = 454 ± 3 ms, measured pulse-to-drop) | Charge-pump watchdog: Q stays high only while CHG_PUMP keeps arriving; times out 0.45 s after the last pulse |
| U5, U6 | SN74AHC14 hex Schmitt-trigger inverters | Level inversion / conditioning for every switch line and SoC readback |
| U17 | SN74AHC08 quad 2-input AND | The four gates: DOORS, HV_ENABLE, and the two-stage LASER_ON gate |
| U23 | CD4043B quad R/S latch (NOR type, active-high S/R, output enable tied high) | Latch 1 = button latch, latch 2 = interlock latch |
| U32 | 74AHC1G32 single 2-input OR | Lid-open OR SoC lock → button latch SET |
| U24 | 74AHC1G04 single inverter | HV_ENABLE readback to the SoC (the pin carries ¬HV_ENABLE; the factory design labels this net E-STOP) |
| U18 | i.MX6 Solo | The SoC: drives CHG_PUMP, LATCH_RESET, INTERLOCK_RESET, FIRE; reads everything else |
2.2 Inputs
| Net | Source | Conditioning | SoC pin | Linux exposure | Meaning |
|---|---|---|---|---|---|
| DOOR_SW1 (L) | J4_13, lid switch pulled to 3.3 V when closed | U5-1 inverts | GPIO4_14 (ball T6) | gpio-keys code 0 door1, active low → active = closed |
Left lid switch |
| DOOR_SW2 (R) | J4_12 | U5-2 inverts | GPIO1_06 (T3) | code 1 door2, active low → active = closed |
Right lid switch |
| DOORS | U17-1 = DOOR_SW1 · DOOR_SW2 | U5-3 inverts | GPIO1_00 (T5) | code 3 doors, active low → active = both closed |
The lid term the chain actually uses |
| BUTTON | J5_5, 12 V through the button, 27 kΩ / 8.7 kΩ divider (≈ 2.9 V when pressed) | U5-5 inverts | GPIO4_09 (U6) | code 2 button, active low → active = pressed |
Big front button. Also the RESET input of the button latch |
| INTERLOCK_SW | J8, 12 V through the remote-interlock loop, 432 Ω / 165 Ω divider (≈ 3.3 V when the loop is closed); factory-jumpered on Basic/Plus, brought out on Pro | U6-2 inverts | GPIO1_09 (T2) | code 5 interlock, active high → active = loop OPEN |
Also the RESET input of the interlock latch |
| CHG_PUMP watchdog Q | U1-1 Q (pin 13): /A = GND, /CLR = 3.3 V, B = CHG_PUMP; each rising edge retriggers | U6-6 inverts | GPIO1_08 (R5) | cnc/charge_pump_alive (logical), interlock_circuit bit 5 (raw, 0 = alive) |
watchdog alive |
| Button latch state | U23-1 Q → U5-6 → U6-1 | double inversion | GPIO1_03 (R7) | cnc/button_latch, interlock_circuit bit 2 |
1 = latch SET (fire blocked / not armed), 0 = armed |
| Interlock latch state | U23-2 Q → U6-3 → U6-4 | double inversion | GPIO1_02 (T1) | code 6 interlock_latch, active high → active = latch SET |
1 = interlock latch blocking |
| LASER_ON readback | J1_12 net (U17-3 output) | U6-5 inverts | GPIO1_05 (R4) | cnc/laser_on, laser_on_sampled, interlock_circuit bit 0 (raw, active low) |
The gated output — the only software-visible proof of emission permission |
| HV_ENABLE readback (factory net name E-STOP) | U24 = ¬HV_ENABLE | — | GPIO4_06 (W5) | code 4 hv_enable, active low → active = HV_ENABLE asserted |
Readback of the chain's own output, not an input: inactive at idle, active only while a run feeds the watchdog with the lid closed |
| LASER_PGOOD | J1_14 (the supply's HV_OK line) | — | GPIO4_21 (P24) | cnc/laser_pgood, laser_pgood_sampled (active low) |
Read as "power good" from the laser supply; what the supply actually signals on it is not fully characterized |
2.3 SoC outputs into the chain
| Net | SoC pin | Driven by | Effect |
|---|---|---|---|
| CHG_PUMP | GPIO3_24 (F22, charge-pump-gpio) |
glowforge.ko: one 0→1→0 pulse at run start, then every 200 ms from a soft hrtimer only while state == running; forced low on stop, disable, unload and kernel panic |
Retriggers U1-1 (t_w = 454 ms, so a 200 ms feed holds Q solidly high and one missed pulse is tolerated). No edges → Q falls 0.45 s after the last pulse → HV_ENABLE drops with it |
| LATCH_RESET | GPIO1_07 (R3, latch-reset-gpio, init HIGH) |
cnc/laser_latch (1 = lock). Also drives the FIRE line to high impedance while locked |
Into U32 with lid-open; SETs the button latch → LASER_ON blocked until the next button press |
| INTERLOCK_RESET | GPIO4_05 (P5, interlock-latch-reset-gpio, init HIGH) |
glowforge.ko: high whenever the remote-interlock loop reads open, or until a switch device reporting the loop has attached; low only while an attached device reports it closed (in-kernel input handler on the gpio-keys switch, EV_SW code 5). Read back as interlock_latch_reset / interlock_circuit bit 4 |
SET input of the interlock latch → LASER_ON blocked in hardware while the loop is open |
| FIRE (LASER_ENABLE) | GPIO2_30 (E22, laser-enable-gpio) |
The SDMA script, from bit 4 of each pulse byte; Hi-Z whenever the latch is locked or no run is in flight | One input of the final LASER_ON AND gate |
Laser power (PWM2 on J1_13) is not part of the chain: it sets the tube current setpoint and is not gated. Emission permission is FIRE ∧ chain; the laser-off guarantee rests on FIRE, and the kernel drops FIRE within one tick on end-of-data or underrun.
2.4 Logic
DOORS_OK = DOOR_SW1 · DOOR_SW2 (U17-1)
WDOG_ALIVE = U1-1 Q, retriggered by every CHG_PUMP rising edge
HV_ENABLE = DOORS_OK · WDOG_ALIVE (U17-4) → J1_16
¬HV_ENABLE (U24 inverter) → GPIO4_06, read back as `hv_enable`
Button latch (U23-1):
SET = ¬DOORS_OK + LATCH_RESET (U32 OR)
RESET = BUTTON pressed
Q1 = 1 → fire blocked; 0 → armed
Interlock latch (U23-2):
SET = INTERLOCK_RESET (SoC: high while the loop reads open or is unobservable)
RESET = interlock loop closed
Q2 = 1 → fire blocked
LASER_ON = FIRE · ¬Q1 · ¬Q2 (U17-2, U17-3) → J1_12
The CD4043B is set-dominant: while SET is high the latch cannot be cleared. That ordering is what makes the button meaningful — a press only arms the machine when the lid is closed and the SoC has already released its lock.
2.5 What each condition does, in hardware alone
| Event | HV_ENABLE | LASER_ON | Recovery |
|---|---|---|---|
| Lid opens (either switch) | drops (DOORS_OK low) | drops immediately: ¬DOORS_OK SETs the button latch | close the lid, SoC lock released, press the button |
SoC asserts LATCH_RESET (kernel laser_latch=1) |
unchanged | blocked: button latch SET; FIRE line is also Hi-Z | laser_latch=0, then a button press |
| SoC stops toggling CHG_PUMP (hang, panic, stop, fault, underrun) | drops within one one-shot period | FIRE is parked by the same paths | next run restarts the feed |
| Button pressed with lid closed and lock released | — | armed (Q1 cleared) | — |
| Button pressed while lid open or lock held | — | stays blocked (SET is dominant) | — |
| Remote-interlock loop opens (Pro) | unchanged | blocked: the kernel drives INTERLOCK_RESET high on the switch edge, setting the interlock latch. Opening the loop by itself only releases the latch's RESET — the board has no direct trip path — so this SoC drive is what makes the interlock a hardware cut (see §3.1); software additionally cancels (or, with lid_policy = hold, parks) the job on interlock |
close the loop: the kernel releases INTERLOCK_RESET and the closed loop resets the latch |
| Interlock latch already SET | unchanged | blocked | closing the loop clears it |
hv_enable (GPIO4_06) is a readback of this chain's own output, not an
input: it is inactive on an idle machine and active for the duration of any
kernel run — the window in which the charge pump is fed and HV_ENABLE is
alive. Nothing in ForgeFIRM gates on it; it is telemetry. (The factory design
labels the net E-STOP; no Glowforge model has an e-stop input, and a
retrofitted one belongs in the lid-switch chain, where the hardware enforces
it.)
3. Software layers on top
Every layer below sits in front of the chain: it can only withhold FIRE, hold the lock, or starve the charge pump. None can produce emission the hardware would not allow.
3.1 glowforge.ko (kernel)
- Laser latch (
cnc/laser_latch, write-only): 1 = lock. Locking drives LATCH_RESET high (button latch SETs) and puts the FIRE line in high impedance so the SDMA stream physically cannot raise it. Locked by default; every close of/dev/glowforgerelocks. Unlocking never restores the FIRE drive while a run or ramp is in flight — only run start and the resume waypoint do, and only if the latch is unlocked at that moment. - Charge pump only while running. The 200 ms retrigger starts with the
run and the callback returns without rearming as soon as the state leaves
running(stop, halt, fault, underrun). Stop/disable/unload pin sets force CHG_PUMP low. A paused job is one of those states, so the chain de-energizes itself behind a pause without anyone asking it to: measured at the pads, motion stops 317 ms after the pause command and HV_ENABLE drops with the watchdog 550 ms after it (the feed ends with the run, then t_w expires) — a pause shorter than about half a second never drops HV at all. On the resume the pump primes with the run and HV_ENABLE is back within ~3 ms, while motion only restarts at ~219 ms: the chain re-arms about 216 ms before the first step, so a resumed cut is never waiting on it. - FIRE backstop. At end-of-data and on underrun the SDMA script drops FIRE and the step lines within one tick; the FIRE line is parked Hi-Z at every run end and only a latch unlock plus a new run restores it.
- Interlock latch drive. The board's interlock latch is reset by a closed
loop but can only be set by the SoC's INTERLOCK_RESET line; an open loop
alone does not trip it. The driver owns that line through an in-kernel
input handler on the gpio-keys switch device: it is high (latch set,
LASER_ON blocked) from probe until the switch device attaches, whenever the
loop reads open, and again if the switch device goes away — an
unobservable loop counts as open. Only an attached device reporting the
loop closed releases it, and the set-dominant latch then clears through
its own RESET. The policy is host-tested (
tests/interlock_test.c). - Dead man's switch. A feeder holds
/dev/glowforgeopen withflock LOCK_EX; if that fd closes while a program runs, the driver performs an emergency stop, puts the head in its safe state and de-energizes the thermal-loop heat sources. - Panic handler. On a kernel panic the driver stops the EPIT and drives the pins safe directly: FIRE Hi-Z, CHG_PUMP low, LATCH_RESET asserted, steppers de-energized — because SDMA and EPIT would otherwise keep playing the ring with no kernel alive.
- Readbacks (
interlock_circuitbits 0–5,laser_on[_sampled],laser_pgood[_sampled],button_latch,charge_pump_alive,interlock_latch_reset) are monitoring only; the driver enforces nothing from them. Bits 1, 3 and 4 are driven outputs read back from the data register — bit 3 says what the driver commanded,laser_onsays what the chain did.
3.2 grblHAL controller (grblHAL-glowforge)
- Operator-armed window. The first laser-on of a job runs the arm flow on
the protocol thread: coolant fire verdict must be OK, a head must be present
(lens, air assist and beam detector live on it and the chain has no head
term), fans go to the run profile, the latch is unlocked, and the controller
then waits for the physical button (
laser_button_timeout_s, default 300 s; a timeout or soft reset relocks and aborts). The hardware button latch is what the press clears — the software wait exists so the job does not start streaming FIRE bits into a blocked gate. - Disarm. After
laser_disarm_s(default 60 s) of no laser use, or on program end/abort, the controller relocks the latch, turns the button LED off and stands the cooling profile down. A job paused on the button is no laser use: the grace counts down through the hold and closes the window under a job left standing, so a long pause ends with the machine disarmed and the next emission needs a fresh press. The relock waits for the kernel to finish the queue tail so a controlled stop can never leave FIRE driven. - Coolant fire gates. The armed window requires a fresh
fire_okverdict from the cooling engine (flow verification, over-temperature, the airflow floors on every fan, lid-IR emission witness); a stale or failed verdict relocks in-process. The thermal gates are settings with a wide range whose far end turns the gate off by value (COOLING.md§8a), loudly; the fresh-report rule, the emission witness, the dead-man and the latch are not settings and stay in force whatever the gates are set to. - Safety door.
doors(lid) andinterlock(loop open) are the core's safety-door signal, shown to the core only while it is in a job-time state (cycle, hold, tool change, door): a running job parks with a planned deceleration and — withlid_policy = cancel, the default and the factory firmware's behavior — is then cancelled: the armed window closes, a soft reset ends the sender's stream (from a fully parked state, so the position is kept and no alarm is raised), and the head returns to where the job started with the latch locked, lid open or not. The next job re-arms with a fresh button press, which is also what clears the hardware button latch the lid set — the software armed window and the hardware latch cannot disagree.lid_policy = holdkeeps the stock door hold (once the door/loop closes the controller reportsDoor:0and a cycle start resumes it). During the arm wait either opening cancels the job outright under both policies. While idle, jogging or homing — and during the return-to-start motion after a cancel — the signal is hidden: the lid is opened at idle every time material is loaded and a door seen there would strand the controller in Door; it is delivered the moment the core leaves those states, so a job started with the lid open parks (and cancels) on its first poll. This is a motion/UX gate; the lid is also cut in hardware by the button latch, and the interlock by the interlock latch (§3.1). - Button. Outside the arm wait the button is the job pause/resume toggle in both controller modes (feed hold / cycle start in GRBL mode; the factory's stop-backtrack-hold and lead-in resume in cloud mode); a held button has no further meaning during a job. A pause is deliberately not a cancel: the latch stays unlocked and the armed window open, which is what lets the next press resume the job. Emission still ends with the pause — the stream stops driving FIRE and the chain drops HV_ENABLE by itself (§3.1) — and the window closes on its own if the pause outlives the disarm grace. A lid or interlock open while paused takes the cancel path, so nothing resumes past an enclosure opening.
- Head/motion witnesses. Position counters are not proof of motion (the
step-stream drives are open loop); the head accelerometer is the motion
witness, and
beam_detect_analogon the head is the live emission witness.
3.3 forgectrl (machine services)
- Holds
/dev/glowforgefor its lifetime (pulse-device broker) so controller handovers never close the device, and relocks the latch (cnc/stop+cnc/laser_latch=1) on every transition out of a running child — unexpected death, mode switch, restart. - The cooling engine is the sole owner of the thermal hardware and
publishes the fire verdict the controllers enforce; on a FIRE-class verdict
it writes
cnc/stop+cnc/laser_latch=1itself. - The motion-liveness gate refuses to hand a controller a machine whose drivers may have wedged (counters running, motors dead) — a laser-safety corollary of "counters are not motion".
/statusreports the switch map andlaser_locked(interlock_circuitbit 3) for the panel and telemetry; nothing in forgectrl reads the Grbl socket for machine state.
3.4 Cloud mode
The factory-experience client runs behind the same kernel latch, charge-pump, backstop and dead-man rules; the precomputed pulse file it loads is subject to the same FIRE gating as the live stream.
4. What is proven, and how
Verified on the bench with a probe on the PSU-connector LASER_ON pin and the
kernel readbacks (CAMPAIGN-LOG.md holds the drill records):
- Latch locked: 40,000 streamed FIRE bits → PSU pin flat,
laser_enable0 — the lock severs the FIRE drive entirely. - Latch unlocked, chain unarmed (no button press):
laser_enable1 mid-window, PSU pin flat,laser_on0 — the AND gate holds. - FIRE drop at end-of-data and at true underrun: ≤ 1 tick, both termination paths.
- A latch unlock inside an acceleration ramp does not restore the FIRE drive for the in-flight run; a locked latch survives a stop + resume replay.
- Armed kill mid-FIRE: emission tail equals the ring in-flight only (15–171 ms), the latch relocks, the burn line ends abruptly.
- Switch bits 0–3, 5, 6 verified against physical state; bit 4
(
hv_enable) characterized live: inactive at idle, active through any run, and it flips together withcharge_pump_aliveon both edges (HV_ENABLE = DOORS_OK · WDOG_ALIVE observed). - Interlock latch drive: with the connector unjumpered,
interlock,interlock_latch_resetandinterlock_latchall assert within one 50 ms sample and all clear when the loop is closed again. - Watchdog period, measured directly from the SoC pins
(
scripts/bench/cp_watchdog_timing.py: every CHG_PUMP pulse latched by the GPIO edge detector, the ¬Q and ¬HV_ENABLE pads polled at ≈0.2 ms): Q falls 451.8 / 455.6 ms after the last pulse (t_w = 454 ± 3 ms, matching R·C); Q rises on the priming pulse and HV_ENABLE falls with Q within one sample; the kernel feed period is 199.98 ms (199.87–200.07). A feed late by more than ≈254 ms therefore drops HV_ENABLE.
5. Not yet established
Present gaps in the hardware picture. None of them changes the safety argument (every gap is on the readback/sense side or is a "which part" question), but each is worth closing:
laser_pgood(HV_OK, J1_14) semantics are not fully characterized.