Add laser-safety and regulatory documentation; scrub bench identity

- LIGHTBURN.md: mandatory "Before you cut" safety section; the
  walkthrough now reflects the firing machine (dry runs need the
  layer output off or M5; live first-cut instructions); the homing
  entry documents homing_mode and the gfcloud method; the machine
  address is a placeholder.
- README.md: condensed safety section linking the full text and the
  regulatory notes.
- INSTALL.md: "Regulatory and legal" section ahead of the install
  steps; routine updates route through the panel updater rather than
  the installer.
- BRINGUP.md: the release signing key is described as held offline
  (no on-disk path); bench address and credential notes removed;
  Next-work item 7 corrected (the installer embeds the production
  release key); status entry for audit remediation Phases 0-1; the
  GATE A kernel drills join the pending image-flash checklist.
- bench scripts: the target host comes from GF_HOST (or argv) instead
  of a hardcoded address.
- laser_stream_test.py: per-session controller runs with a hermetic
  cooling-verdict publisher; new assertions that every stream
  terminates with FIRE clear (including M3 held to stream end) and
  that no FIRE bit rides a zero-step gap; a cycle-churn session
  exercises the stop/start seams.

Audit findings D-1, D-2, D-3, D-5, D-10, D-12, B-10, and the harness
half of D-4/G-1.
This commit is contained in:
ScottW514
2026-08-14 15:38:24 -04:00
parent 5dddea12ee
commit cc927aca5f
13 changed files with 361 additions and 63 deletions
+60 -13
View File
@@ -1,7 +1,42 @@
# ForgeFIRM bring-up status & cold-start runbook
Last updated: **2026-08-13** — **shared machine services complete and
closed out.** forgectrl is the one machine-services daemon behind both
Last updated: **2026-08-14** — **audit remediation Phases 0 + 1 landed**
(from an independent whole-tree audit dated 2026-08-13; the remediation
is sequenced behind two gates — GATE A, uncommanded energy, before any
further live-fire; GATE B, control surface + release, before any
published release). Phase 0: user-facing laser-safety and
regulatory text is in place (LIGHTBURN.md "Before you cut", README,
INSTALL.md "Regulatory and legal" + updater-first update path, a
persistent panel safety banner), the walkthrough no longer claims the
laser cannot fire, bench-machine identity and the signing-key location
are scrubbed from tracked files (bench scripts take `GF_HOST`), and
every repo has a commit-msg hook enforcing commit attribution. Phase 1
(GATE A, uncommanded energy) is **code-complete and host-verified**:
the stream engine records the cycle-end laser-off so idle-gap pads ship
dark and every stream terminates FIRE-clear (G-1), latch writes are
serialized against the shipper's relight (G-5) with the arm-state and
verdict caches made properly atomic (G-19/G-20/G-21), the cooling
report path moved to a bounded-connect reporter thread off the protocol
thread (A-3/G-7), and gf.lock is priority-inheriting with PIC-SPI and
rail-settle work moved outside it (G-8). Kernel fixes K-1 (saturating
decel ramp + EPIT divisor clamp), K-2 (resume-waypoint latch guard) and
K-3 (latch writes under status_lock; FIRE drive never restored mid-run
or mid-ramp) are code-complete and **ride the pending full-image
flash** with the platform-hygiene batch. `scripts/bench/
laser_stream_test.py` now asserts the termination and zero-step-gap
rules across M4, M3-to-stream-end, and cycle-churn sessions (with a
hermetic cooling-verdict publisher): all PASS on the fixed controller
(the M4 session reproduces the recorded baseline byte-for-byte:
28 354 fire ticks, X peak 533 net 0, 534 dark return steps), and a
build with only the G-1 hunks reverted FAILS on the M3 termination
rule — the harness catches the defect class. **GATE A stays open — no
live-fire — until the flashed image passes the bench drills**
(controlled stop decelerates at the default cloud tick, resume with the
latch locked stays laser-less, mid-ramp latch writes do not re-arm
FIRE) and the harness is wired into CI.
Previously — **shared machine services complete and
closed out (2026-08-13).** forgectrl is the one machine-services daemon behind both
controller modes: the cooling engine (single owner of the thermal
hardware), controller-mode supervision, the pulse-device broker, and
the motion-liveness gate. Both controllers are cooling-engine clients
@@ -121,9 +156,8 @@ item 8.
## The bench
- **Board**: SSH `root@172.16.1.97` (fixed DHCP lease since 2026-08-02;
was .130), empty password
(`ssh -o PreferredAuthentications=none` logs straight in). The bench
- **Board**: SSH `root@<machine-ip>` (dev images permit passwordless
root login). The bench
machine is a **Basic/Plus** (the control board is common to
Basic/Plus/Pro). Dev image
(`forgefirm-image-dev`) on SD; BusyBox userland + python3 + gdb/strace.
@@ -167,11 +201,9 @@ item 8.
proven both ways (modern-packed signed archives apply with 0.14.2;
modern fwup verifies+applies the factory .fw — signer key
2017-05-001.pub). The production signing-key ceremony
(UPDATE-SYSTEM.md gate 8) was executed 2026-08-08.
**The production release key lives at
`~/forgefirm-release-key/fwup-key.priv`** (0600; `fwup-key.pub` +
`fwup-key-raw.pub` beside it; offline backups held by the operator) —
the installer embeds its pubkey, so releases sign with THIS key only.
(UPDATE-SYSTEM.md gate 8) was executed 2026-08-08. **The production
release key is held offline by the operator** — the installer embeds
its public key, so releases sign with that key only.
Pack releases with `scripts/mkfw.sh`; the full pipeline is
`scripts/release.sh`, invoked on this host as:
`FWUP=~/fwup-lab/bin/fwup-v1.16.0 FWUP_COMPAT=~/fwup-lab/bin/fwup-0.14.2
@@ -235,7 +267,7 @@ hardware I/O — host testing).
since the last run, `$RST=$` once (stored settings win). Each motion
run logs a producer-stats line to stderr (callbacks, µs/call,
max-behind, clamped) — clamped should stay 0.
4. Connect LightBurn/UGS to `172.16.1.97:23`, or jog raw:
4. Connect LightBurn/UGS to `<machine-ip>:23`, or jog raw:
`$J=G91X40F1200`. `^X` mid-motion aborts via kernel `cnc/stop`
(controlled decel) and raises an alarm; TCP disconnects never kill the
process (the deadman fd stays held).
@@ -1412,8 +1444,8 @@ accordingly ("Automatic — AP country, else World").
needs `-f` from factory). The **bench board now runs ForgeFIRM
v0.1.0 from eMMC slot 2** (factory 2024 in slot 1, archives in
/data/forgefirm/archive, dev image still on SD via `ffboot -s`).
Remaining Phase 2 nicety: the installer's embedded pubkey is the
DEV key until the production ceremony.
The installer's embedded pubkey is the **production release key**
(ceremony executed 2026-08-08; `release.sh` enforces the match).
**Post-test: the bench rests on the SD dev image again** (`ffboot
-s`; slot 1 = factory 2024, slot 2 = ForgeFIRM v0.1.0, archives in
/data/forgefirm/archive). Platform fact pinned by experiment while
@@ -1513,3 +1545,18 @@ accordingly ("Automatic — AP country, else World").
- The uniprocessor locking assumption and the panic/dead-man safe
states are documented in `kernel-module-glowforge/UAPI.md`; no
bench item.
- **GATE A kernel fixes added to the same flash (2026-08-14):**
the controlled-deceleration ramp now floors at the minimum step
frequency with a saturating decrement, and `epit_hz_to_divisor()`
can no longer return the degenerate divisor 0 (a 0 Hz request maps
to the slowest achievable tick); the resume waypoint re-enables
the FIRE drive only when the laser latch is unlocked; and
`laser_latch` writes run under `status_lock`, restoring the FIRE
output drive only when no run or ramp is in flight.
**Bench (GATE A stays open — no live-fire — until these pass):**
a controlled-stop drill at the default cloud tick (10 kHz, ramp
125000) shows a decelerating tail rather than a max-rate burst;
feed-hold, jog-cancel and `^X` each land in a controlled stop with
position preserved; a resume waypoint with the latch locked stays
laser-less; `laser_latch=0` written mid-ramp does not re-arm FIRE
(probe the PSU-connector LASER_ON line as in `fire_test.py`).
+66 -11
View File
@@ -1,5 +1,28 @@
# LightBurn setup & operation (ForgeFIRM)
## Before you cut — safety (read this first)
ForgeFIRM replaces the factory software, **not** the factory safety rules. The
machine contains a Class 4 CO₂ laser emitting invisible 10.6 µm infrared at
roughly 45 W. **Jobs sent from LightBurn fire the laser.**
- **Eyes.** The enclosure and lid glass are the eye-safety barrier. Never
defeat the lid switches or the Pro's remote-interlock plug, and never
operate with any cover removed. Direct or reflected 10.6 µm radiation
blinds and burns.
- **Fumes.** Vent the exhaust to the outdoors, always. Laser-cutting fumes
are toxic and flammable.
- **Materials.** Never cut PVC, vinyl, or any chlorinated plastic — they
release chlorine gas that corrodes the machine and injures your lungs.
Know what your material is before you cut it.
- **Fire.** Never leave a running job unattended. Small flare-ups are normal
with some materials; sustained flame is not. Keep a fire extinguisher
(CO₂ preferred) within reach and know how you will open the lid and
smother a fire before you start.
- **Stop means stop.** The big button, LightBurn's Stop, and opening the
lid each halt the job. If anything looks wrong, stop first and diagnose
second.
The laser fires only inside an operator-armed window:
- **Starting a job that fires: press the button.** At the first
@@ -26,21 +49,30 @@ The laser fires only inside an operator-armed window:
## One-time device setup
Prerequisite: the controller is running on the board (see BRINGUP.md;
`grblHAL_glowforge` on TCP port 23 at 172.16.1.97).
`grblHAL_glowforge` on TCP port 23 at your machine's IP address, shown
below as `<machine-ip>`).
1. **Laser window → Devices → Create Manually** (skip auto-find; it
scans serial ports).
2. Device type: **grblHAL** if your LightBurn version lists it,
otherwise **GRBL** — both speak the right protocol.
3. Connection: **Ethernet/TCP**. IP address: **172.16.1.97** (LightBurn
uses TCP port 23 for GRBL devices, which is exactly where the
controller listens).
3. Connection: **Ethernet/TCP**. IP address: **`<machine-ip>`**
(LightBurn uses TCP port 23 for GRBL devices, which is exactly where
the controller listens).
4. Name: e.g. `Glowforge ForgeFIRM`. Work area: **X 495 mm, Y 279 mm**.
5. **Origin**: pick the corner where the head sits after parking at
home — **rear-left as you face the machine** (the top-left dot in
the selector). This is what keeps jobs un-mirrored: machine +X runs
right, +Y runs from the rear rail toward you.
6. **Auto-home on startup: NO.** Homing is not wired yet; `$H` errors.
6. **Auto-home on startup: NO** — LightBurn would issue `$H` at every
connect, and the working homing method runs a multi-minute session.
`$H` itself works and is selected by the `homing_mode` setting in the
machine's web control panel: `gfcloud` (Glowforge web-service vision
homing — the working method; X/Y home to the factory corner, Z to the
hall sensor; requires a signed-in Glowforge session), `switches`
(physical limit switches, once installed), or `none` (`$H` is
rejected). Run `$H` deliberately from the Console tab when you want a
true machine origin.
7. Finish. If a stale device profile already exists, edit its IP
instead of creating a new one.
8. Device Settings (wrench icon): **S-Value Max = 1000** (matches $30).
@@ -54,12 +86,12 @@ In the Laser window set **Start From: Current Position**, and set the
**Job Origin** dot to the same corner as the machine origin (top-left
dot). The job then runs into the bed from wherever the head currently
sits — absolute machine zero never matters, which is the forgiving mode
while the machine has no homing switches.
when you have not homed.
(`Absolute Coords` also works, but only if the head was parked at the
home corner when the controller started; after any Stop/alarm the
absolute frame is stale until the controller is restarted with the head
re-parked.)
(`Absolute Coords` also works after a successful `$H`, or if the head
was parked at the home corner when the controller started. After any
Stop/alarm the absolute frame is stale until you re-home with `$H` or
restart the controller with the head re-parked.)
## Operating basics
@@ -90,15 +122,38 @@ full, exhaust and intake fans to factory run speeds, then a ~15 s
cooldown after the layer before returning to idle. Leave it ON for
anything that will eventually involve the beam; expect real fan noise.
## Dry runs (motion only, no fire)
**Setting a low power value does NOT make a job inert — any laser layer
prompts for the arm button and then fires.** The motion-only modes are:
- **Frame** and jogging — never fire.
- A job whose layers emit no laser-on command: turn the layer's
**Output** off in the cut settings, or send gcode that stays in `M5`.
- A job run with the laser latch left locked (never press the arm
button): the job pauses at the white-button prompt and aborts after
`laser_button_timeout_s` — useful only to confirm the prompt itself.
If the white arm prompt appears and you did not intend to fire, press
**Stop** in LightBurn.
## A good first job
First a dry run, then a light cut on scrap:
1. Draw a rectangle (~100 × 60 mm) with a circle inside.
2. Double-click the layer color bar (bottom): mode **Line**, speed
**50 mm/s** (= 3000 mm/min; check Edit → Settings for your speed
units), power anything (ignored — nothing fires).
units). For the dry run turn the layer's **Output** off.
3. Park the head where the job's rear-left corner should be (or leave
it at home), **Frame**, watch the perimeter trace, then **Start**.
Expected behavior: darting travels at up to 200 mm/s, smooth 50 mm/s
tracing of the shapes, silky and near-silent motion (factory currents +
decay mode), and the head finishing per the job's return setting.
4. For the live pass: put scrap material on the bed (never an empty
honeycomb over the fan grill), re-enable the layer's **Output**, set
power to **30 %** or more (below ~30 % the tube barely marks), turn
the layer's **Air Assist** on, close the lid, **Frame**, **Start**,
and press the white button when it lights. Watch the whole job.