mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
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:
+19
-25
@@ -12,13 +12,7 @@ Read together with:
|
||||
|
||||
| Document | What it settles |
|
||||
|---|---|
|
||||
| `kernel-module-glowforge/UAPI.md` | the pulse-stream feeder contract, sysfs attributes, sensor conversions |
|
||||
| `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 |
|
||||
| [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 |
|
||||
|
||||
## 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
|
||||
operator's button press starts the fire with every arm gate standing),
|
||||
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
|
||||
accumulator's cross-pixel averaging recovers the levels, with no
|
||||
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
|
||||
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
|
||||
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`
|
||||
`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
|
||||
contract** — EV_SW switch map, sensor conversions, hardware single-writer
|
||||
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`
|
||||
(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.
|
||||
- `GET /slots`, `POST /boot`, `POST /update/check|download|apply|upload`,
|
||||
`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.
|
||||
- `GET /logs`, `GET /logs/tail`, `POST /logs/export` — the logging tree
|
||||
(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
|
||||
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
|
||||
(`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
|
||||
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
|
||||
@@ -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`
|
||||
export for issue reports (`POST /logs/export`; `src/sanitize.c` replaces
|
||||
serial, hostname, cloud credentials, panel token, SSID/PSK, IPs, MACs and
|
||||
e-mails with stable placeholders). Design and contract: `SERVICES.md`
|
||||
"Logging".
|
||||
e-mails with stable placeholders). Design and contract:
|
||||
[logging](https://docs.forgefirm.org/technical/forgefirm/logging/).
|
||||
|
||||
## 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
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -744,7 +738,7 @@ is committed.
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -879,7 +873,7 @@ is committed.
|
||||
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
|
||||
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).
|
||||
- **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.
|
||||
@@ -964,7 +958,7 @@ is committed.
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -1104,7 +1098,7 @@ is committed.
|
||||
LOW through a run — the same physical behavior, inverted). The DTS now
|
||||
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
|
||||
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.
|
||||
- **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
|
||||
@@ -1118,7 +1112,7 @@ is committed.
|
||||
hunt is not lid-gated.
|
||||
- **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
|
||||
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
|
||||
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
|
||||
@@ -1190,7 +1184,8 @@ is committed.
|
||||
|
||||
## 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
|
||||
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
|
||||
ready to publish. Repoint the core submodule to
|
||||
upstream if the `step_us_min` sizing fix merges.
|
||||
5. **Update system Phase 5 — recovery refresh.** The remaining phase of
|
||||
`docs/UPDATE-SYSTEM.md` (a refreshed recovery image in boot0); Phases 0–4
|
||||
are done.
|
||||
5. **Update system — recovery refresh.** A refreshed recovery image in
|
||||
boot0; the design is on [install and update](https://docs.forgefirm.org/technical/forgefirm/install-and-update/).
|
||||
6. **Head IRQ (exploratory).**
|
||||
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),
|
||||
@@ -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
|
||||
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"
|
||||
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
|
||||
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
|
||||
|
||||
-488
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
@@ -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
@@ -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 +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 |
@@ -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 |
Reference in New Issue
Block a user