docs: the moved documents live on the documentation site

INSTALL.md, SERIAL.md, docs/COOLING.md, docs/LIGHTBURN.md,
docs/MOTION.md, docs/SAFETY.md, docs/UPDATE-SYSTEM.md, docs/VIDEO.md and
their images are pages on https://docs.forgefirm.org/ now. Every
reference in the README, BRINGUP, the kas config, the cold-build
workflow, forgetest, and the bench scripts points to the site page. The
README carries the beta banner. docs/ keeps BRINGUP.md and
CAMPAIGN-LOG.md.

No catalog consequence: the deleted files are documents, and the code
changes are comment and help-text repoints only.
This commit is contained in:
ScottW514
2026-09-01 15:33:17 -04:00
parent 8a93202c00
commit 6a48cd3969
23 changed files with 48 additions and 2637 deletions
+2 -1
View File
@@ -1,7 +1,8 @@
# Cold-build reproducibility probe: proves a fresh clone still builds the # Cold-build reproducibility probe: proves a fresh clone still builds the
# release image, and publishes the artifact checksums for comparison # release image, and publishes the artifact checksums for comparison
# against locally built releases. Dispatch-only - releases are built and # against locally built releases. Dispatch-only - releases are built and
# signed on the maintainer's build host (see docs/UPDATE-SYSTEM.md); this # signed on the maintainer's build host (see
# https://docs.forgefirm.org/technical/forgefirm/install-and-update/); this
# workflow never produces release artifacts. # workflow never produces release artifacts.
# #
# A cold Yocto build on a 4-core hosted runner takes hours and lives # A cold Yocto build on a 4-core hosted runner takes hours and lives
-144
View File
@@ -1,144 +0,0 @@
# Installing OpenGlow/ForgeFIRM
> # ⚠️ IN DEVELOPMENT — NOT YET RELEASED
>
> **ForgeFIRM has no public release yet.** No images are published, nothing here
> is installable, and there is no supported way to put this on a machine. The
> documentation describes the firmware as it is being built and validated on the
> bench — it is here to be read, not followed.
>
> If you come across a file claiming to be a ForgeFIRM image, it did not come
> from this project.
> # What this costs
>
> Nothing. Not yesterday, not today, and if anyone ever charges you for
> it, it wasn't this project. ForgeFIRM is free in both senses: free as
> in beer, free as in speech. All of it is public, under licenses (MIT
> and GPL) any lawyer will tell you are not a trap.
>
> No paid tier. No license key, no subscription, no activation, no Pro
> edition, no product page, no waitlist, no pre-order, nothing to buy.
> Skepticism is fair and cheap to settle: ten minutes with the licenses
> and the commit log does it, which beats trust on both cost and
> accuracy.
>
> If someone offers to sell you this firmware, the licenses allow it and
> nobody's calling it theft. Just note that what you take home is their
> build, not this one: a stranger's code, running a laser that has no
> opinion about what it burns. Get it from the source.
ForgeFIRM installs **alongside** the factory firmware on the stock eMMC,
using the factory's own A/B rootfs slot scheme: the installer writes
ForgeFIRM to the slot the factory firmware is *not* running from, and
switches the bootloader to it. Nothing is repartitioned, the factory
`/data` partition (settings, calibration, logs) is untouched, and the
booted factory firmware stays installed in the other slot.
Before anything is overwritten, the installer archives **every** factory
firmware version on the machine — both rootfs slots and the recovery
boot partitions — to `/data/forgefirm/archive/`, so a full offline
factory restore is always possible, no Glowforge cloud required.
> **Requires a ForgeFIRM release that ships the `forgefirm.fw` asset
> (v0.1.0 or later).**
## Prerequisites
- [Serial console](SERIAL.md) access. Current factory firmware does not
offer SSH, so the install is run at the console (login `root`, no
password).
- ~300 MB free on `/data` (a factory machine has far more).
- Internet access on the machine for the standard flow. For an offline
install, place a `forgefirm.fw` on `/data` beforehand and pass its
path to the installer.
**A very important warning: this is experimental software. Use of this
software could seriously maim or kill you or others, and voids your
warranty. It is not affiliated with or endorsed by Glowforge. Use it at
your own risk.**
## Regulatory and legal
Installing ForgeFIRM replaces the firmware of a certified laser product.
This is disclosure, not legal advice — but understand the categories
before you install:
- **Laser product certification (US).** The factory machine is
certified under FDA/CDRH 21 CFR 1040.10 and 1040.11. Modifying it
makes **you** the manufacturer of a modified laser product for
regulatory purposes; the original accession no longer describes the
article you operate.
- **CE/UKCA (EU/UK).** The manufacturer's declaration of conformity no
longer covers the modified machine.
- **Safety listing.** Any UL/ETL or equivalent listing applies to the
product as shipped, not as modified.
- **Insurance.** Property and liability policies commonly exclude fire
loss involving modified equipment. Check yours before running jobs.
The hardware safety interlocks (lid switches, interlock, power-fault
chain) remain active under ForgeFIRM — but the regulatory status of the
machine is yours to own once you flash it.
## Install
Log in at the factory console and run:
```sh
curl -fL https://raw.githubusercontent.com/ScottW514/forgefirm/master/scripts/install-forgefirm.sh --output /tmp/install-forgefirm.sh
sh /tmp/install-forgefirm.sh
```
(For an offline install: `sh /tmp/install-forgefirm.sh /data/forgefirm.fw`)
One stage, no intermediate reboots. The installer:
1. Confirms it is running on factory firmware, from a factory eMMC
slot, with the factory partition layout.
2. Stops the Glowforge services (including the updater).
3. Archives every factory slot version and the recovery boot
partitions to `/data/forgefirm/archive/` (manifest with checksums;
a few minutes each, with progress).
4. Downloads the latest `forgefirm.fw` release (or uses the local file
you passed) and **verifies its signature** before touching anything.
5. Writes ForgeFIRM to the inactive slot with the factory's own `fwup`,
then verifies the written filesystem.
6. Installs `/data/ffboot` (the boot-slot tool) and switches the saved
U-Boot environment to the new slot — the switch is read-back
verified; on any failure the machine keeps booting factory firmware.
7. Reboots into ForgeFIRM.
Login is `root`, no password (also via SSH). Change it.
## Switching firmware
Both systems stay installed; `ffboot` switches between them (as
`/data/ffboot` on factory firmware, on the PATH in ForgeFIRM):
```sh
ffboot -l # inventory: what is in each slot, what boots next
ffboot -e # switch to the factory firmware (newest factory slot)
ffboot -e2 # switch to ForgeFIRM (slot 2 on a standard install)
```
Switch targets are probed first — `ffboot` refuses to select a slot
that does not look bootable (`-f` overrides). Reverting to factory
firmware and back requires no reinstall.
**Routine updates do not use the installer.** Update from the web
control panel's System page, which downloads (or accepts an upload of)
a signed `forgefirm.fw` release, verifies it, and applies it to the
inactive slot. Rerunning the installer is only for recovering a broken
ForgeFIRM install: switch to factory firmware and run it again — it
skips archives it already has and simply rewrites the ForgeFIRM slot.
## Upgrading from a legacy dual-partition install
Machines installed with the previous (partition-carving) installer
migrate automatically: run this installer from the factory firmware;
on ForgeFIRM's first boot from its new slot, the legacy partition is
reclaimed and `/data` grows back to the full factory size. `/data`
contents are preserved throughout.
**NOTE:** This firmware is in beta. It mostly works. It is for
experimentation purposes — not for production. Expect problems.
+13 -14
View File
@@ -1,12 +1,11 @@
# OpenGlow/ForgeFIRM Firmware for Glowforge # OpenGlow/ForgeFIRM Firmware for Glowforge
> # ⚠️ IN DEVELOPMENT — NOT YET RELEASED > # ⚠️ BETA
> >
> **ForgeFIRM has no public release, yet.** Nothing here > **ForgeFIRM is in beta.** Every release below 0.1.0 is a beta release.
> is installable. > Expect problems, and expect frequent updates. Upgrade whenever a newer
> The documentation describes the firmware as it is being built > release is available, and report what you find on the
> and validated on the bench. It is here to be read, not followed. > [community forum](https://community.openglow.org).
> The first public release is expected in September 2026. If you are interested in being an early tester, please reach out to the developer.
Open-source firmware for Glowforge brand CNC lasers. ForgeFIRM replaces the Open-source firmware for Glowforge brand CNC lasers. ForgeFIRM replaces the
cloud-dependent factory software on the **stock control board** — no hardware cloud-dependent factory software on the **stock control board** — no hardware
@@ -14,13 +13,13 @@ modification — and gives the machine a local controller, a local web control
panel, and a standard Grbl interface. panel, and a standard Grbl interface.
* [Latest Release](https://github.com/ScottW514/forgefirm/releases) * [Latest Release](https://github.com/ScottW514/forgefirm/releases)
* [Installation Instructions](https://github.com/ScottW514/forgefirm/blob/master/INSTALL.md) * [Installation Instructions](https://docs.forgefirm.org/install/)
* [Build Instructions](https://docs.forgefirm.org/developers/building/) * [Build Instructions](https://docs.forgefirm.org/developers/building/)
* [Connecting LightBurn](https://github.com/ScottW514/forgefirm/blob/master/docs/LIGHTBURN.md) * [Connecting LightBurn](https://docs.forgefirm.org/usage/lightburn/)
* [How motion and the laser are driven](https://github.com/ScottW514/forgefirm/blob/master/docs/MOTION.md) * [How motion and the laser are driven](https://docs.forgefirm.org/technical/forgefirm/)
* [How cooling and airflow work](https://github.com/ScottW514/forgefirm/blob/master/docs/COOLING.md) * [How cooling and airflow work](https://docs.forgefirm.org/technical/forgefirm/cooling-engine/)
* [The cameras and the video stream](https://github.com/ScottW514/forgefirm/blob/master/docs/VIDEO.md) * [The cameras and the video stream](https://docs.forgefirm.org/usage/cameras/)
* [How the laser safing works](https://github.com/ScottW514/forgefirm/blob/master/docs/SAFETY.md) * [How the laser safing works](https://docs.forgefirm.org/safety/)
* [How a release is tested before being accepted](https://docs.forgefirm.org/developers/acceptance/) * [How a release is tested before being accepted](https://docs.forgefirm.org/developers/acceptance/)
* [Community Support](https://community.openglow.org) * [Community Support](https://community.openglow.org)
@@ -76,8 +75,8 @@ you get to read what you're running.
**These machines contain a CO₂ laser: it burns, blinds, and starts **These machines contain a CO₂ laser: it burns, blinds, and starts
fires.** Never defeat the lid switches or interlock. Never leave a fires.** Never defeat the lid switches or interlock. Never leave a
running job unattended. Keep a fire extinguisher within reach. Read [this](docs/LIGHTBURN.md#before-you-cut--safety-read-this-first) before you cut running job unattended. Keep a fire extinguisher within reach. Read [this](https://docs.forgefirm.org/safety/) before you cut
your first job, and read this [regulatory and legal](INSTALL.md#regulatory-and-legal) section before installing. [This](docs/SAFETY.md) document describes the hardware safety chain and the software gates ForgeFIRM stacks on it. your first job, and read this [regulatory and legal](https://docs.forgefirm.org/install/#regulatory-and-legal) section before installing. [This](https://docs.forgefirm.org/technical/machine/safing-chain/) page describes the hardware safety chain and the software gates ForgeFIRM stacks on it.
**THIS IS EXPERIMENTAL SOFTWARE **THIS IS EXPERIMENTAL SOFTWARE
Use of this software Use of this software
-24
View File
@@ -1,24 +0,0 @@
# Glowforge Serial Port Access
Older Glowforges have a Micro-USB serial port connector on the control board (J13 located in the lower left of the board picture below). If you are fortunate enough to have one of those units, you can just plug that into your PC. It's a standard [FTDI FT230](https://ftdichip.com/products/ft230xq/) serial-to-USB adapter and the driver is included in most operating systems.
If you are not that fortunate, you can follow these instructions:
First, you will need a soldering iron. No getting around that one. Next, you need a FTDI [TTL-232RG-VREG1V8-WE](https://www.ftdichip.com/Products/Cables/USBTTLSerial.htm) USB to Serial Port adapter. It is important that you get the 1.8V model. If you get a 3.3V or 5V version you will irreversibly destroy your control board. You can purchase this directly from FTDI, Amazon, or most electronic supply shops.
When you receive your adapter, you'll need to prep it. We only require three of the lead wires. The others should be trimmed (to unequal lengths, lest they short out when you heatshrink/tape them up).
![CablePrep|690x218](https://raw.githubusercontent.com/ScottW514/forgefirm/master/docs/assets/CablePrep.jpg)
We'll be connecting to the microprocessor's serial console port test points: RXD: D3B, TXD: D3D.
![Control_PCB_TestPoints|502x500](https://raw.githubusercontent.com/ScottW514/forgefirm/master/docs/assets/Control_PCB_TestPoints.jpg)
I recommend soldering the ground wire first to the ground pad located to the upper left of the microprocessor (see picture below). Clip the leads for the TXD and RXD to be no longer a millimeter before soldering. This helps to avoid unwanted contact with surrounding items.
![Connection|690x435](https://raw.githubusercontent.com/ScottW514/forgefirm/master/docs/assets/Connection.jpg)
After you've soldered all the leads, secure the cable to the PCB with a tie wrap, as pictured. This is important. The test points will not stand much mechanical stress, and will rip off the board easily. Don't worry, if this happens to you, they are still accessible from the bottom of the board - second chance.
From there, connect the USB cable to your computer and fire up your favorite terminal (Putty works well). The serial settings are 115,200 8N1. Be sure to select a color terminal with UTF-8 encoding if you want the best experience from the OpenGlow firmware.
NOTE: You will need to keep the serial port adpater powered on at all times when you are operating the Glowforge. If it is not powered, it draws down the 1.8V bus on the control board and prevents it from booting. If you don't have always have a computer near by, you can plug it into a USB charging adapter.
+19 -25
View File
@@ -12,13 +12,7 @@ Read together with:
| Document | What it settles | | Document | What it settles |
|---|---| |---|---|
| `kernel-module-glowforge/UAPI.md` | the pulse-stream feeder contract, sysfs attributes, sensor conversions | | [docs.forgefirm.org](https://docs.forgefirm.org/) | the documentation site: safety, install, usage, the machine as built, how ForgeFIRM works (the kernel module and the pulse feeder contract, the forgectrl machine-services contract, the cooling engine, the video pipeline, cloud mode), and the developer pages: build, release flow, acceptance, tests, the bench runbook |
| `forgectrl/docs/SERVICES.md` | the machine-services contract: switch map, hardware ownership, cooling channels, mode supervision, pulse-device ownership, logging |
| `docs/SAFETY.md` | the hardware safing chain, decoded |
| `docs/VIDEO.md` | the cameras as users meet them: endpoints, delivered geometry, and what the sensors can do that ForgeFIRM does not send |
| `docs/LIGHTBURN.md`, `docs/UPDATE-SYSTEM.md`, `INSTALL.md` | sender setup, A/B update system, install |
| [docs.forgefirm.org/developers](https://docs.forgefirm.org/developers/) | build, release flow, tests, the bench runbook |
| `python3-gfhardware/forgefirm-app/docs/CLOUD.md` | cloud mode, including its own open items |
## Where the project stands ## Where the project stands
@@ -263,7 +257,7 @@ the dose-curve recorder streams the ladder job itself from one Record
press (absolute from X0 Y0, refused while a sender is connected; the press (absolute from X0 Y0, refused while a sender is connected; the
operator's button press starts the fire with every arm gate standing), operator's button press starts the fire with every arm gate standing),
records the tube current and the head thermopile, fits the rungs, and records the tube current and the head thermopile, fits the rungs, and
Apply writes the result (`forgectrl/docs/SERVICES.md`). Rasters hold their tonality down Apply writes the result ([forgectrl](https://docs.forgefirm.org/technical/forgefirm/forgectrl/)). Rasters hold their tonality down
to ~14 pulse slots per pixel (508 DPI at 6000 mm/min): the dither to ~14 pulse slots per pixel (508 DPI at 6000 mm/min): the dither
accumulator's cross-pixel averaging recovers the levels, with no accumulator's cross-pixel averaging recovers the levels, with no
visible dither pattern. visible dither pattern.
@@ -325,7 +319,7 @@ report, overrides, driver version, `ts_mono` for age; on change plus a
`grbl` block only while it supervises a live GRBL controller, serves `grbl` block only while it supervises a live GRBL controller, serves
the settings file at `GET /grbl/settings`, and the panel's GRBL card the settings file at `GET /grbl/settings`, and the panel's GRBL card
renders it. Position stays out: it changes per segment and is served renders it. Position stays out: it changes per segment and is served
from the kernel counters. Contract: `forgectrl/docs/SERVICES.md`. from the kernel counters. Contract: [forgectrl](https://docs.forgefirm.org/technical/forgefirm/forgectrl/).
**Emission evidence.** `cnc/laser_on_sampled` (surfaced as `/status` **Emission evidence.** `cnc/laser_on_sampled` (surfaced as `/status`
`laser.emission_samples`) is the reliable live-emission witness; emission `laser.emission_samples`) is the reliable live-emission witness; emission
@@ -423,7 +417,7 @@ sysvinit script from the repo's `init/`; bench builds cross-compile with
`forgefirm/scripts/bench/build-forgectrl.sh`. The **machine-services `forgefirm/scripts/bench/build-forgectrl.sh`. The **machine-services
contract** — EV_SW switch map, sensor conversions, hardware single-writer contract** — EV_SW switch map, sensor conversions, hardware single-writer
ownership, cooling channels, mode supervision, pulse-device ownership, logging ownership, cooling channels, mode supervision, pulse-device ownership, logging
— is `forgectrl/docs/SERVICES.md`. — is [forgectrl](https://docs.forgefirm.org/technical/forgefirm/forgectrl/) on the documentation site.
Every state-changing endpoint requires the first-boot bearer token in `/data` Every state-changing endpoint requires the first-boot bearer token in `/data`
(embedded in the panel), a Host address-literal check, and (embedded in the panel), a Host address-literal check, and
@@ -487,7 +481,7 @@ One ulfius daemon serves it all:
snapshot answer 409 while the lid is open. snapshot answer 409 while the lid is open.
- `GET /slots`, `POST /boot`, `POST /update/check|download|apply|upload`, - `GET /slots`, `POST /boot`, `POST /update/check|download|apply|upload`,
`GET /update/status`, `POST /restore/factory`, `POST /system/reboot` — the `GET /update/status`, `POST /restore/factory`, `POST /system/reboot` — the
A/B update manager (`docs/UPDATE-SYSTEM.md`). Upload is auth + idle + job A/B update manager ([install and update](https://docs.forgefirm.org/technical/forgefirm/install-and-update/)). Upload is auth + idle + job
gated; a booted-slot write is refused under any `root=` spelling. gated; a booted-slot write is refused under any `root=` spelling.
- `GET /logs`, `GET /logs/tail`, `POST /logs/export` — the logging tree - `GET /logs`, `GET /logs/tail`, `POST /logs/export` — the logging tree
(below). (below).
@@ -511,7 +505,7 @@ open lid refuses stream and snapshot with HTTP 409 and a lid opened mid-capture
tears the pipeline down; `gfhardware.cam.capture()` enforces the same rule for tears the pipeline down; `gfhardware.cam.capture()` enforces the same rule for
the cloud client's direct-V4L2 fallback and raises `LidOpen`. No setting the cloud client's direct-V4L2 fallback and raises `LidOpen`. No setting
disables it, and the factory's lid-open focus hunt now fails as a result disables it, and the factory's lid-open focus hunt now fails as a result
(`docs/VIDEO.md` §2, `forgectrl/docs/SERVICES.md`). Geometry, Bayer depth and ([the video pipeline](https://docs.forgefirm.org/technical/forgefirm/video-pipeline/)). Geometry, Bayer depth and
the manual control set come from a **sensor profile** chosen by whichever the manual control set come from a **sensor profile** chosen by whichever
driver bound on that camera's I2C bus, so one image serves both the 5 MP driver bound on that camera's I2C bus, so one image serves both the 5 MP
OV5648 (2592×1944) and the 8 MP OV8856 (3264×2448) — both 8-bit BGGR, so the OV5648 (2592×1944) and the 8 MP OV8856 (3264×2448) — both 8-bit BGGR, so the
@@ -623,8 +617,8 @@ settings **applied at reboot** — the panel's Logs tab shows configured vs.
effective and offers the reboot, plus a live viewer and a sanitized `tar.gz` effective and offers the reboot, plus a live viewer and a sanitized `tar.gz`
export for issue reports (`POST /logs/export`; `src/sanitize.c` replaces export for issue reports (`POST /logs/export`; `src/sanitize.c` replaces
serial, hostname, cloud credentials, panel token, SSID/PSK, IPs, MACs and serial, hostname, cloud credentials, panel token, SSID/PSK, IPs, MACs and
e-mails with stable placeholders). Design and contract: `SERVICES.md` e-mails with stable placeholders). Design and contract:
"Logging". [logging](https://docs.forgefirm.org/technical/forgefirm/logging/).
## Release acceptance (forgetest, port 8090) ## Release acceptance (forgetest, port 8090)
@@ -710,7 +704,7 @@ is committed.
GPIO 7 to GND. The lid contact (NC) goes in series with the lid-switch GPIO 7 to GND. The lid contact (NC) goes in series with the lid-switch
loop at J4.12/13; the button contact (NO) across the front button input loop at J4.12/13; the button contact (NO) across the front button input
at J5 (BTN and its 12 V); the interlock contact (NC) in the remote at J5 (BTN and its 12 V); the interlock contact (NC) in the remote
interlock loop at J8 (SAFETY.md; J6 is the speaker). The machine's 3.3 V interlock loop at J8 ([the safing chain](https://docs.forgefirm.org/technical/machine/safing-chain/); J6 is the speaker). The machine's 3.3 V
rail carries the three coils with room to spare. The DevKit and the rail carries the three coils with room to spare. The DevKit and the
machine share a ground through the modules, so the DevKit is powered from machine share a ground through the modules, so the DevKit is powered from
a USB wall adapter. The interposer harness itself is bench-local and is a USB wall adapter. The interposer harness itself is bench-local and is
@@ -744,7 +738,7 @@ is committed.
every fan held to a measured floor with a fault, not a pause, for the every fan held to a measured floor with a fault, not a pause, for the
session; a coolant critical line above the ceiling's pause; the board session; a coolant critical line above the ceiling's pause; the board
temperatures watched per job; and the rest of the envelope declared, tag by temperatures watched per job; and the rest of the envelope declared, tag by
tag, in `CLOUD.md` "The pulse header". tag, on [the factory firmware](https://docs.forgefirm.org/technical/machine/factory-firmware/) under "The pulse header".
- **Board temperatures at idle** (room ~22 C, machine on for hours): the - **Board temperatures at idle** (room ~22 C, machine on for hours): the
chassis LM75 reads **29.0 C**, `pic/pwr_temp` reads **589 raw** (the chassis LM75 reads **29.0 C**, `pic/pwr_temp` reads **589 raw** (the
unverified guess `raw * 0.08715 - 21` would make that 30.3 C), the SoC die unverified guess `raw * 0.08715 - 21` would make that 30.3 C), the SoC die
@@ -879,7 +873,7 @@ is committed.
magnitude to spare. Bounded queue depth plus `SCHED_FIFO` for the feeder is magnitude to spare. Bounded queue depth plus `SCHED_FIFO` for the feeder is
the design; RT is worth revisiting only if the underrun bench ever the design; RT is worth revisiting only if the underrun bench ever
contradicts this arithmetic. contradicts this arithmetic.
- Byte layout and stream rules: see the UAPI.md feeder contract - Byte layout and stream rules: see [the pulse feeder contract](https://docs.forgefirm.org/technical/forgefirm/pulse-feeder-contract/)
(authoritative). (authoritative).
- **Z**: bit 6 SET = lens UP = +Z (hardware-verified). Home = hall trigger at - **Z**: bit 6 SET = lens UP = +Z (hardware-verified). Home = hall trigger at
TOP; usable travel ≈ 30 half-steps ≈ 10.6 mm ≈ 0.417"; 0.3534 mm/half-step. TOP; usable travel ≈ 30 half-steps ≈ 10.6 mm ≈ 0.417"; 0.3534 mm/half-step.
@@ -964,7 +958,7 @@ is committed.
33 °C, resume 31 °C (factory job-header CMrx/…); the factory's low side 33 °C, resume 31 °C (factory job-header CMrx/…); the factory's low side
(floors ≈1.0/4.0 °C, ~16 °C warm-up gate) is not implemented yet. The (floors ≈1.0/4.0 °C, ~16 °C warm-up gate) is not implemented yet. The
coolant thermistor conversion is the factory B-equation recovered from the coolant thermistor conversion is the factory B-equation recovered from the
v2.6.0 binary — derivation in `kernel-module-glowforge/UAPI.md`; the old v2.6.0 binary — derivation on [sensors](https://docs.forgefirm.org/technical/machine/sensors/); the old
UAPI "best guess" linear formula was 3–5 °C high and everything derived from UAPI "best guess" linear formula was 3–5 °C high and everything derived from
it had to be re-derived. The flow check's bands hold from 19 to 27 C, the it had to be re-derived. The flow check's bands hold from 19 to 27 C, the
loop heater's ceiling in a 20 C room, with the margin widening warm; above loop heater's ceiling in a 20 C room, with the margin widening warm; above
@@ -1104,7 +1098,7 @@ is committed.
LOW through a run — the same physical behavior, inverted). The DTS now LOW through a run — the same physical behavior, inverted). The DTS now
declares it active-low, and the former `estop_halts_motion` / declares it active-low, and the former `estop_halts_motion` /
`MOTION.ESTOP_HALTS_MOTION` opt-in is gone: a real e-stop belongs in the `MOTION.ESTOP_HALTS_MOTION` opt-in is gone: a real e-stop belongs in the
lid-switch chain (`docs/SAFETY.md`). Doors/door1/door2 stay stable during lid-switch chain ([the safing chain](https://docs.forgefirm.org/technical/machine/safing-chain/)). Doors/door1/door2 stay stable during
motion. motion.
- **Factory job behavior on the lid and the button, measured on 2.6.0-2228** - **Factory job behavior on the lid and the button, measured on 2.6.0-2228**
(bench session 2026-08-16; this is what ForgeFIRM's parity policy (bench session 2026-08-16; this is what ForgeFIRM's parity policy
@@ -1118,7 +1112,7 @@ is committed.
hunt is not lid-gated. hunt is not lid-gated.
- **The hardware button latch is what makes the armed window honest.** A lid - **The hardware button latch is what makes the armed window honest.** A lid
open SETs it (set-dominant), and it stays SET until the lid is closed, the SoC open SETs it (set-dominant), and it stays SET until the lid is closed, the SoC
lock is released **and the button is pressed** (`docs/SAFETY.md`). So a policy lock is released **and the button is pressed** ([the safing chain](https://docs.forgefirm.org/technical/machine/safing-chain/)). So a policy
that cancels the job on a lid open and re-arms only through a fresh button that cancels the job on a lid open and re-arms only through a fresh button
press keeps software and hardware in agreement by construction; one that press keeps software and hardware in agreement by construction; one that
resumes a job after a lid open leaves the beam blocked in hardware while resumes a job after a lid open leaves the beam blocked in hardware while
@@ -1190,7 +1184,8 @@ is committed.
## Next work ## Next work
Open items only. Anything closed is in `CAMPAIGN-LOG.md`. Open items only. Anything closed is in `CAMPAIGN-LOG.md`. Open items (bugs,
feature requests, enhancements) will eventually be tracked as GitHub issues.
1. **Limit-switch homing.** The planned second homing method (`$22` stays 0 1. **Limit-switch homing.** The planned second homing method (`$22` stays 0
until it lands). until it lands).
@@ -1222,9 +1217,8 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
first GitHub release, per the site (Developers, "Release flow"), once first GitHub release, per the site (Developers, "Release flow"), once
ready to publish. Repoint the core submodule to ready to publish. Repoint the core submodule to
upstream if the `step_us_min` sizing fix merges. upstream if the `step_us_min` sizing fix merges.
5. **Update system Phase 5 — recovery refresh.** The remaining phase of 5. **Update system — recovery refresh.** A refreshed recovery image in
`docs/UPDATE-SYSTEM.md` (a refreshed recovery image in boot0); Phases 0–4 boot0; the design is on [install and update](https://docs.forgefirm.org/technical/forgefirm/install-and-update/).
are done.
6. **Head IRQ (exploratory).** 6. **Head IRQ (exploratory).**
Owed for the head IRQ, only if a coarse hardware interrupt is wanted Owed for the head IRQ, only if a coarse hardware interrupt is wanted
instead of the poll: arm the accel bit in the head MCU (reg 0x03/0x04), instead of the poll: arm the accel bit in the head MCU (reg 0x03/0x04),
@@ -1249,7 +1243,7 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
(`cloud_pause_backtrack_ticks` 2000, `cloud_resume_lead_ticks` 1950), and (`cloud_pause_backtrack_ticks` 2000, `cloud_resume_lead_ticks` 1950), and
the kernel offers the same mechanism to a live feed, bounded by the ring's the kernel offers the same mechanism to a live feed, bounded by the ring's
retained history (`cnc/max_backtrack`; the facts bank "SDMA pulse engine" retained history (`cnc/max_backtrack`; the facts bank "SDMA pulse engine"
and `UAPI.md`). What is not settled is the bookkeeping above it: a and [the pulse feeder contract](https://docs.forgefirm.org/technical/forgefirm/pulse-feeder-contract/)). What is not settled is the bookkeeping above it: a
backward run moves the head and the kernel's counters while grblHAL's backward run moves the head and the kernel's counters while grblHAL's
planner still holds a partly executed block, so borrowing the mechanism planner still holds a partly executed block, so borrowing the mechanism
means reconciling the two, and a GRBL cut runs a much shorter queue than a means reconciling the two, and a GRBL cut runs a much shorter queue than a
-488
View File
@@ -1,488 +0,0 @@
# Cooling and airflow
The tube is water-cooled and the enclosure is air-cleared, and both matter
while the laser fires: coolant that has stopped circulating will let a tube
overheat within a cut, and smoke that is not pulled out spoils the work and
fogs the optics. ForgeFIRM runs this as one service — the **cooling engine** —
that owns every piece of thermal hardware and answers one question at a time
for whichever controller is running: *is it safe to fire right now?*
This page explains what the system is made of, how it decides, what you will
see when it intervenes, and what you can tune.
- The beam itself is gated in hardware; see [Laser safety](SAFETY.md).
- For how the laser and motion are driven, see [Motion and laser drive](MOTION.md).
---
## 1. What the hardware is
**The coolant loop** is closed: a pump, a radiator with fans, the laser tube,
and two thermistors — one **upstream** of the tube and one **downstream** of a
small inline heater. Pro machines are specified with a thermoelectric cooler
(TEC) on the loop; the board cannot tell whether one is fitted (§9). The
heater exists for diagnostics, not for warming the machine up: it is
how the engine proves the coolant is actually moving (§4).
**The airflow path** has four independently driven pieces:
| Piece | What it does |
|---|---|
| Exhaust blower | pulls smoke out of the enclosure |
| Two intake fans | feed clean air in behind it |
| Air assist (in the head) | blows the cut line clear at the focal point |
| Purge air (in the head) | keeps the optics clean; on whenever the machine is on |
Every fan reports a tachometer, so the engine can tell a commanded duty from an
actual airflow, and the panel shows real speeds rather than setpoints.
**Coolant temperature is read, not guessed.** Both thermistors are converted
with the factory's own beta-equation curve, checked against a thermometer. A
sensor reading at either rail is treated as open or shorted — not as a
temperature.
---
## 2. One owner, two clients
The cooling engine lives in `forgectrl`, the machine-services daemon, and it is
the **only** thing that writes fans, pump, TEC and heater. Whichever controller
is running — GRBL or cloud — is a client of it, over two channels:
- **The controller reports its job state** about once a second: idle, running
or cooling down, whether the laser is armed, and (in cloud mode) the fan
duties the job asks for. The reports are level-triggered, so a lost one
simply corrects itself on the next.
- **The engine publishes a verdict** the controller reads and enforces in its
own process: may the laser fire, should the job hold, may it resume.
Two properties of that split are worth understanding, because they explain the
machine's behavior in odd situations:
**A missing verdict is a bad verdict.** If the verdict is absent or more than two
seconds old, a controller treats it as *fire blocked, hold*. The engine going
away looks exactly like a fault, never like permission.
**Arming requires being seen.** The engine only grants fire when it is
receiving fresh job reports. A controller about to fire is, by contract, one
that is reporting — an armed window the engine cannot see never gets a green
light.
**If a controller goes silent** past five seconds, the engine blocks fire
immediately and stands the machine down through the normal cooldown, because a
smoke clear is the right physical response to a job that died mid-cut. If the
silence happens while the laser is armed, or while the pulse engine still says
a program is playing, the engine additionally stops motion and locks the laser
latch itself. It also refuses to let exhaust and intake drop below cooldown
duty while a program is still running.
**A cloud job brings its own envelope.** The pulse file the Glowforge service
sends opens with the job's operating limits, and the cloud client hands the
ones the engine has a use for along with every report: the coolant window
and the fans' minimum speeds. The engine takes each only where it is
stricter than the setting on the Machine tab: a ceiling can only come down
for a job, a floor can only go up, a looser value is noted in the log and
ignored, and a gate you turned off (§8a) stays off whatever the job says.
The coolant ceiling is the one limit a job can tighten today (the service
sends 33 °C on a cut, which is also the shipped default); the fan floors
are carried and logged ahead of the airflow gates. The effective set shows
in the log as `effective limits:` and in `/cool/status` as `limits`. A GRBL
job has no header and runs on the settings alone.
**If a diagnostic takes the hardware over** (§6), the engine suspends its own
writes and publishes fire-blocked until the diagnostic finishes.
**If the engine itself is provably gone** while the laser is armed, the
controller writes the factory run duties to the fans once, holds the job, and
stands down. That is the single sanctioned exception to single-owner control,
and the duties are compiled in so that a lost configuration file cannot take
the fans with it.
---
## 3. What the fans do, and when
The engine runs in phases. Duties are the factory machine's own values.
| Phase | Pump | Air assist | Exhaust | Intake | Heater |
|---|---|---|---|---|---|
| **Idle** | on | 204 | off | off | off |
| **Run** (or armed, whatever the reported mode) | on | 1023 | 65535 | 43278 | flow checks only |
| **Cooldown — smoke clear** (15 s) | on | run duty | run duty | run duty | off |
| **Cooldown — thermal** | on | idle | 32768 | 21639 | off |
| **Over-temp / fault hold** | on | run duty | forced | forced | off |
Notes on the phases:
- **The pump runs whenever the machine is on**, including at idle. Circulation
is cheap; a stagnant loop with a warm tube is not.
- **The heater is off at idle by design.** An always-on flow heater measurably
warms the loop within minutes, eating headroom below the start gate for no
benefit while nothing can fire.
- **Being armed counts as running.** If the laser is armed, the engine forces
the run profile and the flow checks regardless of what mode the controller
reported — fire never happens without cut airflow and active flow
verification.
- **Cooldown has two stages**: a smoke clear at full run duty, then reduced
airflow (the radiator cools the loop measurably) until the upstream coolant
temperature is back under the resume gate or the cooldown budget expires.
- **The TEC is left off.** Its output has no readback, so the machine cannot
tell whether one is fitted; driving it blind is not something ForgeFIRM does
(see §9).
In **GRBL mode** the run profile follows your sender's `M8`/`M9` (LightBurn's
per-layer Air Assist), OR'd with the armed window. In **cloud mode** the job's
own header carries the duties and the client passes them through, so a print
gets the fan profile the service designed for it and a lens hunt stays quiet.
### 3a. Airflow gates: a fan that is not moving the air
Commanding a fan and getting airflow are two different things, and the
machine can tell them apart: the exhaust, the two intakes and the air assist
carry tachometers, and the purge-air fan in the head reports its current.
While the run profile is applied, the engine holds every one of them to a
floor.
- **The floors** are settings (§8): `cool_tach_exhaust_min_rpm`,
`cool_tach_intake_min_rpm` (either intake), `cool_tach_air_assist_min_rpm`
and `cool_purge_min_current`, each 55 percent of the steady speed the fan
reaches at the cut profile on the bench machine (exhaust 11640, intakes
4160, air assist 11050 rpm; the recommended bands are 50 to 60 percent).
A cloud job's header can raise a tach floor for that job, never lower it
(§2).
- **A fan is judged at the operating point its floor was measured at.**
While the laser is armed every fan is judged, and a job's own fan profile
(a cloud header's run duties) may raise a fan above the cut profile but
never lower it while armed. Unarmed, a fan is judged whenever it is
commanded at or above the cut profile (a bare `M8` from a GRBL job), and a
fan the job runs slower is measured, published as `unjudged`, and not
judged: the factory's hunts and homing moves run with the exhaust and the
intakes off and the air assist at idle, and nothing can fire during them.
The purge fan has no duty (it is always on) and is judged in every run.
- **A spin-up grace** (`cool_fan_grace_s`) runs from the moment the run
profile is written; nothing counts inside it, because the big exhaust fan
takes seconds to reach speed.
- **Three seconds under the floor trip the gate**, and a single reading at
or above it in between clears the count, so a tach reading that wanders
does not end a job.
- **A trip is a fault, not a pause.** The verdict goes `AIRFLOW`, fire is
blocked, the job holds, and there is no resume for the rest of that run
session: a fan that has stopped moving air is not a condition to cut
through. The fans stay at run duty (a stalled extraction fan needs every
other fan around it running), and the reason names the fan, the reading
and the floor. The fault ends with the session: at idle the verdict is
`OK` again (a standing hold would cancel jogs and refuse the next job
before it could re-prove the fan), and the next session judges every fan
afresh after the grace.
- **A floor of zero is that gate off** (§8a). It still measures: the first
reading in a job that would have tripped the shipped default is logged.
`/cool/status` carries each fan's reading, floor and state (`grace`, `ok`,
`under`, `TRIPPED`, `off`, `unjudged` for a fan the job runs below the cut
profile unarmed, or `idle` outside a run) as `fan_gates`.
---
## 4. Coolant flow verification
### The problem
A pump can stop, an impeller can slip, a line can airlock — and none of it
shows up in a temperature reading until the tube is already in trouble.
Absolute coolant temperature only tracks a loop that is *circulating*, and
"coolant should warm up while cutting" is not a usable signal either: a light
engrave may add no measurable heat at all.
### The method
The small heater sits between the two thermistors. Each check runs it at a
fixed duty for a fixed window and watches how far the **downstream** sensor
climbs:
- **flowing coolant carries that heat away** — the downstream sensor rises a
little;
- **a stagnant loop cooks the sensor** — the downstream sensor rises a lot.
The discriminator is the rise, not the difference between sensors, and the
operating point is measured rather than assumed:
| Parameter | Value | Why |
|---|---|---|
| Heater duty | 40 % | Below about 40 %, natural convection sheds the heat well enough to *mimic* flow — dead-pump trials have looked healthier than a working pump. At 40 % heat input outruns convection, and it is the cheapest duty that does. |
| Window | 50 s | Long enough for the bands to separate cleanly. |
| Fault threshold | 14.4 °C rise | Midway between the observed flowing band and the observed stagnant band. |
| Re-check interval | 150 s | A pump that stops mid-job is invisible otherwise. |
Each check costs the loop under a degree of heating, and with cut-profile fans
running the loop still nets cooler over a long job.
### Checks start from a settled loop
Measuring a rise from a baseline captured while the loop is still cooling from
earlier heat produces garbage — and it fails in the dangerous direction: it can
report flow with the pump stopped. So a check is *requested*, and starts only
once the two sensors agree within 1.5 °C **and** the downstream reading has
stopped drifting.
Stationarity is judged by comparing the mean of the first half of a 15-second
window against the second half, not by peak-to-peak spread. On a settled loop,
peak-to-peak noise is about 0.5 °C while the split-half difference is about
0.1 °C — any peak-to-peak threshold tight enough to catch real drift would sit
below the noise floor and never open the gate.
### One bad reading is a suspicion, not a fault
Transients happen: cycling the pump by hand can burp an airlock that clears
itself within minutes. So the engine runs a two-step decision:
1. **First over-limit check → `COOLANT FLOW SUSPECT`.** A warning, a hold
request, and an immediate re-check — no waiting for the normal cadence.
2. **The next completed check decides.** Over-limit again, with no clean check
in between → `COOLANT FLOW FAULT`. Clean → the suspicion clears and the job
continues.
Two more rules close the loopholes:
- **A suspicion that cannot resolve escalates.** If no verdict can be produced
within the confirmation budget (default 480 s), it becomes a fault: a loop
that will not settle after a fault-level reading has shown no evidence of
health.
- **Cleared suspicions still count.** Three of them in one job earn an
aggregated "check your coolant" warning; the counter resets when cooldown
reaches idle.
A clean check from the fault state logs a recovery.
### What the verdicts do
| Verdict | Effect |
|---|---|
| `OK` | Fire permitted. |
| `SUSPECT` | Hold requested, cut airflow held; auto-resumes on a clean re-check. |
| `FAULT` | Fire gated and the hold stands — for the operator to resolve. |
| `OVERTEMP` | Hold with forced cooling airflow; auto-resumes below the resume gate (§5). |
| `CRITICAL` | The coolant at or over the critical line in a run session: fire blocked, hold, no resume this job (§5). |
| `AIRFLOW` | A fan under its floor: fire blocked, hold, no resume this job (§3a). |
| `FIRE` | Motion stopped, latch locked, hold until the next run session (§7). |
Practical note: **expect a legitimate suspicion on the first checks after
manually stopping and starting the pump.** That is an airlock, the machinery
above absorbs it, and it clears on its own.
---
## 5. Over-temperature
The engine uses the factory's coolant windows:
- **Run ceiling 33 °C** — above this, the verdict goes `OVERTEMP` with a hold
request and cooling airflow forced on.
- **Resume gate 31 °C** — below this, recovery is signaled and the controller
resumes automatically.
- **Critical line 38 °C** (`cool_temp_critical_c`, §8) — a second tier above
the ceiling, and a different kind: at or over it during a run session the
verdict goes `CRITICAL`, fire is blocked, the job holds, and there is no
resume for the rest of that session, because a loop that ran through the
pause tier and kept climbing is not a condition to cut through. The fault
ends with the session; the ceiling's pause keeps holding while the loop is
hot, and the next session judges the line afresh. A cloud job's header
carries no critical line for the coolant, so this one is always the local
setting; the settings API keeps it above the ceiling while the ceiling is
a gate (a ceiling at its off end leaves the line standing alone), and at
its top (70 °C) it is the gate turned off (§8a).
The **upstream** sensor gates, because it reads the coolant actually entering
the tube.
What you see depends on what the machine was doing. A running cycle takes a
feed hold and resumes by itself once the loop recovers — your sender shows the
hold state and a warning message. A jog is canceled instead (a jog cannot be
held). Fire stays gated for the whole excursion.
---
## 6. Diagnostics: verifying and calibrating flow
The web panel's **Diagnostics** tab runs the two cooling tools. Both take the
hardware over: the active controller is suspended for the duration, the engine
stands aside, and the controller is restored on every exit path — completion,
error, or your pressing Abort. The laser stays latched throughout. Progress,
both coolant temperatures and a scrolling log stream to the page while it runs.
Both tools run at your *configured* duty, window and threshold, so the verdict
applies to the check the machine actually performs, and both use cut-profile
chassis fans — the condition the numbers were characterized under. Any
pump-off window aborts immediately if the downstream sensor passes 48 °C.
**Flow verify** (about 3 minutes) — one check with the pump running and one
with it commanded off.
- **PASS** = your threshold separates the two readings.
- Margins under 1.5 °C add a warning that you should re-calibrate.
- A failure here means the threshold no longer suits the loop, or the loop has
a real problem.
**Flow calibrate** (15–25 minutes) — three trials of each case, alternating,
with settle gates between them. It reports both bands and recommends a
threshold midway between the highest flowing reading and the lowest stagnant
one, with an **Apply** button that writes it to your settings.
- If the gap between the bands is under 3 °C it refuses to recommend anything
and tells you to raise the heater duty and rerun.
**When to calibrate:** after replacing coolant, after changing or servicing the
pump, if flow verify warns about thin margins, or if you see suspicions that
you can trace to nothing real. The shipped default suits the factory loop; a
rebuilt one may differ.
---
## 7. The fire watch
Alongside the flow work, the engine watches for evidence of things going wrong
at one-second resolution:
- **Emission evidence.** The kernel samples the *gated output* of the hardware
AND-gate — actual emission, not a commanded state. Emission seen with no
armed window in the recent past stops motion and locks the latch, and keeps
doing so while the evidence persists.
- **Laser power-good degradation** during an armed window is warned once per
session.
- **Stepper-driver faults** appearing during a run are warned, and HV current
is ranged for each job in the same log line.
- **Lid infrared channels** are polled every tick, and every job logs their
baseline and peaks.
**About the lid IR fire watch specifically:** it ships in *watch-only* mode and
logs rather than acts. The reason is honest and worth stating — those sensors
are, first of all, a photometer for the lid lamp. A full-power cut raises them
only a few counts above the level the lamp sets, a candle burning on the bed
raises them about the same amount, and anything that changes the lamp (a camera
snapshot, for instance) moves them by tens of counts. A fixed threshold would
therefore stop jobs for lighting changes while still missing a small flame. A
lamp-aware design is planned; until then the channels are recorded, not acted
on, and **the fire watch is not a fire alarm**. Never leave a running laser
unattended.
---
## 8. Settings
All of these live in the panel's Machine tab, are validated on entry, and can
only be changed while the machine is idle. The engine re-reads them at the
start of every run, so a change takes effect on your next job.
| Setting | Default | Legal range | Recommended | What it controls |
|---|---|---|---|---|
| `cool_flow_rise` | 14.4 °C | 1 to 40 °C | 8 to 16 °C | Downstream rise that counts as no-flow. Set this from **flow calibrate**; above the band the check can never fault. |
| `cool_flow_heater_pct` | 40 % | 0 to 100 % | | Heater duty during a check. Raising it separates the bands further at the cost of warming the loop more. |
| `cool_flow_check_s` | 50 s | 0 to 300 s | 30 to 120 s | Length of a check window. `0` turns flow verification off (§8a). |
| `cool_recheck_s` | 150 s | 0 to 3600 s | | How often checks repeat during a job. |
| `cool_confirm_max_s` | 480 s | 60 to 3600 s | | How long a suspicion may stay unresolved before it escalates to a fault. |
| `cool_temp_max` | 33 °C | 5 to 60 °C | 25 to 38 °C | Run ceiling: above it, hold. `60` turns the gate off (§8a). |
| `cool_temp_resume` | 31 °C | 5 to 59 °C | 20 to 36 °C | Resume gate: below it, continue. Always kept below the ceiling. |
| `cool_temp_critical_c` | 38 °C | 6 to 70 °C | 36 to 45 °C | Critical line: a fault with no resume in the job (§5). Kept above the ceiling while the ceiling is a gate (a ceiling at 60 leaves the line standing alone); `70` turns the gate off. |
| `cool_cooldown_s` | 15 s | 0 to 1800 s | | Smoke-clear phase at run duty after a job. |
| `cool_cooldown_max_s` | 300 s | 0 to 1800 s | | Cap on the thermal cooldown phase. |
| `cool_tach_exhaust_min_rpm` | 6400 rpm | 0 to 20000 | 5800 to 7000 | Exhaust fan floor at run duty (§3a). `0` turns the gate off. |
| `cool_tach_intake_min_rpm` | 2290 rpm | 0 to 20000 | 2100 to 2500 | Intake fan floor, either intake (§3a). `0` turns the gate off. |
| `cool_tach_air_assist_min_rpm` | 6000 rpm | 0 to 30000 | 5500 to 6600 | Air-assist fan floor (§3a). `0` turns the gate off. |
| `cool_purge_min_current` | 300 raw | 0 to 1023 | 150 to 500 | Purge-air fan current floor (the fan has no tachometer; about 1 off, about 630 on). `0` turns the gate off. |
| `cool_fan_grace_s` | 15 s | 0 to 120 s | 5 to 30 s | Spin-up window after the run profile is written, during which no floor counts. |
Two settings are deliberately not on the panel:
- `cool_fire_ir_delta`, the lid-IR fire gate (§7). It is `0`, watch-only, and
changing it by hand is not recommended until the watch is lamp-aware.
- `GFCOOL_*` environment overrides exist for bench work; they win for the
lifetime of the process and are not a normal operating path.
### 8a. Turning a gate off
The gates are settings, and the far end of a gate setting's range is the off
switch: a coolant ceiling of 60 °C never trips, a check window of 0 s runs
no flow verification at all, and a fan floor of 0 never trips. There is no other switch, and no list of names to
get wrong. The ranges are wide on purpose: the shipped defaults and the
recommended bands come from one bench machine, and a machine whose loop or
sensors read differently changes the number rather than waiting for new
firmware.
A gate that is off is not a gate that is forgotten. The panel flags any value
outside its recommended band beside the field and says "this gate is OFF" at
the far end; the Status tab shows a standing banner while any gate is off; the
engine logs one line per gate setting at every run start, and with the ceiling
off it still logs the first reading in a job that would have tripped the
default. `/status` and `/cool/status` carry the off gates as `gates_off`.
Nothing about it reaches the cloud service.
What no setting can reach: the hardware safety chain, the laser latch, the
emission witness, the lid-IR fire watch, the controller-silence dead-man, and
the motion-liveness gate. A machine with every thermal gate off still stops
firing the moment its controller goes quiet; what it no longer does is hold a
job for a stopped pump or an overheating loop. The banner says so.
---
## 9. Not implemented yet
Stated plainly so nobody counts on them:
- **Low-temperature gates and warm-up.** The factory holds a job and warms the
coolant when the loop is below roughly 16 °C, and refuses to fire at all near
freezing. ForgeFIRM does not yet; a cold-room machine will start cutting at a
temperature the factory would have waited out. Two settings — a hard floor
and a warm-up gate — are planned.
- **TEC control.** ForgeFIRM never drives the thermoelectric cooler. Presence
cannot be detected (the output has no readback), so this will become a user
setting plus a simple hysteresis around the factory's setpoints.
- **A fire watch that acts** (§7).
- **Chassis, SoC and supply ceilings.** Three temperatures are measured and
not gated: the chassis LM75 and the SoC die in degrees, the supply sensor
as a raw count, in `/status` as `temps` (with the kernel's CPU throttle
state beside them), on the Status tab, and ranged over every job in one
log line (`temps this job: ...`, naming a throttle if one happened). The
SoC already guards itself (the kernel throttles the CPU at 85 C and
powers the board off at 90 C on this part). A ceiling for each comes
from that record once there is enough of it. The supply's conversion
stays unverified by decision (its heatsink is not reachable with a
thermometer while the machine runs), so its reading stays a raw count
and any ceiling for it would be set in raw counts too.
- **Fan floors measured on more than one machine.** The shipped floors are
a fraction of one bench machine's run-duty speeds; a machine whose fans
read differently sets its own (§8), and a floor of zero turns that gate
off while it does.
---
## 10. Quick reference: what the machine does when
| Situation | Machine response |
|---|---|
| Idle | Pump on, purge air on, fans at idle, heater off, TEC off. |
| Job starts (or the laser arms) | Cut airflow, flow check requested once the loop is settled. |
| Flow check over limit, first time | `SUSPECT`: warning, hold, immediate re-check. |
| Second consecutive over limit | `FAULT`: fire gated, hold stands until you resolve it. |
| Suspicion unresolved past the budget | Escalates to `FAULT`. |
| Three cleared suspicions in one job | Aggregated "check your coolant" warning. |
| Upstream coolant above 33 °C | `OVERTEMP`: hold + forced cooling; auto-resume under 31 °C. |
| Upstream coolant at or over 38 °C during a job | `CRITICAL`: fire blocked, hold, no resume this job; the ceiling's hold stands until the loop is under 31 °C. |
| A fan under its floor inside the spin-up grace | Nothing yet: the gate reads `grace`. |
| A fan under its floor for three seconds after the grace | `AIRFLOW`: fire blocked, hold, no resume this job; fans held at run duty; the next job starts the gates fresh. |
| Purge-air current absent at run duty | `AIRFLOW`, the same way. |
| A gate setting at its off end (ceiling 60 °C, check window 0 s) | No verdict from that gate; a run-start log line, `gates_off` in `/status`, and a standing panel banner. |
| Job ends | 15 s smoke clear at run duty, then reduced airflow until the loop is under the resume gate. |
| Controller stops reporting | Fire blocked at once, stand-down through cooldown. |
| Silence while armed, or a program still playing | Motion stopped and the latch locked by the engine itself. |
| Verdict file missing or stale | The controller treats it as fire-blocked and holds. |
| Diagnostic running | Engine suspends its writes and publishes fire-blocked. |
| Engine gone while armed | Controller writes factory run duties once, holds, stands down. |
---
## See also
- [Motion and laser drive](MOTION.md) — arming, job phases, both controller modes.
- [Laser safety](SAFETY.md) — the hardware chain the beam actually passes through.
- [LightBurn setup & operation](LIGHTBURN.md) — `M8`/`M9` and air assist in practice.
- `forgectrl/docs/SERVICES.md` — the machine-services contract, including the
report and verdict channels in full.
-214
View File
@@ -1,214 +0,0 @@
# 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.** Opening the lid cancels the job (the beam is cut by
the hardware the same instant); LightBurn's Stop aborts it; the big button
pauses it. 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
laser-on command of a job the machine unlocks its laser latch,
lights the big button **white**, and pauses the incoming gcode until
you **press the button** (the same press the factory firmware
requires). LightBurn simply waits; press the button and the job
runs. If nobody presses within `laser_button_timeout_s` (default
300 s) the job aborts with alarm 3. Stop in LightBurn (soft reset)
cancels the wait at any time. Opening the lid (or a Pro's interlock
loop) while the button is lit cancels the job the same way — the
message names the reason, the latch relocks, and a press with the lid
open never arms; the job ends for LightBurn (a clean cancel, no alarm to
clear); close the lid and start again.
- One press covers one job — power changes and M5/M3 toggles do not
re-prompt. The window relocks when the job ends (program end
`M2`/`M30`), when the sender connection changes, or after
`laser_disarm_s` (default 60 s) with the spindle off — counting even
while a job sits paused in Hold or with the lid open; the next job
prompts again. Both timeouts are machine settings (keys in
`/data/forgefirm.conf`, set through the control panel's settings API);
the defaults suit normal use.
- S-value scale: `$30` defaults to 1000, so set LightBurn's S-max to
1000. 100 % power = S1000. Use M4 (variable/dynamic) mode for cuts
and engraves.
- The machine forces the cut fan profile on while armed and
continuously verifies coolant flow; a flow fault or over-temperature
pauses/blocks firing (messages appear in LightBurn's console).
- The hardware safety chain stands above all of this: lid open,
interlock open, or power faults make firing physically impossible
regardless of software state.
## One-time device setup
Prerequisite: the controller is running on the board (see BRINGUP.md;
`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: **`<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** — 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).
9. Optional backup: File → Export Devices → saves a `.lbdev` you can
re-import later (the format is not editable text; export is the way
to make one).
## Job start mode (important on an unhomed machine)
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
when you have not homed.
(`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
- **Frame** traces the job's bounding box at travel speed — do it
before every Start. There are **no limit switches**: framing is your
crash protection.
- **Start** runs the job. Travels run up to 200 mm/s; anything faster
in a layer is clamped by the controller ($110/$111 = 12000 mm/min).
- **Pause** = grbl feed hold: motion parks within ~0.4 s (0.2 s stream
queue + deceleration); Resume continues exactly. **The big button does
the same**: one press while a job runs pauses it (LightBurn shows Hold),
the next press resumes it — the factory's pause/resume, on the machine.
Two things to know before you pause a cut. **Resume where it stopped:**
there is no backtrack in GRBL mode, so the beam restarts from where the
deceleration ended and accelerates away from a standstill — at constant
power (`M3`) that leaves a deeper spot you can see, while `M4` scales power
with speed and mostly hides it. **Don't leave it paused:** the armed window
has an idle grace (`laser_disarm_s`, default 60 s) that counts down through
a hold, so a job left paused disarms itself and the resume asks for the
button again before it can fire.
- **Opening the lid (or a Pro's interlock loop) during a job cancels it**,
as the factory firmware does: the head parks with a controlled
deceleration (the hardware cut the beam the instant the lid moved), the
console reports the reason, the job ends for LightBurn (the controller
resets — position is kept, no alarm), and the head returns on its own to
where the job started, lid open or not. Close the lid and start again
from LightBurn; the next job asks for the button, which is also what
re-arms the machine's hardware button latch. The `lid_policy` setting on
the control panel's GRBL tab can select the stock Grbl behavior instead
(Door hold, Resume once closed). At idle, while jogging, or during homing
the lid is yours to open and close freely — the controller does nothing
there (the hardware blocks the beam anyway), so a lid cycle while loading
material never leaves LightBurn waiting.
- **Stop** = soft reset: motion aborts with a controlled deceleration
and grblHAL raises an alarm with **position declared lost** (the
stream queue means up to ~40 mm of in-flight difference). It leaves the
head where it stopped — the return to the job start belongs to the lid
and interlock policy alone, so Stop never moves the machine on its own.
Recovery: unlock (`$X` in Console or LightBurn's prompt), jog the head
clear, and carry on in Current Position mode. Restart the controller with
the head re-parked if you want a clean absolute frame.
- **Move tab**: jogging (set a sane speed, e.g. 6000 mm/min), Get
Position, distance buttons.
- **Console tab**: raw grbl — `?` status, `$$` settings, `$X` unlock,
`$J=G91X10F1200` jog.
## Power
The controller drives the tube the way the factory does: every pulse
fires at full power, and the power setting decides how many ticks of
each 710 us period fire. Every power level marks, low levels included,
because no pulse is ever too weak to strike. The raw response is not
linear - 80 % of the pulses deliver about half the light - so the
controller maps your power setting through a measured dose curve: 50 %
commands half the light, not half the pulses. The machine ships with
the bench-measured curve; record your own tube's from the control
panel (GRBL tab, "Dose-curve recorder": download the ladder file, press
Record, run the file from LightBurn on scrap, press the button, Apply
the fit). Grayscale images fade cleanly into the shadows (a low level
becomes sparse full-power pulses), and 254 to 508 DPI rasters hold
their tonal steps.
`$35`, the power floor, is set by the controller from the machine config
(the control panel's GRBL tab, "Laser dose"): do not type it, it is
overwritten at every job.
## Air assist / fans
Each cut/engrave layer has an **Air Assist** toggle (in the layer's cut
settings). Turning it on makes LightBurn emit M8/M9 around that layer,
which drives the machine's full cut-profile ventilation: air assist to
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). 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 **20 %** (any nonzero power marks under the default power
model; 20 % is a light pass on scrap), turn the layer's **Air Assist**
on, close the lid, **Frame**, **Start**, and press the white button
when it lights. Watch the whole job.
-452
View File
@@ -1,452 +0,0 @@
# Motion and laser drive
Everything the machine does physically — every step of the gantry, every lens
move, every laser pulse — comes out of **one stream of bytes** played by
hardware at a fixed rate. This page explains that stream, why the laser is part
of it rather than beside it, and how the two controller modes (GRBL and cloud)
feed it.
You do not need any of this to run a job. It is here so that what the machine
does makes sense, and so the settings you can change mean something.
- To cut from LightBurn, see [LightBurn setup & operation](LIGHTBURN.md).
- For the safety chain that gates the beam, see [Laser safety](SAFETY.md).
- For fans, pump and coolant, see [Cooling and airflow](COOLING.md).
---
## 1. The pulse stream
The control board does not decide, moment by moment, when to move a motor.
Instead a hardware timer (EPIT) fires at a fixed **machine tick**, and a DMA
engine (SDMA) hands the next byte of a prepared stream straight to the GPIO
register that drives the stepper and laser lines. No software runs between the
timer and the pins.
That is what makes motion smooth: step timing cannot be disturbed by a busy
CPU, a camera stream, a network client, or a garbage collector. The worst a
loaded system can do is fail to supply bytes fast enough — and that case is
detected and treated as a fault rather than as silent damage.
### One byte per tick
Each byte covers exactly one tick. If the top bit is clear, the byte commands
steps and fire; if it is set, the byte sets laser power.
| Bit | Meaning |
|---|---|
| 0 | X step |
| 1 | X direction (set = −X) |
| 2 | Y step |
| 3 | Y direction (set = +Y; the two Y motors are driven complementary) |
| 4 | **Laser fire during this tick** |
| 5 | Z step |
| 6 | Z direction (set = lens up, away from the bed = +Z) |
| 7 | 0 = step byte · 1 = power byte (low 7 bits are the power level) |
**Speed is density, not clock.** The tick rate never changes inside a job.
Going faster means setting a step bit in more of the bytes; going slower means
spacing them out. A move is planned in the usual way — acceleration, cruise,
deceleration — and then resampled onto this fixed grid.
Two consequences worth knowing:
- **Resolution is bounded by the tick rate.** At the default GRBL machine tick
of 28160 Hz, one axis can take at most 28160 steps per second — about
528 mm/s, comfortably above the machine's 200 mm/s top speed.
- **There is a hardware ceiling.** The playback script needs about 6 µs per
byte, so beyond roughly 165 kHz the timer outruns it. Ticks are chosen far
below that.
### The ring, and two ways to fill it
Pulse bytes go into a 32 MiB ring buffer in reserved memory, the same size the factory firmware uses. There are two ways
to use it, and the mode you run decides which:
- **Live streaming (GRBL mode).** The controller keeps only a small window of
the job in the ring — a fraction of a second — and refills it continuously
while the job plays. A write that would overflow is refused, and the feeder
backs off; that is normal flow control, not an error. If the feeder ever
falls behind far enough to empty the ring, the machine enters an **underrun**
state: motion stops instantly, and position is no longer trusted.
- **Preloading (cloud mode).** The whole job is written into the ring before it
starts. Nothing can starve, but the ring size caps job length: roughly
1 MiB per 100 seconds at the cloud's 10 kHz tick, so about 56 minutes. A job
larger than the ring is rejected cleanly before it runs.
### Stopping and resuming at the hardware level
The pulse engine itself offers three ways out of a running program, and both
modes are built on them:
- **Controlled stop** — the tick rate ramps down at a set rate (125000 Hz/s by
default) until motion halts. No steps are lost, so position stays accurate.
This is what a feed hold, a jog cancel, a lid-open cancel and a soft reset
all use.
- **Halt** — an immediate stop with no ramp. Steps can be lost; used only for
emergencies.
- **Resume with a waypoint** — from a controlled stop the program can be
resumed a chosen number of steps backward (laser forced off) or forward. This
is how the factory's pause-and-resume works, and cloud mode uses it. It is
only available for a preloaded job: a live-streamed ring no longer holds the
bytes to back into, and the kernel refuses the request.
Whenever a stream ends — normally or by starvation — the playback script drives
the fire and step lines low as a hardware backstop.
---
## 2. Laser drive is part of the motion stream
The laser is not a separate subsystem that gets told "on" and "off" while
motion happens elsewhere. **Power and fire ride the same bytes as the steps**,
on the same grid:
- A **power byte** (top bit set) sets the PWM duty of the laser drive: 7 bits
written straight into the hardware PWM against a 127-count period, at a
carrier near 40 kHz. 127 is full power.
- The **fire bit** (bit 4) requests emission for that one tick, and only that
tick.
Because both travel with the steps, power and position cannot drift apart. A
power change lands at exactly the point along the path where it was planned,
regardless of what the rest of the system is doing.
Three rules follow from the hardware, and both controllers obey them:
1. **Power before fire.** Starting a program resets the duty to about 100 %, so
a stream must set power before its first fire bit — otherwise the first
pulses would fire at full power.
2. **No two power bytes in a row.** The playback script applies the first of a
run of power bytes and discards the rest, so power changes are spaced by at
least one step byte.
3. **End dark.** Every stream ends with fire clear; the end-of-data backstop is
the safety net, not the mechanism.
Also worth knowing: **the duty setting persists after a program ends.** The
laser-off guarantee rests entirely on the fire bit and the hardware chain, never
on power being zero.
### What actually lets the beam out
The fire bit is a *request*. Emission additionally requires the hardware safety
chain — lid switches, the remote interlock loop, HV good, supply rails, the
charge-pump watchdog the kernel feeds only while a program is playing, and the
physical button latch — to agree. On top of that, ForgeFIRM keeps the kernel's
**laser latch** locked except inside an operator-armed job window (§5.4), and
the kernel relocks it whenever the pulse device is closed.
Fire only ever rides motion segments of laser blocks. Jogs, rapids and homing
are fire-free by construction, not by convention. See [SAFETY.md](SAFETY.md)
for the chain itself.
---
## 3. Geometry, speeds and limits
| Property | Value |
|---|---|
| X/Y resolution | 0.15 mm per full step, ×8 microstepping → 53.333 µsteps/mm |
| Z resolution | 0.3534 mm per half-step → 2.832 half-steps/mm |
| Work area | 495 × 279 mm |
| Z travel | about 10.6 mm (0.417"), hall-referenced at the top |
| Max X/Y rate | 12000 mm/min (200 mm/s) |
| Max Z rate | 300 mm/min |
| Acceleration | 700 mm/s² X, 590 mm/s² Y, 50 mm/s² Z |
| Laser PWM carrier | 39.98 kHz, 7-bit duty |
Origin is the **back-left** corner, and the workspace is all-positive from
there. **+Y moves the gantry toward the front of the machine.** Z counts
positive upward, away from the bed.
Z is never driven blind: the lens carriage is referenced against a hall sensor
at the top of travel, and moves are supervised against it.
The machine has **no limit or home switches** as it ships. What that means in
practice — how each mode establishes an origin, and how the machine behaves
without one — is in §5.7 and §6.3.
---
## 4. Who owns the motion hardware
`forgectrl`, the machine-services daemon, owns the pulse device for as long as
it runs and hands the open connection to whichever controller is active. Only
one controller — GRBL or cloud — runs at a time, and switching between them is
a live operation from the web panel.
Two behaviors follow from this that you will notice:
- **The 40 V motor rail stays up while the machine is on.** Handing the device
from one controller to another never cycles it. The stepper drivers on this
board can latch into an unserviceable state on a rail glitch — the position
counters keep counting while the motors produce nothing — so the rail is left
alone.
- **The machine proves it can move before the first job of a session.** Before
the first controller start, forgectrl makes a short test move (always to the
right first — a cable lives at the left end of travel) and confirms it with
the accelerometer in the print head. If it sees no motion it powers the rail
down and retries with progressively longer off periods; if the drivers still
will not wake, it reports a **motion fault** instead of starting a
controller, and the panel offers a retry. Position counters advancing are
never accepted as proof that the machine moved.
---
## 5. GRBL mode
GRBL mode turns the machine into a standard Grbl-speaking laser cutter. It is
the default and the one to use for your own designs.
### 5.1 Connecting
The controller speaks **Grbl 1.1 over TCP port 23**. Point LightBurn, UGS,
cncjs or any Grbl sender at the machine's address on port 23. Setup details and
a first job are in [LIGHTBURN.md](LIGHTBURN.md).
Only one sender at a time is meaningful. Opening a second connection displaces
the first — which is also why the web panel reads position from the machine's
own counters and never from the Grbl socket.
### 5.2 From G-code to pulse bytes
1. Your sender streams G-code over TCP.
2. grblHAL parses it and plans motion in the usual way: look-ahead, junction
deviation, acceleration ramps.
3. A producer thread runs the planner's step generator against a virtual clock
a thousand times finer than the machine tick and places each step event on
the byte grid.
4. A high-priority shipper thread writes due bytes to the pulse device roughly
every 10 ms, keeping a bounded queue ahead of real time.
The queue depth is the trade: deeper means more immunity to system load,
shallower means a feed hold or a power override takes effect sooner. The
default is 200 ms, and the machine tick defaults to 28160 Hz — the same tick
the factory firmware uses for travel moves.
### 5.3 Laser mapping
- `$32` (laser mode) is **on by default**, so `M3`/`M4` and `S` behave the way
senders expect. `M4` gives dynamic power scaled with speed through
acceleration ramps; `M3` gives constant power.
- `$30` is 1000, and S values map linearly onto the 7-bit power byte —
`S1000` = full power, `S500` ≈ half.
- Power changes are emitted ahead of the tick they apply to, so a power change
and the motion it belongs to stay together.
### 5.4 Arming: the button press is part of every job
The first laser-on of a job does not fire. Instead the controller:
1. **Checks the coolant verdict.** If a flow fault or an over-temperature
condition stands, arming is refused outright ([COOLING.md](COOLING.md)).
2. **Checks that a print head is present.** No head, no arming.
3. **Forces the cut airflow profile on**, so every fire window is covered by
running fans and active flow verification.
4. **Unlocks the kernel laser latch, lights the button white, and pauses the
job** — the sender keeps getting status reports, so it does not time out —
until you press the physical button.
A press with the lid open does not arm; the hardware button latch would not
clear on it either. A soft reset, or a lid or interlock open, cancels the job
instead. If nobody presses within `laser_button_timeout_s` (default 300 s), the
job ends in an alarm with the latch relocked. The coolant verdict is re-checked
after the press, so a window can never open against a fault that appeared
during the wait.
**The window is per job, not per fire.** It survives `S` changes and `M5`/`M3`
toggles, so nothing re-prompts mid-job, and it closes — relocking the latch —
when any of these happens:
- program end (`M2`, `M30`, `%`) — the normal case, within the cycle;
- the sender's connection changes (the consent belonged to that session);
- `laser_disarm_s` (default 60 s) of spindle-off idle, counted down in Hold,
Door and Tool Change as well as Idle;
- immediately on alarm, homing, reset, or a stream fault.
### 5.5 Pausing, stopping and faults
| You do | What happens |
|---|---|
| Feed hold (`!`) | Controlled ramp to a stop, position kept, laser off. The disarm grace keeps counting. |
| Cycle start (`~`) | Resumes from the hold. A live-streamed job cannot back up, so the cut resumes where the deceleration ended. |
| Jog cancel (`0x85`) | Controlled stop, jog abandoned, position kept. |
| Soft reset (`^X`) | Controlled deceleration into Alarm, latch relocked, machine position retained; `$X` clears the alarm. |
| Press the button mid-job | Pause; press again to resume (§5.6). |
| Open the lid or the interlock loop | The job is **canceled**, not paused (§5.6). |
| Ring runs dry (underrun) | Motion stops instantly. While armed this is a hard fault: alarm, latch relocked, position invalidated — re-home before trusting coordinates. A motion-only job gets one sanctioned retry. |
| Coolant fault or over-temp | Feed hold with cut airflow forced on; fire is gated. Over-temp resumes automatically once the loop recovers. |
| Controller crash or hang | The daemon stops motion and relocks the latch, then restarts the controller. |
### 5.6 Lid, interlock and button
ForgeFIRM reproduces the factory machine's behavior:
- **A lid or interlock open during a job cancels it.** Motion stops within
milliseconds of the switch edge, the job is not resumable, the latch relocks,
and the head returns to the position the job started from — **with the lid
still open**, exactly as the factory does. The return-home move always runs
to completion.
- **The button pauses and resumes.** In GRBL mode a press is a feed hold and
the next press is a cycle start. A pause is not a cancel: the armed window
stays open across it.
- **Idle lid cycles are ignored.** Opening the lid to load material, or
powering up with it open, does not leave the controller parked — senders
connect normally.
- **Jogs are not lid-gated.** The core is blind to the door signal while it is
idle, jogging or homing, so a jog both starts and runs with the lid open —
the beam is blocked in hardware regardless.
- **Homing is lid-gated in practice**, even though the core does not see the
door during `$H`. With `homing_mode = gfcloud` — the only method that works
today — the cycle is a cloud homing session (§5.7), and its move to the home
corner is an ordinary motion action: refused with the lid open, and stopped
if the lid opens partway through. The camera steps need the lid closed
anyway. Only the lens/Z **hunt** inside that session ignores the lid (§6.3),
which is where hunts happen in GRBL mode — there is no hunt outside a cloud
homing session. Under `homing_mode = switches` a Z reference would just be
part of the core homing cycle.
The next job re-arms with a fresh button press — the same press the hardware
button latch itself requires, which is why software and hardware cannot
disagree about whether the machine is armed.
If you prefer stock Grbl door behavior, set `lid_policy = hold`: the job parks
in the Door state and a cycle start after the lid closes finishes the move with
its position intact.
### 5.7 Homing, and running unhomed
The homing method is a setting (`homing_mode`), chosen in the web panel:
- **`gfcloud`** — camera homing through the Glowforge web service, the same
cycle the factory machine runs. `$H` suspends the stream engine, runs the
session, then hands the machine back. Takes roughly a minute and uses the
machine's builtin credentials.
- **`switches`** — the future limit-switch cycle. Not enabled yet; brackets for
the switches are in the project's `3d-models/` directory.
- **`none`** — `$H` is rejected.
**The machine cuts fine unhomed.** Without a reference, coordinates are
relative to wherever the head happened to be, so the panel shows position in
red to say so, and your sender should use a job-start mode that does not depend
on machine coordinates. After a successful home the position is anchored and
shown normally.
Anything that invalidates position — an underrun, a stream fault — drops the
anchor deliberately, so a stale origin cannot be reused.
---
## 6. Cloud mode
Cloud mode runs the factory experience: the Glowforge app and web service, the
camera bed image, the lens hunt, "push the button to print". It is kept and
maintained on purpose. Behavior specific to the service — actions, events,
credentials — is in the cloud-mode documentation (`python3-gfhardware/forgefirm-app/docs/CLOUD.md`).
### 6.1 What is different about the motion path
In cloud mode the machine does not plan anything. The service sends a
**precomputed pulse file** — already resampled to the byte format described in
§1 — which the client downloads, writes into the ring, and plays:
1. The service issues a print action with a URL for the motion data.
2. The client downloads it and validates the header before a byte reaches the
ring. A job larger than the ring is refused cleanly.
3. The header's own parameters are applied: the machine tick (10 kHz for prints
and hunts), the acceleration ramp, and the per-job fan duties, which are
passed to the cooling engine as the run profile.
4. The button wait arms the laser, exactly as in GRBL mode.
5. The ring plays to the end; the client supervises it and reports state.
Because the whole job is preloaded, there is no feeder to starve — but there is
also no live re-planning, and job length is capped by the ring.
### 6.2 Pause, cancel and park
- **The button pauses and resumes a print**, and here it does so exactly as the
factory does: a press stops motion under control and then backs the stream up
2000 ticks with the laser off; the next press runs forward and re-enables the
laser after a 1950-tick lead, so the resumed cut overlaps the material
already burned instead of starting cold. Both counts are settings
(`cloud_pause_backtrack_ticks`, `cloud_resume_lead_ticks`). Motions and
hunts do not pause.
- **A lid or interlock open, or a cancel from the app, ends the job.** Motion
stops, whatever remains in the ring is dropped so nothing can play later, and
the head parks back at the job's starting point — ignoring the lid, as the
factory does. The job is reported as canceled.
- **The service dead-reckons position**, so the park after every print,
finished or aborted, matters: cutting it short would offset everything until
the next camera home. That is why the park ignores the lid and the cancel
flag.
### 6.3 Homing and hunts
Cloud homing is camera-based: the service takes a lid image, moves the head,
takes another, and computes where it is. The lens hunt references Z against the
hall sensor. Hunts are not lid-gated. Connecting zeroes the machine's counters
at the head's current position, so GRBL-mode coordinates do not survive a
switch to cloud mode and back — re-home after switching.
---
## 7. The two modes side by side
| | GRBL mode | Cloud mode |
|---|---|---|
| Who plans motion | grblHAL on the machine | the Glowforge service |
| Input | G-code over TCP:23 | a downloaded pulse file |
| Ring use | live-streamed, small window | whole job preloaded |
| Machine tick | 28160 Hz default | 10 kHz (from the job header) |
| Job length limit | none | ~56 minutes (ring size) |
| Needs internet | no | yes |
| Laser arming | button press per job | button press per job |
| Button mid-job | feed hold / cycle start | pause with backtrack / resume with lead |
| Lid or interlock open | cancel + return to job start | cancel + park at job start |
| Homing | `$H` (camera or, later, switches) | automatic, camera-based |
| Fan control | `M8`/`M9` plus the armed window | per-job duties from the job header |
| Underrun possible | yes (handled as a fault) | no (nothing is streamed) |
Only one mode runs at a time. Switch from the panel's Status tab; the switch is
allowed only when the machine is idle.
---
## 8. Settings that affect motion
Machine settings live in the web panel and are stored on the machine. They can
only be changed while the machine is idle.
| Setting | Default | Effect |
|---|---|---|
| `controller_mode` | `grbl` | Which controller runs: `grbl` or `cloud`. |
| `homing_mode` | `gfcloud` | What `$H` does: `gfcloud`, `switches`, `none`. |
| `gfcloud_home_x/y/z` | 0 / 0 / Z max | Coordinates assigned after a successful camera home. |
| `gfcloud_home_timeout_s` | 300 | How long a homing session may take before it alarms. |
| `lid_policy` | `cancel` | `cancel` = factory behavior; `hold` = stock Grbl door parking. |
| `laser_button_timeout_s` | 300 | How long the machine waits at the button prompt. |
| `laser_disarm_s` | 60 | Spindle-off grace before the armed window closes. |
| `laser_floor_density` | 10 | The S-range floor, percent of full: the lowest pulse density that still marks. Loaded into `$35` at every spindle precompute; `$35` is derived, never typed. |
| `laser_dose_curve` | (bench default) | The measured dose curve as density:light percent pairs; S commands a light fraction and the driver maps it onto the density that delivers it. `off` = identity; a bad value falls back to the default. The panel's recorder measures and applies a machine's own. |
| `laser_corner_gamma` | 2 | The corner rolloff under M4: delivered light follows (v/v_programmed)^gamma, so 1 is plain proportionality and higher values starve the slow spots where heat accumulates. Rides the curve. |
| `laser_pulse_ticks` | 20 | Density base period in machine ticks (35.5 us each). |
| `laser_pulse_min_ticks` | 3 | Shortest density pulse in ticks; below it a period is skipped and its debt carried. |
| `rail_settle_s` | 2.5 | Motor-rail off period when a controller takes the device standalone. |
| `cloud_pause_backtrack_ticks` | 2000 | Cloud pause: laser-off backtrack after the stop. |
| `cloud_resume_lead_ticks` | 1950 | Cloud resume: laser-off lead before firing again. |
Grbl `$` settings (steps/mm, rates, accelerations, laser mode) are set through
your sender in the usual way; the defaults above are baked in from the factory
machine's own measured values. If you change a baked default and it does not
appear to take, remember that stored settings win — `$RST=$` restores the
defaults.
---
## See also
- [LightBurn setup & operation](LIGHTBURN.md) — practical sender setup.
- [Laser safety](SAFETY.md) — the hardware chain and what each interlock does.
- [Cooling and airflow](COOLING.md) — the fire gates referenced above.
- `kernel-module-glowforge/UAPI.md` — the pulse-stream contract in full detail.
- `forgectrl/docs/SERVICES.md` — device ownership, mode supervision, switch map.
-316
View File
@@ -1,316 +0,0 @@
# 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.
![Laser safing chain: lid switches, charge-pump watchdog, button latch, interlock latch and FIRE combine into HV_ENABLE and LASER_ON](img/safety-chain.svg)
### 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/glowforge` relocks.** 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/glowforge` open with `flock
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_circuit` bits 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_on` says 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.
- **Dose model.** Density is the only model: every pulse fires at full
power and the commanded level only masks FIRE ticks the core asked for,
never adds one, so emission stays exactly where the core commanded it.
The analog rendering (continuous FIRE at a duty) is not selectable on a
machine - it fires the tube's strike transient as a spot at every
beam-on - and exists only as the host harness's conservatism reference.
- **Coolant fire gates.** The armed window requires a fresh `fire_ok` verdict
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) and `interlock` (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 — with `lid_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 = hold` keeps the stock door hold (once the door/loop
closes the controller reports `Door:0` and 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_analog` on the head is the live emission witness.
### 3.3 forgectrl (machine services)
- Holds `/dev/glowforge` for 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=1` itself.
- 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".
- `/status` reports the switch map and `laser_locked` (`interlock_circuit`
bit 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_enable`
0 — the lock severs the FIRE drive entirely.
- Latch **unlocked, chain unarmed** (no button press): `laser_enable` 1
mid-window, PSU pin flat, `laser_on` 0 — 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 with `charge_pump_alive` on both edges (HV_ENABLE =
DOORS_OK · WDOG_ALIVE observed).
- Interlock latch drive: with the connector unjumpered, `interlock`,
`interlock_latch_reset` and `interlock_latch` all 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.
-289
View File
@@ -1,289 +0,0 @@
# ForgeFIRM install, update & recovery system
Design and contracts of the ForgeFIRM install/update/recovery system:
the factory's own A/B slot scheme, signed `.fw` packaging, the GUI
update manager, factory restore, and the recovery image. The system is
described in the implementation units ("phases") it is built from. The
measured ground truth this rests on lives in `BRINGUP.md` (eMMC layout,
boot0/boot1 maps, saved-env location, factory `.fw`/updater internals —
"eMMC boot & recovery architecture"), which also carries what is still
open; the bench record of each phase is in `CAMPAIGN-LOG.md`.
## Settled decisions
- **Factory partition scheme, unmodified**: ForgeFIRM lives in the two
200 MiB rootfs slots (`mmcblk2p1`/`p2`); `/data` (p3) keeps its full
factory size. No repartitioning at install, ever.
- **Single-OS, not dual-boot**: a machine runs ForgeFIRM *or* factory
firmware, with a clean migration each way. The factory updater's
behavior toward foreign slot contents is irrelevant because we never
operate both long-term.
- **fwup is the universal package/apply format** (the factory's own
mechanism): ForgeFIRM upgrades, factory restore, and provisioning all
use signed `.fw` archives applied to the inactive slot, followed by a
U-Boot env flip — exactly the factory update flow.
- **Factory firmware is archived to `/data` before any factory slot is
overwritten.** Restore-to-factory never depends on Glowforge's
servers; the cloud path (`GET /update/current`, already implemented
in gfutilities) is the optional "restore to *latest*" upgrade.
- **Release artifacts are built and signed locally**, uploaded as
GitHub releases. The Ed25519 private key never leaves the build
host, so GitHub is untrusted hosting: machines verify signatures
before applying. CI does compile checks only, never artifacts.
- **Reinstalling ForgeFIRM from factory = run the installer again**,
until the recovery refresh (Phase 5) subsumes it.
- **Recovery refresh is squashfs-only in v1**: factory U-Boot, DTB and
the 3.14.28 recovery kernel stay in place; only the recovery
userspace is replaced.
## Invariants (every flash path, every phase)
1. Never write the active (running) slot.
2. Machine idle; one flash operation at a time (lock file); no
flash/reboot while a job runs.
3. Archive factory content before the write that would destroy the
last copy of it (rootfs slots; boot0/boot1 before a recovery
refresh).
4. Env flips are atomic: one `fw_setenv -s` transaction setting all
four of `mmcdev`/`mmchwpart`/`mmcpart`/`mmcroot`.
5. Automatic paths (release updater, cloud restore) require a valid
signature — ours or Glowforge's respectively. Manual uploads may be
unsigned behind an explicit "unsigned dev image" warning.
6. The image must fit the 200 MiB slot; the build fails past the size
gate rather than producing an unflashable release.
7. Boot selection refuses targets that fail the content probe (no
kernel / no recognizable rootfs).
8. Verify a written slot (fwup on-the-fly hashes, or an explicit
readback/mount check for raw writes) before flipping boot to it.
## Phase 0 — enablers (no eMMC flashing)
- **0.1 Slot-agnostic images.** Goal: one image boots unmodified from
p1, p2, or SD, steered only by the saved env (U-Boot's `mmcargs`
already takes `root=${mmcroot}` from the env). Audit what our
`/boot/uEnv.txt` currently sets; strip it to entries that are not
per-location (`fdt_file` etc.); bench-verify by flipping env alone.
This removes the mount-and-sed step from every flash path.
*Exit: the same built image boots from two locations with no
per-slot edit.*
- **0.2 fwup toolchain + keys.** Yocto recipe for fwup (target) and a
host-side pack step. Generate the ForgeFIRM Ed25519 keypair
(custody: offline on the build host, passphrase-protected, backed
up). Compatibility gates, both directions: (a) a `.fw` we pack must
apply with the **factory's** fwup 0.14.2 (the installer runs on
factory firmware; fall back to shipping a static armv7 fwup with the
installer if archive-format drift bites), and (b) our shipped fwup
must apply a **factory** `.fw` verified against the GF pubkeys
(carried from the factory image) for cloud restore.
- **0.3 Build outputs.** `forgefirm-image` additionally emits the raw
ext4 rootfs and a packed+signed `forgefirm-<ver>.fw` with
`upgrade.a` / `upgrade.b` tasks in the factory pattern
(partition-relative raw writes, unmounted-destination +
on-the-fly-verify options); size gate enforced here. A `complete`
full-provisioning task joins with the Phase 5 recovery work. The wic
stays for SD/dev burns.
## Phase 1 — ffboot v2 + slot probe
- Atomic env flip (invariant 4) — fixes the existing gaps: three
separate `fw_setenv` calls today, and `mmchwpart` never set (relies
on the saved 0).
- `ffboot -l` (or a sibling tool): inventory every candidate — eMMC
p1/p2, legacy p4, SD — by read-only mount: factory `/etc/version` or
`/etc/forgefirm-version`, kernel presence; plus the current env
selection. Machine-parsable output; this is the probe the GUI and
the installer both reuse.
- *Exit: bench-verified flips SD ↔ eMMC slots; inventory correct for
factory / ForgeFIRM / empty slots.*
## Phase 2 — slot installer (factory → ForgeFIRM)
Rewrite `install-forgefirm.sh` as a **single-stage** script run from
factory firmware:
1. Sanity: factory 3-partition layout, both slots 200 MiB, active slot
detected (`rdev`), enough `/data` space.
2. Archive: **every factory slot version** not already archived —
`dd | gzip` to `/data/forgefirm/archive/factory-rootfs-<ver>.img.gz`
with a manifest line (slot, version, date, md5); also dump
boot0/boot1 (32 MiB) into the archive now, ahead of Phase 5. With
both slots archived, any later overwrite needs no second archive
step.
3. Fetch `forgefirm.fw` from GitHub releases (fixed asset name — the
`releases/latest/download/` URL needs one; the version lives in the
fwup metadata and the release tag), or take a local file argument
for offline/dev installs. Verify the signature against the
ForgeFIRM pubkey embedded in the installer (raw 32-byte form for
the factory's fwup; a dev key until the production ceremony).
4. Apply to the **inactive** slot (fwup + our pubkey). The booted
factory install stays bootable in the other slot.
5. Atomic env flip (embed the flip logic — the factory rootfs has no
ffboot v2), reboot.
No repartitioning, no `/data` backup/restore dance, no stage 2.
Rewrite `INSTALL.md` accordingly (serial console procedure stays).
*Exit: a factory machine converts in one pass; `ffboot` returns it to
the intact factory slot; `/data` (calibration, credentials, logs)
demonstrably untouched.*
## Phase 2b — legacy p4 migration
- Boot-time init script (before `/data` mounts), gated on: booted from
`mmcblk2p1`/`p2` (never SD, never p4) AND legacy geometry present
(p4 exists, or p3 ends short of the disk). Actions: delete p4,
extend p3's end to the disk (starts unchanged), `resize2fs`.
Idempotent and power-safe: every step keyed off actual disk state,
re-runnable after interruption.
- Existing p4 users reach the new scheme by running the new installer
from their running ForgeFIRM (same flow as Phase 2; both factory
slots intact → archive newer, overwrite older), then the boot-time
check reclaims p4/p3 on first slot boot.
- *Exit: a legacy-layout machine migrates with `/data` contents intact
and grown to full size; re-boot is a no-op.*
## Phase 3 — release pipeline
- `scripts/release.sh` (build host): gates → kas build → pack `.fw` →
sign → `sha256sums.txt` → staged assets + `gh release create`
command (`--publish` runs it where gh is authenticated). Gates:
clean tree, version single-source, rootfs-vs-slot size
(warn ≥ 170 MiB / fail ≥ 195 MiB, under bitbake's own hard cap),
**installer-embedded pubkey must match the signing key**,
factory-era fwup (0.14.2) verification of the packed archive, and the
**release acceptance gate**: `releases/v<version>/acceptance.json`
(exported by forgetest on the bench) must authorize the built rootfs
per the site (Developers, "Acceptance") - the gate recomputes every catalog test's
domain fingerprint from `/etc/forgefirm-manifest.json` inside the
release ext4. The artifact is attached to the GitHub release.
- One version source: `FORGEFIRM_RELEASE` = git tag =
`/etc/forgefirm-version` = `.fw` meta-version; the script enforces
agreement.
- Cloud-mode compatibility baseline: the cloud client's connect-time
probe records `{latest_gf_version, tested_against_gf}` to
`/data/forgefirm/gf-latest.json`, and forgectrl's panel warns when
the live Glowforge service has moved past the tested version (cloud
mode may break). `tested_against_gf` is the cloud client's configured
firmware version (`FACTORY_FIRMWARE.FW_VERSION`, the same value it
advertises to the service); it is **not** release metadata — neither
`release.sh` nor the `.fw` meta carries such a field.
- `release.sh --dev` packs a **dev-key-signed** `forgefirm-dev.fw`
from the release rootfs for the GUI upload path (decides open
question 4: dev archives are signed with the dev key, never
unsigned — the GUI exercises the same verification path either
way).
- GitHub Actions: per-push compile checks for grblHAL-glowforge and
forgectrl (minutes, no Yocto); optional `workflow_dispatch`
cold-Yocto reproducibility build whose only product is a checksum.
## Phase 4 — forgectrl update manager (GUI)
Endpoints in `forgectrl/src/update.c`, driven from the panel's System
tab; trust anchors in `/etc/forgefirm/keys` (`forgefirm-keys` recipe:
the release pubkey + the Glowforge keyring). Release version resolves
from the fixed-name asset redirect (`.../releases/latest/download/forgefirm.fw`
→ `.../download/v<ver>/...`), so no GitHub API / rate limits. All slot
writes run on one background job (polled `/update/status`), take the
installer's `/data/forgefirm/update.lock`, require idle + no diagnostic,
refuse the booted root slot, verify signature before writing, and
re-verify the written filesystem. `GET /slots` inventory, `POST /boot`
(probe-gated), `POST /update/{check,download,apply,upload}`,
`POST /restore/factory` (archive md5 checked), `POST /system/reboot`.
Every state-changing call is behind forgectrl's auth layer (bearer
token + origin checks; unsigned installs additionally require the
physical button held).
Functions of the panel page:
- **Inventory**: slot contents (Phase 1 probe), current/next boot
selection, archive presence/version.
- **Update check** against the GitHub releases API (manual button +
periodic while idle; offline-tolerant, rate-limit friendly).
- **Apply release**: download `.fw` to `/data`, verify signature,
apply to inactive slot, verify, then flip only on explicit user
confirmation, prompt reboot.
- **Upload**: streamed multipart to `/data` (never RAM-buffered);
accepts `.fw` (verify; warn if unsigned) and `.wic.gz`/`.ext4.gz`
(dev; size + superblock sanity checks).
- **Boot selector** incl. SD, with warnings — most prominently on
switch-to-factory: the factory updater may auto-update and overwrite
the other slot. Refuses unprobeable targets.
- **Factory restore**: from the `/data` archive (offline) or cloud
latest (gfutilities device auth → GF-signed `.fw` → verify with GF
pubkeys) → inactive slot → flip. Optional cleanup of ForgeFIRM
residue in `/data` for true factory condition.
- Interlocks throughout: idle-only, update lock, never the active
slot, rollback = flip back to the previous slot.
*Exit: full loop on the bench — GUI upgrade, rollback via boot
selector, factory restore and return — without touching a shell.*
## Phase 5 — recovery refresh (not yet built)
- **v1 scope**: replace only the boot0 recovery squashfs (boot1 `/usr`
only if needed). Never write below offset 0xC0000 in boot0 — U-Boot
is physically untouchable by the refresh tool. Factory DTB and
kernel 3.14.28 stay.
- Userspace: static busybox + fwup + a small C webapp (ulfius) +
hostapd/wpa_supplicant. No Python. Must carry 3.14.28-matched WiFi
modules (decision gate: lift from the factory recovery vs rebuild
from Glowforge's published GPL kernel source).
- Functions: button-hold → AP + web UI (factory UX): upload a `.fw`
(verified against our **and** GF pubkeys — either firmware
installable), install from the `/data` archive, set boot target,
export logs.
- Flash tool: boot0/boot1 archived first (Phase 2 already does),
`force_ro` unlock, write high regions only, readback verify; if both
partitions are written, boot1 first, boot0 last.
- First flashes bench-gated on an attached serial console.
- Documented recovery ladder from then on: other slot → button-hold
recovery → SD card → serial console.
## Contracts
- **Artifacts** (consumers: installer, GUI updater, recovery):
`forgefirm.fw` (fixed asset name; signed; version in the fwup
metadata = release tag `v<semver>`; tasks `upgrade.a`/`upgrade.b`,
`complete` from Phase 5), `sha256sums.txt`,
`forgefirm-image-glowforge.rootfs.wic.gz` (SD burns).
- **Env**: SD = `0/0/1//dev/mmcblk1p1`; slot N = `1/0/N//dev/mmcblk2pN`
(`mmcdev/mmchwpart/mmcpart/mmcroot`, always one transaction).
- **Archive layout**: `/data/forgefirm/archive/` —
`factory-rootfs-<ver>.img.gz`, `boot0.img`, `boot1.img`,
`manifest` (slot versions, dates, checksums).
## Decisions
- uEnv.txt keeps its `mmcargs` override with `root=${mmcroot}` — the
image is slot-agnostic, steered only by the saved env.
- Modern-fwup-packed signed archives apply with the factory 0.14.2
binary (raw 32-byte pubkey form); no shipped fwup is needed on the
factory side.
- Size gates live in two layers: bitbake fails past the 200 MiB slot;
`release.sh` warns ≥ 170 MiB and fails ≥ 195 MiB.
- Dev archives are always signed with the dedicated dev key
(`release.sh --dev`), never unsigned.
- Production signing key: held offline by the operator (never in the
repo, CI, or cloud-synced plaintext), public key embedded in the
installer. Production-signed archives verify with fwup 1.16 and the
factory's 0.14.2 (raw pubkey form); dev-signed archives are rejected.
Custody optimizes against compromise over loss: loss means users
re-run a fresh installer; compromise means attacker-signed firmware
on fielded machines.
- U-Boot bootcount/auto-revert is out of scope — the recovery ladder
covers bad flips.
## Open items
- Periodic GUI update check default-on vs opt-in (it pings GitHub;
proposal: on by default, apply always manual, config switch to
disable).
- Recovery kernel modules: carried from the factory image vs rebuilt
from GPL source (Phase 5 gate).
## Dependencies between the phases
0 → 1 → 2 + 2b → 3 → 4 → 5: everything after Phase 0 assumes
slot-agnostic images and working `.fw` round-trips; the GUI (4) reuses
the probe (1) and pipeline (3); recovery (5) is an independent
mini-project on top of the stable slot scheme.
-440
View File
@@ -1,440 +0,0 @@
# Video and the cameras
The machine has two cameras — one in the lid looking down at the bed, one in the
print head looking at the material under the lens — and ForgeFIRM serves both
over plain HTTP from the web control panel: **MJPEG** for anything that can read
a stream of JPEGs, and an **H.264** live stream for clients that decode video
(the panel uses it when the browser can). There is no app, no cloud relay, and
no proprietary protocol.
One rule governs all of it: **the cameras only capture with the lid closed**
(§2). Everything else here assumes that condition is met.
This page explains what you get, how to point a client at it, what the sensors
are physically capable of versus what ForgeFIRM actually sends and why, and what
you can change.
- For sender setup in general, see [Connecting LightBurn](LIGHTBURN.md).
- The cloud mode described in §8 is documented in
[Cloud mode](https://github.com/ScottW514/python3-gfhardware/blob/master/forgefirm-app/docs/CLOUD.md).
---
## 1. What the hardware is
The two cameras carry the same kind of sensor and feed one shared path into the
board. A **hardware MIPI switch** (the factory `CAM_SEL` line) selects which of
them reaches the board's single camera receiver, so **exactly one camera can be
capturing at any moment**. That is a property of the board, not a software
limit — there is no configuration in which both stream at once.
| | Lid camera | Head camera |
|---|---|---|
| Sees | the whole bed, from above | the material directly under the lens |
| Its lamp | the lid LED strip | the white LED in the print head |
| Used for | bed view, camera-referenced homing, LightBurn's camera overlay | focus and material inspection (cloud mode's distance measurement) |
The lid lens is a wide fisheye, which matters for how you use the image (§5.5).
Which sensor is fitted depends on the machine. Standard machines carry a **5 MP
OV5648**; "HD" machines carry an **8 MP OV8856**. ForgeFIRM reads which one
bound and configures itself accordingly — one firmware image covers both — and
reports it in `/cam/status` and on the panel's Status tab.
---
## 2. Privacy: the cameras only work with the lid closed
**Neither camera captures anything while the lid is open.** Not the live view,
not a snapshot, and not an image requested by the Glowforge service in cloud
mode. Close the lid and everything works; open it and the sensors stop.
The reason is where the lid camera points. It is mounted in the lid, so raising
the lid swings it up to face the room — and in cloud mode the shutter is not
yours to press: the service asks for images on its own schedule, whenever it is
connected. The rule removes the question. The enclosure being shut is the
condition for an image to exist at all.
**What the rule covers**
- **Both cameras.** The head camera is gated too, so this is one rule to
remember rather than a rule with an exception you have to trust.
- **Every way in:** the panel, `/cam/stream`, `/cam/snapshot`, the
mjpg-streamer aliases, LightBurn, and cloud mode's image actions.
- **Capture already running.** Opening the lid stops a live stream within
about a frame and shuts the sensor down; it does not merely block new
requests.
- **The lamps.** A refused capture never raises them, so an attempt with the
lid open leaves no trace.
**How it behaves**
| Situation | What happens |
|---|---|
| Snapshot requested with the lid open | `409` and a message naming the lid; no image data |
| Stream requested with the lid open | `409`; the stream never opens |
| Lid opened while a stream is running | the stream ends cleanly and the pipeline is torn down |
| Lid state unreadable | treated as open — capture refused |
| Cloud service asks for an image with the lid open | refused, and reported back to the service as a failed action rather than left hanging |
| Lid closed again | everything works immediately; nothing to restart |
`GET /cam/status` reports it: **`capture_allowed`** is false whenever the lid is
open, and **`stopped_by_lid`** records that the last capture ended because the
lid opened rather than going idle. The panel's Status tab says *lid open — the
cameras are off* rather than showing a stream error.
**Where the check comes from.** The lid signal is the same one the hardware
safety chain uses to gate the beam — the series combination of both lid
switches, not a software flag — and the check **fails closed**: if the lid
state cannot be read at all, the cameras stay dark. That direction is proven by
a unit test in CI; the end-to-end behavior is an acceptance test run on real
hardware.
**One thing it costs.** The factory firmware ran the cloud's focus *hunt* with
the lid open, and part of a hunt is a head capture. Those captures are now
refused, so a hunt attempted with the lid open fails instead of completing.
Close the lid before letting the app focus or print.
**What it is not.** This is a rule enforced by the two programs that own the
sensors, not a hardware cut-off: the sensor rails stay powered, and anyone with
root on the machine could bypass it. It protects you from the Glowforge
service, from other software on your network, and from a stream you forgot was
running — not from someone who already controls the board. There is
deliberately no setting to turn it off.
---
## 3. Watching it
**In the panel.** Open `http://<machine>:8080/` and go to the **Status** tab.
The *Lid camera* card shows a still by default with **Live** and **Refresh**
buttons; **Live** switches the same frame to the running stream.
**From another program.** The endpoints are:
| URL | What it returns |
|---|---|
| `/cam/stream?cam=lid` | continuous MJPEG (`multipart/x-mixed-replace`) |
| `/cam/stream?cam=head` | the same, from the head camera |
| `/cam/h264?cam=lid` | continuous H.264 as fragmented MP4 (see §5.6): the same picture in a fraction of the bytes, for clients that decode video |
| `/cam/snapshot?cam=lid` | one full-resolution JPEG |
| `/cam/snapshot?cam=lid&res=half` | one half-resolution JPEG (much faster) |
| `/cam/status` | JSON: which sensor, which camera, frame rate, frame sizes, whether the lid currently permits capture |
| `/?action=stream` | the lid stream again, under the name mjpg-streamer clients expect |
| `/?action=snapshot` | one full-resolution lid JPEG, same aliasing |
The `?action=` pair exists because a lot of software — print-server dashboards,
camera widgets, anything written against mjpg-streamer — assumes those exact
URLs. Point such a client at `http://<machine>:8080/` and it will work.
Every one of them answers **`409`** with the lid open (§2), so a client that
checks status codes can tell "close the lid" apart from "the camera is broken".
**Access.** Reading the camera needs no token. It does need a request that
addresses the machine by IP address (or `localhost`) and, from a browser, one
that is not cross-site — that is what stops a hostile page in another tab from
reaching into your machine. It is not protection against other people on your
LAN. Anything that *changes* machine state does need the panel's token. In
practice: paste the URL into any local client and it works.
---
## 4. What you actually get
| | 5 MP machine (OV5648) | 8 MP machine (OV8856) |
|---|---|---|
| Sensor frame captured | 2592 × 1944 | 3264 × 2448 |
| Live stream | 1296 × 972 | 1632 × 1224 |
| Full snapshot | 2592 × 1944 | 3264 × 2448 |
| Half snapshot | 1296 × 972 | 1632 × 1224 |
| Stream formats | MJPEG (quality 75 by default) and H.264 (~1.5 Mbit/s by default) | same |
| Frame rate | **15 fps** sustained | not yet measured (§10) |
Measured on a 5 MP machine: 15.0 fps with a viewer attached, which is the rate
the sensor itself produces in this mode — the machine is not the bottleneck.
The daemon uses about 41 % of one CPU with one viewer, and LightBurn can watch
the stream while jogging from the same session without disturbing motion. A
full-resolution still takes about 2.4 s to produce (2.7 s if the camera has to
be started first), because 5 megapixels of demosaicing and JPEG encoding happen
on the machine's CPU.
The stream frame is not a resampled copy of the full frame. Each 2 × 2 group of
sensor pixels becomes exactly one output pixel, which is why the stream is
precisely half the capture in each axis and why it is cheap enough to run
continuously.
The 41 % figure is the NEON demosaic feeding the hardware JPEG encoder. When
the GPU demosaic and the H.264 stream carry the load instead (§5.6), the
stream's CPU cost drops to bookkeeping; those two paths are newer than the
figure above and their own numbers will be measured on the bench the same way.
---
## 5. What the sensor can do versus what ForgeFIRM sends
This is where expectations usually come unstuck: the sensors are more capable
on paper than the video you get. Each difference below is deliberate and has a
reason.
| | The sensor can | ForgeFIRM sends | Why |
|---|---|---|---|
| Live resolution | full frame | half in each axis | CPU and bandwidth; §5.1 |
| Frame rate (5 MP) | 30 fps in reduced modes | 15 fps | the full-field mode runs at 15 fps; §5.1 |
| Resolution (8 MP) | 3280 × 2464 | 3264 × 2448 | the widest frame the board's camera receiver can take; §5.2 |
| Bit depth | 10 bits per pixel | 8 bits | JPEG is 8-bit, and 8-bit is what makes §5.2 fit |
| Exposure / color | auto exposure and auto white balance | fixed values | a bed image has to look the same frame to frame; §5.4 |
| Lens | — | no correction applied | correction belongs in the client; §5.5 |
| Encoding | — | MJPEG and H.264, nothing recorded | §5.6 |
| Mirroring | a mirror register | mirrored in software instead | the register breaks capture on this board; §5.7 |
### 5.1 Resolution and frame rate on a 5 MP machine
The OV5648 offers several modes, and they are not simply "the same picture,
smaller":
| Mode | Rate | Field of view |
|---|---|---|
| 2592 × 1944 | 15 fps | the whole sensor |
| 1920 × 1080 | 15 fps | a crop from the middle |
| 1600 × 1200 | 15 fps | a crop from the middle |
| 1280 × 960 | 30 fps | the whole sensor, every other pixel |
| 1280 × 720 | 30 fps | a crop, every other pixel |
| 640 × 480 | 30 fps | the whole sensor, every fourth pixel |
ForgeFIRM runs the **2592 × 1944** mode. The cropped modes are unusable for a
bed camera — they would show the middle of the bed and cut off the corners. That
leaves the full-frame mode at 15 fps or the skipped 1280 × 960 mode at 30 fps.
The camera runs **one mode at a time**, and the live stream and the snapshots
come from the same frames: that is what lets a snapshot be delivered while a
stream is running, and it is why the picture does not stutter or re-expose when
you take one. Choosing 1280 × 960 would double the frame rate and permanently
give up full-resolution stills — and full resolution is exactly what bed
alignment, camera calibration and cloud mode need. Full stills win; 15 fps is
the price.
The stream is halved to 1296 × 972 rather than sent at full size because
demosaicing and encoding 5 megapixels 15 times a second is far beyond this
CPU, and because a 5 MP live view of the bed is of no practical use — it is a
positioning aid, not a photograph.
### 5.2 8 MP ("HD") machines: a few rows short of the full array
The OV8856's largest frame is 3280 × 2464. ForgeFIRM captures **3264 × 2448**,
which is 16 columns and 16 rows less — the whole field of view, edge to edge,
just without the last few pixels of margin.
Getting there is not free, and it explains why §5.3 matters. The sensor can
send its full frame over the two data lanes this board wires, but at 10 bits
per pixel that means running the link at **1.44 Gbit/s per lane**, and the
i.MX6's camera receiver tops out at **1 Gbit/s per lane** — it has no timing
setting for anything faster, so it refuses the mode outright. Asking the sensor
for 8-bit pixels instead cuts a fifth off every sample and lets the same frame
travel at half the rate, which the receiver takes comfortably. That is how an
HD machine gets its full resolution, and it costs nothing, because the
delivered JPEG was going to be 8-bit anyway.
The result is about 15 frames per second off the sensor, and roughly the same
bytes per second across the bus as a 5 MP machine at its own full frame.
### 5.3 Ten bits in, eight bits out
Both sensors can emit 10 bits per pixel. ForgeFIRM asks both for 8 instead, and
the delivered image is 8 bits per channel because that is what JPEG is.
Two bits would buy nothing without a tone curve to spend them on, and there is
no tone curve (§5.4) — while asking for 8 halves the data crossing the bus,
which is what keeps the stream cheap on a 5 MP machine and what makes full
resolution reachable at all on an 8 MP one (§5.2).
### 5.4 Exposure, gain and color are fixed
There is no auto-exposure and no auto white balance. Exposure, gain and the
color balance are set to fixed values when the camera starts, matching what the
factory firmware used, and they are not adjustable from the panel.
That is deliberate. A bed camera is a measuring instrument: camera-referenced
homing, LightBurn's overlay, and cloud mode's alignment all compare images to
known geometry, and an image whose brightness and color shift between frames —
as the head moves through the frame, or as the laser flashes — is worse than a
consistently imperfect one.
ForgeFIRM also applies **no gamma, tone curve, sharpening or noise reduction**.
The JPEG is the sensor's data, demosaiced and encoded. Compared with a phone
photo the result looks flat. That is expected; it is not a fault, and it does
not affect how well the image works for alignment.
One consequence on 8 MP machines: that sensor's driver publishes no color
balance controls at all, so those images will be less color-correct than a
5 MP machine's until the exposure and gain are commissioned on real hardware.
### 5.5 The lens is not corrected
The lid lens is a wide fisheye and the image has heavy barrel distortion —
straight bed edges bow. ForgeFIRM sends the image as the lens sees it and does
not attempt to flatten it.
Correction belongs where the calibration lives: **LightBurn's camera
calibration** pass measures your particular machine's lens and applies the
correction on the host, which is more accurate than a fixed correction baked
into the firmware and costs the machine's CPU nothing. Run that calibration
before trusting the camera overlay for placement.
### 5.6 Two streams, one picture: MJPEG and H.264. Nothing is recorded.
The same live picture is served two ways, and **the machine never writes video
to disk**.
**MJPEG** (`/cam/stream`) is the universal one: every frame is a complete
JPEG, so a viewer can join or leave at any moment, a dropped frame costs
nothing, and browsers, LightBurn and mjpg-streamer clients consume it with no
plugin. It stays, unchanged, and it is what anything that cannot decode video
should use.
**H.264** (`/cam/h264`) exists because bytes on this machine are not free.
MJPEG re-sends the whole scene fifteen times a second, roughly 9 Mbit/s, and
the WiFi transmit path runs on the machine's single CPU core, where measured
cost is about 7 % of the core per MB/s sent. A bed camera's scene barely
changes between frames, which is exactly what an inter-frame codec exploits:
the H.264 stream carries the same picture in roughly 1.5 Mbit/s and gives most
of that CPU back. It arrives as fragmented MP4, the form a browser's Media
Source Extensions accept, with the codec named in an `X-H264-Codec` response
header; the panel's **Live** button uses it automatically where the browser
can and falls back to MJPEG where it cannot. Latency is a beat behind MJPEG
(under a second), which is why LightBurn keeps consuming the MJPEG stream.
Both encoders are hardware: JPEG frames come from the CODA960's JPEG unit and
H.264 from its BIT processor, two independent engines, so serving both at once
does not double any cost that matters. The demosaic that feeds them runs as
fragment shaders on the SoC's GC880 GPU when the image ships the GL stack
(reported as `"convert": "gpu"` in `/cam/status`), reading the sensor frame
and writing the encoder's buffer directly, so a stream frame never crosses the
CPU at all; without the GPU it falls back to the NEON demosaic. (Stills are
still demosaiced and encoded on the CPU, which is most of why a
full-resolution one takes a couple of seconds.)
One more consumer of nothing: with a frame-rate cap set (`FORGECTRL_STREAM_FPS`
of 1 or more), the cap is programmed into the CSI receiver's frame-skip
hardware, and skipped frames are dropped before they are ever written to
memory. `/cam/status` reports `"hw_fps_skip": true` when that is in effect.
If you want a recording, record the stream on the computer watching it. The
machine stores its firmware, settings and logs on a small internal flash device
and has no recording feature to fill it with.
### 5.7 The mirror is applied in software
The image is mirrored horizontally to match the orientation the factory
software produced. The sensors have a mirror register that would do this for
free, but setting it breaks the board's capture path — frames stop completing
altogether — so the flip is done while the image is being demosaiced instead.
The cost is negligible and the result is identical.
---
## 6. Lighting
Each camera has its own lamp, and ForgeFIRM drives them around captures:
- **While capturing**, the relevant lamp is raised to a fixed working level and
restored when the camera goes idle.
- **At rest**, the lid lamp sits at the `lid_lamp_idle` setting (0–255, default
236) — the bed light you normally see. It is asserted when the daemon starts,
when you change the setting, and whenever a controller starts.
- **Per shot**, `/cam/snapshot` accepts `lamp=0..1023` to override the level for
that one image; a few frames are discarded afterward so the image you get was
actually exposed under the light you asked for.
In cloud mode the cloud client drives the lid lamp for as long as it runs, and
ForgeFIRM re-asserts your idle level the next time a controller starts.
---
## 7. Sharing one camera path
Because only one camera can capture at a time, requests have to be arbitrated.
The rule is **the newest request wins**, on the assumption that one person is
standing at the machine:
- **A new stream preempts the current one.** Existing viewers' streams end
cleanly — their picture freezes on the last frame — rather than being torn
mid-frame.
- **A snapshot of the other camera borrows the path.** The stream pauses, the
mux switches, one frame is taken, and it switches back; viewers see a gap of
a second or two. Snapshots do not fail because someone else is watching.
- **The camera shuts down after 10 seconds** with nobody watching and no
snapshot pending, so other software on the machine can use it — and
immediately, whoever is watching, if the lid opens (§2).
---
## 8. Who else uses the cameras
- **Camera-referenced homing** (`$H`) takes a lid image and has the Glowforge
service work out where the head is. This is the factory homing method, so it
needs a service session; it is the one part of GRBL mode that does.
- **Cloud mode** captures both cameras on demand through the same snapshot
endpoint, so it obeys the same arbitration as everything else.
- **LightBurn** consumes the lid stream for its camera overlay while it drives
motion over the Grbl connection; the two coexist.
---
## 9. When something looks wrong
**No picture at all, and a 409 mentioning the lid.** Working as intended: the
lid is open (§2). Close it. `/cam/status` shows `capture_allowed: false` while
that is the case. If the lid *is* shut and you still see this, one of the two
lid switches is not making — the same condition that would stop the laser
firing, so it is worth investigating rather than working around.
**A black or nearly black picture.** The scene is not lit: the exposure is
fixed, so the camera cannot compensate. Check `lid_lamp_idle`, and remember
snapshots can carry their own `lamp=` level.
**The stream stops on its own.** Either the lid opened (§2 — the panel says
so), or someone else — another browser tab, LightBurn, the panel — asked for
the other camera, or for a stream, and preempted yours. Reload; the panel does
this automatically and says which it was.
**"camera switch timed out".** A viewer would not let go within the grace
period. Close the other viewer and retry.
**A snapshot returns 503.** The camera could not start. The usual cause is
another process holding the capture device; the daemon's log names the failing
step.
**`/cam/status` reports `"sensor": "unknown"`.** No camera was found on that
bus, or a sensor bound that this firmware has no profile for. On an HD machine
see §9.
**The picture is bowed / placement is off.** Expected without calibration; see
§5.5.
---
## 10. Status of 8 MP ("HD") machines
Everything an 8 MP machine needs is in the firmware: the kernel patches for the
OV8856 — including the 8-bit full-resolution mode §5.2 depends on — the
device-tree entries, and a capture path that picks its geometry and sensor
controls from whichever sensor bound.
**None of it has run on an 8 MP machine.** No such unit has been available to
test against, so treat 8 MP support as untested rather than working: whether
the receiver locks onto the full-resolution mode, and what exposure and gain
the sensor actually wants, can only be settled on that hardware. Frame-rate and
CPU figures in §4 are from a 5 MP machine and do not carry over — an HD machine
demosaics 60 % more pixels per frame. Reports from anyone with an HD machine
are welcome.
The 5 MP path is hardware-validated and in daily use.
---
## See also
- [Connecting LightBurn](LIGHTBURN.md) — sender setup and the camera overlay
- [Motion and laser drive](MOTION.md) — what the machine does while you watch
- [Cooling and airflow](COOLING.md)
- [Laser safety](SAFETY.md)
-1
View File
@@ -1 +0,0 @@
Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 966 KiB

-215
View File
@@ -1,215 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1500 860" width="1500" height="860" font-family="Segoe UI, Helvetica, Arial, sans-serif" font-size="13">
<title>Glowforge laser safing chain as run by ForgeFIRM</title>
<desc>Lid switches, the charge-pump watchdog, the button latch, the interlock latch and the FIRE line combine on the factory control board into HV_ENABLE and LASER_ON for the laser power supply; the SoC drives four lines and reads back the rest.</desc>
<defs>
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#222"/>
</marker>
<style>
.wire { fill: none; stroke: #222; stroke-width: 2; }
.wire-out { fill: none; stroke: #222; stroke-width: 2; marker-end: url(#arr); }
.dot { fill: #222; }
.gate { fill: #ffffff; stroke: #222; stroke-width: 1.6; }
.gate-l { font-weight: 700; font-size: 13px; text-anchor: middle; fill: #111; }
.gate-s { font-size: 10px; text-anchor: middle; fill: #555; }
.pin { font-size: 10px; fill: #444; }
.in-tag { fill: #e9ebee; stroke: #6b7280; stroke-width: 1.2; }
.out-tag { fill: #dbeafe; stroke: #2563eb; stroke-width: 1.2; }
.rb-tag { fill: #fef3c7; stroke: #d97706; stroke-width: 1.2; }
.psu-tag { fill: #fee2e2; stroke: #dc2626; stroke-width: 1.2; }
.tag-t { font-size: 12px; font-weight: 700; fill: #111; }
.tag-s { font-size: 10px; fill: #444; }
.region { fill: #f6f7f9; stroke: #b8bec8; stroke-width: 1; stroke-dasharray: 5 4; }
.region-l { font-size: 12px; font-weight: 700; fill: #4b5563; letter-spacing: 0.4px; }
.net { font-size: 11px; font-style: italic; fill: #333; }
.note { font-size: 12px; fill: #222; }
.title { font-size: 18px; font-weight: 700; fill: #111; }
</style>
</defs>
<rect width="1500" height="860" fill="#ffffff"/>
<text x="20" y="30" class="title">Glowforge control board — laser safing chain, as run by ForgeFIRM</text>
<!-- regions -->
<rect x="20" y="52" width="212" height="640" class="region"/>
<text x="30" y="70" class="region-l">PHYSICAL INPUTS</text>
<rect x="250" y="52" width="1010" height="640" class="region"/>
<text x="260" y="70" class="region-l">CONTROL-BOARD SAFING LOGIC (hardware)</text>
<rect x="1290" y="52" width="192" height="640" class="region"/>
<text x="1300" y="70" class="region-l">LASER POWER SUPPLY (J1)</text>
<!-- ===================== Row A: lid switches ===================== -->
<rect x="40" y="90" width="172" height="28" rx="5" class="in-tag"/>
<text x="50" y="109" class="tag-t">LID_SW1</text><text x="122" y="109" class="tag-s">J4_13 · closed = 1</text>
<rect x="40" y="130" width="172" height="28" rx="5" class="in-tag"/>
<text x="50" y="149" class="tag-t">LID_SW2</text><text x="122" y="149" class="tag-s">J4_12 · closed = 1</text>
<path class="wire" d="M212 104 H330"/>
<path class="wire" d="M212 144 H310 V116 H330"/>
<rect x="330" y="84" width="64" height="44" rx="6" class="gate"/>
<text x="362" y="103" class="gate-l">AND</text><text x="362" y="118" class="gate-s">U17-1</text>
<path class="wire" d="M394 106 H740 V136 H760"/>
<circle cx="470" cy="106" r="3.5" class="dot"/>
<text x="480" y="98" class="net">DOORS_OK (both lid switches closed)</text>
<!-- doors readback -->
<path class="wire" d="M560 106 V126"/>
<rect x="500" y="126" width="150" height="24" rx="5" class="rb-tag"/>
<text x="508" y="142" class="tag-t">doors</text><text x="552" y="142" class="tag-s">EV_SW 3 · GPIO1_00</text>
<!-- ===================== Row B: charge-pump watchdog / HV_ENABLE ===================== -->
<rect x="262" y="176" width="190" height="40" rx="5" class="out-tag"/>
<text x="270" y="192" class="tag-t">CHG_PUMP</text><text x="352" y="192" class="tag-s">GPIO3_24</text>
<text x="270" y="207" class="tag-s">200 ms pulses, only while running</text>
<path class="wire" d="M452 196 H500"/>
<rect x="500" y="174" width="90" height="44" rx="6" class="gate"/>
<text x="545" y="192" class="gate-l">one-shot</text><text x="545" y="207" class="gate-s">U1-1 · t_w ≈ 0.45 s</text>
<path class="wire" d="M590 196 H620 V156 H760"/>
<circle cx="620" cy="196" r="3.5" class="dot"/>
<text x="630" y="190" class="net">WDOG_ALIVE</text>
<!-- charge_pump_alive readback -->
<path class="wire" d="M620 196 V222"/>
<rect x="540" y="222" width="212" height="24" rx="5" class="rb-tag"/>
<text x="548" y="238" class="tag-t">charge_pump_alive</text><text x="672" y="238" class="tag-s">GPIO1_08 (¬Q)</text>
<!-- AND U17-4 -->
<rect x="760" y="124" width="64" height="44" rx="6" class="gate"/>
<text x="792" y="143" class="gate-l">AND</text><text x="792" y="158" class="gate-s">U17-4</text>
<path class="wire-out" d="M824 146 H1300"/>
<circle cx="880" cy="146" r="3.5" class="dot"/>
<text x="900" y="166" class="net">HV_ENABLE</text>
<!-- U24 inverter -> hv_enable readback (factory net name E-STOP) -->
<path class="wire" d="M880 146 V88 H930"/>
<rect x="930" y="70" width="64" height="36" rx="6" class="gate"/>
<text x="962" y="86" class="gate-l">NOT</text><text x="962" y="99" class="gate-s">U24</text>
<path class="wire" d="M994 88 H1010"/>
<rect x="1010" y="76" width="262" height="24" rx="5" class="rb-tag"/>
<text x="1018" y="92" class="tag-t">hv_enable</text><text x="1092" y="92" class="tag-s">EV_SW 4 · GPIO4_06 · readback</text>
<!-- PSU tag HV_ENABLE -->
<rect x="1300" y="132" width="172" height="28" rx="5" class="psu-tag"/>
<text x="1308" y="151" class="tag-t">J1_16</text><text x="1354" y="151" class="tag-s">HV_ENABLE</text>
<text x="1300" y="180" class="note">HV runs only while this</text>
<text x="1300" y="196" class="note">line is alive (pulsed).</text>
<!-- ===================== Row C: button latch ===================== -->
<!-- ¬DOORS_OK -->
<path class="wire" d="M470 106 V190 A6 6 0 0 1 470 202 V288 H500"/>
<rect x="500" y="270" width="64" height="36" rx="6" class="gate"/>
<text x="532" y="286" class="gate-l">NOT</text><text x="532" y="299" class="gate-s">U5-4</text>
<path class="wire" d="M564 288 H584 V280 H600"/>
<text x="565" y="266" class="net">lid open</text>
<!-- LATCH_RESET -->
<rect x="262" y="296" width="190" height="40" rx="5" class="out-tag"/>
<text x="270" y="312" class="tag-t">LATCH_RESET</text><text x="366" y="312" class="tag-s">GPIO1_07</text>
<text x="270" y="327" class="tag-s">cnc/laser_latch: 1 = lock (FIRE also Hi-Z)</text>
<path class="wire" d="M452 316 H584 V300 H600"/>
<!-- OR U32 -->
<rect x="600" y="268" width="64" height="44" rx="6" class="gate"/>
<text x="632" y="287" class="gate-l">OR</text><text x="632" y="302" class="gate-s">U32</text>
<path class="wire" d="M664 290 H720"/>
<!-- button latch -->
<rect x="720" y="272" width="124" height="76" rx="6" class="gate"/>
<text x="782" y="292" class="gate-l">Button latch</text>
<text x="782" y="306" class="gate-s">U23-1 · CD4043B</text>
<text x="782" y="320" class="gate-s">set-dominant</text>
<text x="726" y="294" class="pin">S</text>
<text x="726" y="336" class="pin">R</text>
<text x="832" y="314" class="pin">Q</text>
<!-- BUTTON -->
<rect x="40" y="332" width="172" height="28" rx="5" class="in-tag"/>
<text x="50" y="351" class="tag-t">BUTTON</text><text x="118" y="351" class="tag-s">J5 · pressed = 1</text>
<path class="wire" d="M212 346 H700 V332 H720"/>
<path class="wire" d="M330 346 V356"/>
<rect x="270" y="356" width="150" height="24" rx="5" class="rb-tag"/>
<text x="278" y="372" class="tag-t">button</text><text x="326" y="372" class="tag-s">EV_SW 2 · GPIO4_09</text>
<!-- Q1 -> NOT -> AND2 -->
<path class="wire" d="M844 311 H890"/>
<circle cx="868" cy="311" r="3.5" class="dot"/>
<rect x="890" y="293" width="64" height="36" rx="6" class="gate"/>
<text x="922" y="309" class="gate-l">NOT</text><text x="922" y="322" class="gate-s">U5-6</text>
<path class="wire" d="M954 311 H975 V380 H1000"/>
<text x="958" y="345" class="net">¬Q1</text>
<!-- button_latch readback -->
<path class="wire" d="M868 311 V262"/>
<rect x="770" y="238" width="222" height="24" rx="5" class="rb-tag"/>
<text x="778" y="254" class="tag-t">button_latch</text><text x="864" y="254" class="tag-s">GPIO1_03 · = Q1 · 1 = set</text>
<!-- ===================== Row D: interlock latch ===================== -->
<rect x="262" y="462" width="190" height="40" rx="5" class="out-tag"/>
<text x="270" y="478" class="tag-t">INTERLOCK_RESET</text><text x="386" y="478" class="tag-s">GPIO4_05</text>
<text x="270" y="493" class="tag-s">1 while loop open or unobserved</text>
<path class="wire" d="M452 476 H720"/>
<rect x="720" y="458" width="124" height="76" rx="6" class="gate"/>
<text x="782" y="478" class="gate-l">Interlock latch</text>
<text x="782" y="492" class="gate-s">U23-2 · CD4043B</text>
<text x="782" y="506" class="gate-s">set-dominant</text>
<text x="726" y="480" class="pin">S</text>
<text x="726" y="522" class="pin">R</text>
<text x="832" y="500" class="pin">Q</text>
<!-- INTERLOCK loop -->
<rect x="40" y="504" width="172" height="40" rx="5" class="in-tag"/>
<text x="50" y="520" class="tag-t">INTERLOCK loop</text><text x="160" y="520" class="tag-s">J8</text>
<text x="50" y="535" class="tag-s">jumpered on Basic/Plus · closed = 1</text>
<path class="wire" d="M212 518 H720"/>
<path class="wire" d="M330 518 V540"/>
<rect x="270" y="540" width="262" height="24" rx="5" class="rb-tag"/>
<text x="278" y="556" class="tag-t">interlock</text><text x="340" y="556" class="tag-s">EV_SW 5 · GPIO1_09 · active = open</text>
<!-- Q2 -> NOT -> AND2 -->
<path class="wire" d="M844 497 H890"/>
<circle cx="868" cy="497" r="3.5" class="dot"/>
<rect x="890" y="479" width="64" height="36" rx="6" class="gate"/>
<text x="922" y="495" class="gate-l">NOT</text><text x="922" y="508" class="gate-s">U6-3</text>
<path class="wire" d="M954 497 H985 V400 H1000"/>
<text x="958" y="470" class="net">¬Q2</text>
<!-- interlock_latch readback -->
<path class="wire" d="M868 497 V546"/>
<rect x="770" y="546" width="236" height="24" rx="5" class="rb-tag"/>
<text x="778" y="562" class="tag-t">interlock_latch</text><text x="878" y="562" class="tag-s">EV_SW 6 · GPIO1_02 · = Q2</text>
<!-- ===================== Row E: LASER_ON ===================== -->
<rect x="1000" y="368" width="64" height="44" rx="6" class="gate"/>
<text x="1032" y="387" class="gate-l">AND</text><text x="1032" y="402" class="gate-s">U17-2</text>
<path class="wire" d="M1064 390 H1110"/>
<rect x="1110" y="378" width="64" height="44" rx="6" class="gate"/>
<text x="1142" y="397" class="gate-l">AND</text><text x="1142" y="412" class="gate-s">U17-3</text>
<!-- FIRE -->
<rect x="850" y="600" width="232" height="40" rx="5" class="out-tag"/>
<text x="858" y="616" class="tag-t">FIRE</text><text x="896" y="616" class="tag-s">GPIO2_30 (laser_enable)</text>
<text x="858" y="631" class="tag-s">SDMA pulse-byte bit 4 · Hi-Z when locked or idle</text>
<path class="wire" d="M1082 620 H1094 V410 H1110"/>
<!-- LASER_ON out -->
<path class="wire-out" d="M1174 400 H1300"/>
<circle cx="1210" cy="400" r="3.5" class="dot"/>
<text x="1182" y="392" class="net">LASER_ON</text>
<path class="wire" d="M1210 400 V446"/>
<rect x="1112" y="446" width="146" height="24" rx="5" class="rb-tag"/>
<text x="1120" y="462" class="tag-t">laser_on</text><text x="1182" y="462" class="tag-s">GPIO1_05</text>
<!-- PSU tag LASER_ON -->
<rect x="1300" y="386" width="172" height="28" rx="5" class="psu-tag"/>
<text x="1308" y="405" class="tag-t">J1_12</text><text x="1354" y="405" class="tag-s">LASER_ON</text>
<text x="1300" y="434" class="note">The tube fires only while</text>
<text x="1300" y="450" class="note">this line is high (and HV</text>
<text x="1300" y="466" class="note">is enabled). Power comes</text>
<text x="1300" y="482" class="note">from PWM on J1_13, which</text>
<text x="1300" y="498" class="note">is not part of the chain.</text>
<!-- ===================== equations ===================== -->
<text x="262" y="612" class="note" font-weight="700">HV_ENABLE = DOORS_OK · WDOG_ALIVE</text>
<text x="262" y="630" class="note" font-weight="700">LASER_ON = FIRE · ¬Q1 · ¬Q2</text>
<text x="262" y="652" class="note">Button latch: SET = lid open OR LATCH_RESET; RESET = button pressed. Cleared only by a press while the lid is closed and the lock released.</text>
<text x="262" y="670" class="note">Interlock latch: SET = INTERLOCK_RESET (driven by glowforge.ko while the loop reads open); RESET = loop closed. Both latches are set-dominant.</text>
<!-- ===================== legend ===================== -->
<rect x="20" y="712" width="1462" height="128" rx="6" fill="#ffffff" stroke="#b8bec8"/>
<text x="34" y="734" class="region-l">LEGEND</text>
<rect x="34" y="746" width="26" height="16" rx="3" class="in-tag"/>
<text x="68" y="759" class="note">physical switch or loop, wired to the board</text>
<rect x="34" y="770" width="26" height="16" rx="3" class="out-tag"/>
<text x="68" y="783" class="note">line driven by the SoC (glowforge.ko)</text>
<rect x="34" y="794" width="26" height="16" rx="3" class="rb-tag"/>
<text x="68" y="807" class="note">line read by the SoC — gpio-keys switch (EV_SW code) or /sys/glowforge/cnc attribute; monitoring only</text>
<rect x="34" y="818" width="26" height="16" rx="3" class="psu-tag"/>
<text x="68" y="831" class="note">output to the laser power supply</text>
<text x="760" y="759" class="note">Boxes are logic functions on the control board (part reference below each). Wires carry logic levels: 1 = true as named.</text>
<text x="760" y="777" class="note">Software can only withhold FIRE, hold LATCH_RESET, drive INTERLOCK_RESET, or stop feeding CHG_PUMP —</text>
<text x="760" y="795" class="note">every one of those makes the hardware block emission. Nothing on the SoC side can add an emission path.</text>
<text x="760" y="817" class="note">Also read: door1 / door2 (EV_SW 0 / 1, GPIO4_14 / GPIO1_06), the two lid switches individually;</text>
<text x="760" y="833" class="note">laser_pgood (GPIO4_21), the supply's HV_OK line on J1_14.</text>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

+2 -2
View File
@@ -189,8 +189,8 @@ TOOLS = [
["watch", "point", "fit", "supply-watch", "supply-point", "supply-fit"]), ["watch", "point", "fit", "supply-watch", "supply-point", "supply-fit"]),
_arg("value", "str", None, "point: the thermometer reading in C; watch: seconds (default 60)")], _arg("value", "str", None, "point: the thermometer reading in C; watch: seconds (default 60)")],
"desc": "Pairs a measured temperature with averaged raw readings; fits a per-machine line. The coolant " "desc": "Pairs a measured temperature with averaged raw readings; fits a per-machine line. The coolant "
"sensors, or the power supply's (thermometer on its heatsink, against the unverified guess in " "sensors, or the power supply's (thermometer on its heatsink, against the documented unverified "
"UAPI.md). Points accumulate in the bench data directory."}, "guess). Points accumulate in the bench data directory."},
{"id": "fan-test", "title": "Fan/coolant bench", "script": "fan_test.py", {"id": "fan-test", "title": "Fan/coolant bench", "script": "fan_test.py",
"safety": "dry", "where": "board", "ported": True, "args": [], "safety": "dry", "where": "board", "ported": True, "args": [],
"desc": "Snapshots fan PWMs/tachs/temps, drives M8 -> cut fans, M9 -> cooldown -> idle; the tach " "desc": "Snapshots fan PWMs/tachs/temps, drives M8 -> cut fans, M9 -> cooldown -> idle; the tach "
+1 -1
View File
@@ -27,7 +27,7 @@ from ..catalog import test
from .. import hw from .. import hw
from ..runner import Failed from ..runner import Failed
# interlock_circuit bits (UAPI.md): bit 3 = the driven latch line, set = locked. # interlock_circuit bits (docs.forgefirm.org, the kernel module page): bit 3 = the driven latch line, set = locked.
LATCH_BIT = 1 << 3 LATCH_BIT = 1 << 3
TICK_HZ = 10000 TICK_HZ = 10000
+3 -3
View File
@@ -2,9 +2,9 @@
# ForgeFIRM — kas build configuration (factory Glowforge control board) # ForgeFIRM — kas build configuration (factory Glowforge control board)
# ============================================================================ # ============================================================================
# The forgefirm repo is the BASE: it controls the build, the output firmware # The forgefirm repo is the BASE: it controls the build, the output firmware
# images land here (build/tmp/deploy/images/glowforge/), and the install docs # images land here (build/tmp/deploy/images/glowforge/), and the status docs
# live here (INSTALL.md, SERIAL.md). The build and release procedure is on the # live here (docs/BRINGUP.md, docs/CAMPAIGN-LOG.md). The install, build and
# documentation site: https://docs.forgefirm.org/developers/ # release procedures are on the documentation site: https://docs.forgefirm.org/
# #
# Target : Yocto Scarthgap (5.0 LTS) + linux-fslc 6.12 (mainline LTS) # Target : Yocto Scarthgap (5.0 LTS) + linux-fslc 6.12 (mainline LTS)
# Machine: glowforge (i.MX6 Solo SOM inside Basic/Plus/Pro) # Machine: glowforge (i.MX6 Solo SOM inside Basic/Plus/Pro)
+1 -1
View File
@@ -40,7 +40,7 @@ page's takeover does that; from a host, stop them first.
| `flow_characterize.py` | Coolant flow characterization using the factory temperature curve (board or host; forgectrl and controller stopped): baseline → flow → no-flow → recovery, printing the ΔT bands and their separation. Takes the heater duty as an argument (`flow_characterize.py 30`); aborts if downstream passes 45 °C. | | `flow_characterize.py` | Coolant flow characterization using the factory temperature curve (board or host; forgectrl and controller stopped): baseline → flow → no-flow → recovery, printing the ΔT bands and their separation. Takes the heater duty as an argument (`flow_characterize.py 30`); aborts if downstream passes 45 °C. |
| `flow_matrix.py` | **The flow-detection design matrix** (board or host; forgectrl and controller stopped; with `flow_sampler.py` from `/usr/share/forgetest/bench/`): duty × duration × flow/no-flow, every run from a common cooled baseline, interleaved repeats. One heating trace yields the metric at every candidate duration, so cost and precision come from the same 60 runs. Prints a cost table, a precision table (mean±sd, worst-case margin, d′) and a ranked shortlist. `flow_matrix.py [duties] [repeats]` (or env `FM_DUTIES`, `FM_REPEATS`, `FM_RESULTS`); results/log in the bench data directory, resumable. | | `flow_matrix.py` | **The flow-detection design matrix** (board or host; forgectrl and controller stopped; with `flow_sampler.py` from `/usr/share/forgetest/bench/`): duty × duration × flow/no-flow, every run from a common cooled baseline, interleaved repeats. One heating trace yields the metric at every candidate duration, so cost and precision come from the same 60 runs. Prints a cost table, a precision table (mean±sd, worst-case margin, d′) and a ranked shortlist. `flow_matrix.py [duties] [repeats]` (or env `FM_DUTIES`, `FM_REPEATS`, `FM_RESULTS`); results/log in the bench data directory, resumable. |
| `flow_sustained.py` | Long-run test of the real re-check cadence via M8 (board or host; controller running): counts verdicts/false faults against the configured `cool_flow_rise` and tracks whether the loop accumulates heat. `flow_sustained.py [minutes]`. | | `flow_sustained.py` | Long-run test of the real re-check cadence via M8 (board or host; controller running): counts verdicts/false faults against the configured `cool_flow_rise` and tracks whether the loop accumulates heat. `flow_sustained.py [minutes]`. |
| `temp_calibrate.py` (`supply-*` modes) | The power supply's sensor (`pic/pwr_temp`, raw) against a thermometer on its heatsink: `supply-watch`, `supply-point <C>`, `supply-fit`; the fit is printed beside `UAPI.md`'s unverified guess. Three points during a long cut settle it. | | `temp_calibrate.py` (`supply-*` modes) | The power supply's sensor (`pic/pwr_temp`, raw) against a thermometer on its heatsink: `supply-watch`, `supply-point <C>`, `supply-fit`; the fit is printed beside the documented unverified guess ([sensors](https://docs.forgefirm.org/technical/machine/sensors/)). Three points during a long cut settle it. |
| `critical_tier_drill.py` | The coolant critical tier on a rising temperature (board or host): sets the ceiling, the resume gate and the critical line a few tenths above the live upstream reading and lets the engine's own flow-check heater warm the loop through them inside one `M8` session, expecting `OVERTEMP` at the ceiling and then `CRITICAL` (fire blocked, hold, no resume) with the fault ending at `M9`; restores the settings and cycles a session so the engine re-reads them. Results as JSON in the bench data directory. | | `critical_tier_drill.py` | The coolant critical tier on a rising temperature (board or host): sets the ceiling, the resume gate and the critical line a few tenths above the live upstream reading and lets the engine's own flow-check heater warm the loop through them inside one `M8` session, expecting `OVERTEMP` at the ceiling and then `CRITICAL` (fire blocked, hold, no resume) with the fault ending at `M9`; restores the settings and cycles a session so the engine re-reads them. Results as JSON in the bench data directory. |
| `aa_offset_check.py` | Coolant offset correction under the run airflow (board; controller running; dark, no press): M8 brings the fans to the run profile while the raw coolant counts, `/status` and the engine's readings are averaged before, during and after; with `cool_aa_offset_counts` at the machine's value the readings hold still while the raw counts step, at zero they drop by about a degree. `aa_offset_check.py [dwell_s]`. | | `aa_offset_check.py` | Coolant offset correction under the run airflow (board; controller running; dark, no press): M8 brings the fans to the run profile while the raw coolant counts, `/status` and the engine's readings are averaged before, during and after; with `cool_aa_offset_counts` at the machine's value the readings hold still while the raw counts step, at zero they drop by about a degree. `aa_offset_check.py [dwell_s]`. |
| `offset_probe.py` | Coolant-sensor offset probe (board; forgectrl idle; dark, no press): switches one actuator at a time (exhaust at 100/50/25 %, intakes, air assist, purge, heater, pump, TEC, lid lamp, then all run fans) with both thermistors sampled at 25 Hz and scores the common-mode step at every edge and the level toggling inside every dwell; `offset_probe.py ladder` runs the air-assist duty ladder alone. Every value is restored on exit. JSON record in the bench data directory. | | `offset_probe.py` | Coolant-sensor offset probe (board; forgectrl idle; dark, no press): switches one actuator at a time (exhaust at 100/50/25 %, intakes, air assist, purge, heater, pump, TEC, lid lamp, then all run fans) with both thermistors sampled at 25 Hz and scores the common-mode step at every edge and the level toggling inside every dwell; `offset_probe.py ladder` runs the air-assist duty ladder alone. Every value is restored on exit. JSON record in the bench data directory. |
+1 -1
View File
@@ -14,7 +14,7 @@ board(cmd) run a shell command on the machine and return its stdout
(local: sh -c; host: ssh, or the client named by GF_SSH, (local: sh -c; host: ssh, or the client named by GF_SSH,
e.g. GF_SSH='wsl -d <distro> -- ssh'). e.g. GF_SSH='wsl -d <distro> -- ssh').
degc(raw) the factory B-equation coolant conversion degc(raw) the factory B-equation coolant conversion
(kernel-module-glowforge/UAPI.md). (docs.forgefirm.org, the sensors page).
data_path(f) where a tool keeps its data files: FORGETEST_BENCH_DATA data_path(f) where a tool keeps its data files: FORGETEST_BENCH_DATA
when set (the bench page passes <data>/bench/), else next when set (the bench page passes <data>/bench/), else next
to the tool. to the tool.
+1 -1
View File
@@ -12,7 +12,7 @@ full download with the GF1 header (magic at [1:4], total header length at
[4:8], then 8-byte key/value records). Header settings (STfr, XSmm) override [4:8], then 8-byte key/value records). Header settings (STfr, XSmm) override
the --rate/--mode defaults when present. the --rate/--mode defaults when present.
Byte layout (kernel-module-glowforge/UAPI.md, hardware-verified): Byte layout (docs.forgefirm.org, the step engine page; hardware-verified):
bit7 set -> laser power byte (low 7 bits = power, no steps this tick) bit7 set -> laser power byte (low 7 bits = power, no steps this tick)
bit0 X_STEP, bit1 X_DIR (set = -X) bit0 X_STEP, bit1 X_DIR (set = -X)
bit2 Y_STEP, bit3 Y_DIR (set = +Y) bit2 Y_STEP, bit3 Y_DIR (set = +Y)
+5 -5
View File
@@ -2,7 +2,7 @@
"""Coolant temperature spot-check helper (runs on the board or from a """Coolant temperature spot-check helper (runs on the board or from a
host; gfbench: GF_HOST). host; gfbench: GF_HOST).
The raw->Celsius conversion in UAPI.md is the factory B-equation (10k The documented raw->Celsius conversion (docs.forgefirm.org, sensors) is the factory B-equation (10k
B3380 NTC in a 10k divider behind a 1.3x gain stage, 10-bit ADC). This B3380 NTC in a 10k divider behind a 1.3x gain stage, 10-bit ADC). This
tool collects reference points - a measured real temperature paired with tool collects reference points - a measured real temperature paired with
the machine's raw ADC readings - and fits a per-machine line to the machine's raw ADC readings - and fits a per-machine line to
@@ -17,7 +17,7 @@ Usage:
temp_calibrate.py supply-watch [seconds] the same for the power supply temp_calibrate.py supply-watch [seconds] the same for the power supply
temp_calibrate.py supply-point <C> [note] sensor (pic/pwr_temp, raw): temp_calibrate.py supply-point <C> [note] sensor (pic/pwr_temp, raw):
temp_calibrate.py supply-fit thermometer on the heatsink vs temp_calibrate.py supply-fit thermometer on the heatsink vs
the raw count; UAPI.md's guess the raw count; the documented guess
raw * 0.08715 - 21 is shown raw * 0.08715 - 21 is shown
beside it. Points go to beside it. Points go to
supply_calibration.json. supply_calibration.json.
@@ -40,7 +40,7 @@ SUPPLY_STORE = data_path('supply_calibration.json')
def supply_guess_c(raw): def supply_guess_c(raw):
"""UAPI.md's unverified guess for pic/pwr_temp.""" """The documented unverified guess for pic/pwr_temp."""
return raw * 0.08715 - 21 return raw * 0.08715 - 21
@@ -55,7 +55,7 @@ def supply_raw(samples=5, delay=1.0):
def uapi_c(raw): def uapi_c(raw):
"""The UAPI.md factory conversion (B-equation NTC behind divider + gain).""" """The documented factory conversion (B-equation NTC behind divider + gain)."""
return degc(raw) return degc(raw)
@@ -149,7 +149,7 @@ def supply_main(mode):
if slope is None: if slope is None:
print('points share one raw value; no fit') print('points share one raw value; no fit')
return 1 return 1
print('fit: degC = %.5f * raw + %.2f (UAPI.md guess: 0.08715 * raw - 21)' % (slope, offset)) print('fit: degC = %.5f * raw + %.2f (documented guess: 0.08715 * raw - 21)' % (slope, offset))
worst = max(abs(p['measured_c'] - supply_guess_c(p['raw'])) for p in pts) worst = max(abs(p['measured_c'] - supply_guess_c(p['raw'])) for p in pts)
print('the guess is off by at most %.1f C at these points' % worst) print('the guess is off by at most %.1f C at these points' % worst)
return 0 return 0