mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-28 01:01: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:
@@ -1,7 +1,8 @@
|
|||||||
# Cold-build reproducibility probe: proves a fresh clone still builds the
|
# Cold-build reproducibility probe: proves a fresh clone still builds the
|
||||||
# release image, and publishes the artifact checksums for comparison
|
# release image, and publishes the artifact checksums for comparison
|
||||||
# against locally built releases. Dispatch-only - releases are built and
|
# against locally built releases. Dispatch-only - releases are built and
|
||||||
# signed on the maintainer's build host (see docs/UPDATE-SYSTEM.md); this
|
# signed on the maintainer's build host (see
|
||||||
|
# https://docs.forgefirm.org/technical/forgefirm/install-and-update/); this
|
||||||
# workflow never produces release artifacts.
|
# workflow never produces release artifacts.
|
||||||
#
|
#
|
||||||
# A cold Yocto build on a 4-core hosted runner takes hours and lives
|
# A cold Yocto build on a 4-core hosted runner takes hours and lives
|
||||||
|
|||||||
-144
@@ -1,144 +0,0 @@
|
|||||||
# Installing OpenGlow/ForgeFIRM
|
|
||||||
|
|
||||||
> # ⚠️ IN DEVELOPMENT — NOT YET RELEASED
|
|
||||||
>
|
|
||||||
> **ForgeFIRM has no public release yet.** No images are published, nothing here
|
|
||||||
> is installable, and there is no supported way to put this on a machine. The
|
|
||||||
> documentation describes the firmware as it is being built and validated on the
|
|
||||||
> bench — it is here to be read, not followed.
|
|
||||||
>
|
|
||||||
> If you come across a file claiming to be a ForgeFIRM image, it did not come
|
|
||||||
> from this project.
|
|
||||||
|
|
||||||
> # What this costs
|
|
||||||
>
|
|
||||||
> Nothing. Not yesterday, not today, and if anyone ever charges you for
|
|
||||||
> it, it wasn't this project. ForgeFIRM is free in both senses: free as
|
|
||||||
> in beer, free as in speech. All of it is public, under licenses (MIT
|
|
||||||
> and GPL) any lawyer will tell you are not a trap.
|
|
||||||
>
|
|
||||||
> No paid tier. No license key, no subscription, no activation, no Pro
|
|
||||||
> edition, no product page, no waitlist, no pre-order, nothing to buy.
|
|
||||||
> Skepticism is fair and cheap to settle: ten minutes with the licenses
|
|
||||||
> and the commit log does it, which beats trust on both cost and
|
|
||||||
> accuracy.
|
|
||||||
>
|
|
||||||
> If someone offers to sell you this firmware, the licenses allow it and
|
|
||||||
> nobody's calling it theft. Just note that what you take home is their
|
|
||||||
> build, not this one: a stranger's code, running a laser that has no
|
|
||||||
> opinion about what it burns. Get it from the source.
|
|
||||||
|
|
||||||
ForgeFIRM installs **alongside** the factory firmware on the stock eMMC,
|
|
||||||
using the factory's own A/B rootfs slot scheme: the installer writes
|
|
||||||
ForgeFIRM to the slot the factory firmware is *not* running from, and
|
|
||||||
switches the bootloader to it. Nothing is repartitioned, the factory
|
|
||||||
`/data` partition (settings, calibration, logs) is untouched, and the
|
|
||||||
booted factory firmware stays installed in the other slot.
|
|
||||||
|
|
||||||
Before anything is overwritten, the installer archives **every** factory
|
|
||||||
firmware version on the machine — both rootfs slots and the recovery
|
|
||||||
boot partitions — to `/data/forgefirm/archive/`, so a full offline
|
|
||||||
factory restore is always possible, no Glowforge cloud required.
|
|
||||||
|
|
||||||
> **Requires a ForgeFIRM release that ships the `forgefirm.fw` asset
|
|
||||||
> (v0.1.0 or later).**
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- [Serial console](SERIAL.md) access. Current factory firmware does not
|
|
||||||
offer SSH, so the install is run at the console (login `root`, no
|
|
||||||
password).
|
|
||||||
- ~300 MB free on `/data` (a factory machine has far more).
|
|
||||||
- Internet access on the machine for the standard flow. For an offline
|
|
||||||
install, place a `forgefirm.fw` on `/data` beforehand and pass its
|
|
||||||
path to the installer.
|
|
||||||
|
|
||||||
**A very important warning: this is experimental software. Use of this
|
|
||||||
software could seriously maim or kill you or others, and voids your
|
|
||||||
warranty. It is not affiliated with or endorsed by Glowforge. Use it at
|
|
||||||
your own risk.**
|
|
||||||
|
|
||||||
## Regulatory and legal
|
|
||||||
|
|
||||||
Installing ForgeFIRM replaces the firmware of a certified laser product.
|
|
||||||
This is disclosure, not legal advice — but understand the categories
|
|
||||||
before you install:
|
|
||||||
|
|
||||||
- **Laser product certification (US).** The factory machine is
|
|
||||||
certified under FDA/CDRH 21 CFR 1040.10 and 1040.11. Modifying it
|
|
||||||
makes **you** the manufacturer of a modified laser product for
|
|
||||||
regulatory purposes; the original accession no longer describes the
|
|
||||||
article you operate.
|
|
||||||
- **CE/UKCA (EU/UK).** The manufacturer's declaration of conformity no
|
|
||||||
longer covers the modified machine.
|
|
||||||
- **Safety listing.** Any UL/ETL or equivalent listing applies to the
|
|
||||||
product as shipped, not as modified.
|
|
||||||
- **Insurance.** Property and liability policies commonly exclude fire
|
|
||||||
loss involving modified equipment. Check yours before running jobs.
|
|
||||||
|
|
||||||
The hardware safety interlocks (lid switches, interlock, power-fault
|
|
||||||
chain) remain active under ForgeFIRM — but the regulatory status of the
|
|
||||||
machine is yours to own once you flash it.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
Log in at the factory console and run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
curl -fL https://raw.githubusercontent.com/ScottW514/forgefirm/master/scripts/install-forgefirm.sh --output /tmp/install-forgefirm.sh
|
|
||||||
sh /tmp/install-forgefirm.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
(For an offline install: `sh /tmp/install-forgefirm.sh /data/forgefirm.fw`)
|
|
||||||
|
|
||||||
One stage, no intermediate reboots. The installer:
|
|
||||||
|
|
||||||
1. Confirms it is running on factory firmware, from a factory eMMC
|
|
||||||
slot, with the factory partition layout.
|
|
||||||
2. Stops the Glowforge services (including the updater).
|
|
||||||
3. Archives every factory slot version and the recovery boot
|
|
||||||
partitions to `/data/forgefirm/archive/` (manifest with checksums;
|
|
||||||
a few minutes each, with progress).
|
|
||||||
4. Downloads the latest `forgefirm.fw` release (or uses the local file
|
|
||||||
you passed) and **verifies its signature** before touching anything.
|
|
||||||
5. Writes ForgeFIRM to the inactive slot with the factory's own `fwup`,
|
|
||||||
then verifies the written filesystem.
|
|
||||||
6. Installs `/data/ffboot` (the boot-slot tool) and switches the saved
|
|
||||||
U-Boot environment to the new slot — the switch is read-back
|
|
||||||
verified; on any failure the machine keeps booting factory firmware.
|
|
||||||
7. Reboots into ForgeFIRM.
|
|
||||||
|
|
||||||
Login is `root`, no password (also via SSH). Change it.
|
|
||||||
|
|
||||||
## Switching firmware
|
|
||||||
|
|
||||||
Both systems stay installed; `ffboot` switches between them (as
|
|
||||||
`/data/ffboot` on factory firmware, on the PATH in ForgeFIRM):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
ffboot -l # inventory: what is in each slot, what boots next
|
|
||||||
ffboot -e # switch to the factory firmware (newest factory slot)
|
|
||||||
ffboot -e2 # switch to ForgeFIRM (slot 2 on a standard install)
|
|
||||||
```
|
|
||||||
|
|
||||||
Switch targets are probed first — `ffboot` refuses to select a slot
|
|
||||||
that does not look bootable (`-f` overrides). Reverting to factory
|
|
||||||
firmware and back requires no reinstall.
|
|
||||||
|
|
||||||
**Routine updates do not use the installer.** Update from the web
|
|
||||||
control panel's System page, which downloads (or accepts an upload of)
|
|
||||||
a signed `forgefirm.fw` release, verifies it, and applies it to the
|
|
||||||
inactive slot. Rerunning the installer is only for recovering a broken
|
|
||||||
ForgeFIRM install: switch to factory firmware and run it again — it
|
|
||||||
skips archives it already has and simply rewrites the ForgeFIRM slot.
|
|
||||||
|
|
||||||
## Upgrading from a legacy dual-partition install
|
|
||||||
|
|
||||||
Machines installed with the previous (partition-carving) installer
|
|
||||||
migrate automatically: run this installer from the factory firmware;
|
|
||||||
on ForgeFIRM's first boot from its new slot, the legacy partition is
|
|
||||||
reclaimed and `/data` grows back to the full factory size. `/data`
|
|
||||||
contents are preserved throughout.
|
|
||||||
|
|
||||||
**NOTE:** This firmware is in beta. It mostly works. It is for
|
|
||||||
experimentation purposes — not for production. Expect problems.
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
# OpenGlow/ForgeFIRM Firmware for Glowforge
|
# OpenGlow/ForgeFIRM Firmware for Glowforge
|
||||||
|
|
||||||
> # ⚠️ IN DEVELOPMENT — NOT YET RELEASED
|
> # ⚠️ BETA
|
||||||
>
|
>
|
||||||
> **ForgeFIRM has no public release, yet.** Nothing here
|
> **ForgeFIRM is in beta.** Every release below 0.1.0 is a beta release.
|
||||||
> is installable.
|
> Expect problems, and expect frequent updates. Upgrade whenever a newer
|
||||||
> The documentation describes the firmware as it is being built
|
> release is available, and report what you find on the
|
||||||
> and validated on the bench. It is here to be read, not followed.
|
> [community forum](https://community.openglow.org).
|
||||||
> The first public release is expected in September 2026. If you are interested in being an early tester, please reach out to the developer.
|
|
||||||
|
|
||||||
Open-source firmware for Glowforge brand CNC lasers. ForgeFIRM replaces the
|
Open-source firmware for Glowforge brand CNC lasers. ForgeFIRM replaces the
|
||||||
cloud-dependent factory software on the **stock control board** — no hardware
|
cloud-dependent factory software on the **stock control board** — no hardware
|
||||||
@@ -14,13 +13,13 @@ modification — and gives the machine a local controller, a local web control
|
|||||||
panel, and a standard Grbl interface.
|
panel, and a standard Grbl interface.
|
||||||
|
|
||||||
* [Latest Release](https://github.com/ScottW514/forgefirm/releases)
|
* [Latest Release](https://github.com/ScottW514/forgefirm/releases)
|
||||||
* [Installation Instructions](https://github.com/ScottW514/forgefirm/blob/master/INSTALL.md)
|
* [Installation Instructions](https://docs.forgefirm.org/install/)
|
||||||
* [Build Instructions](https://docs.forgefirm.org/developers/building/)
|
* [Build Instructions](https://docs.forgefirm.org/developers/building/)
|
||||||
* [Connecting LightBurn](https://github.com/ScottW514/forgefirm/blob/master/docs/LIGHTBURN.md)
|
* [Connecting LightBurn](https://docs.forgefirm.org/usage/lightburn/)
|
||||||
* [How motion and the laser are driven](https://github.com/ScottW514/forgefirm/blob/master/docs/MOTION.md)
|
* [How motion and the laser are driven](https://docs.forgefirm.org/technical/forgefirm/)
|
||||||
* [How cooling and airflow work](https://github.com/ScottW514/forgefirm/blob/master/docs/COOLING.md)
|
* [How cooling and airflow work](https://docs.forgefirm.org/technical/forgefirm/cooling-engine/)
|
||||||
* [The cameras and the video stream](https://github.com/ScottW514/forgefirm/blob/master/docs/VIDEO.md)
|
* [The cameras and the video stream](https://docs.forgefirm.org/usage/cameras/)
|
||||||
* [How the laser safing works](https://github.com/ScottW514/forgefirm/blob/master/docs/SAFETY.md)
|
* [How the laser safing works](https://docs.forgefirm.org/safety/)
|
||||||
* [How a release is tested before being accepted](https://docs.forgefirm.org/developers/acceptance/)
|
* [How a release is tested before being accepted](https://docs.forgefirm.org/developers/acceptance/)
|
||||||
* [Community Support](https://community.openglow.org)
|
* [Community Support](https://community.openglow.org)
|
||||||
|
|
||||||
@@ -76,8 +75,8 @@ you get to read what you're running.
|
|||||||
|
|
||||||
**These machines contain a CO₂ laser: it burns, blinds, and starts
|
**These machines contain a CO₂ laser: it burns, blinds, and starts
|
||||||
fires.** Never defeat the lid switches or interlock. Never leave a
|
fires.** Never defeat the lid switches or interlock. Never leave a
|
||||||
running job unattended. Keep a fire extinguisher within reach. Read [this](docs/LIGHTBURN.md#before-you-cut--safety-read-this-first) before you cut
|
running job unattended. Keep a fire extinguisher within reach. Read [this](https://docs.forgefirm.org/safety/) before you cut
|
||||||
your first job, and read this [regulatory and legal](INSTALL.md#regulatory-and-legal) section before installing. [This](docs/SAFETY.md) document describes the hardware safety chain and the software gates ForgeFIRM stacks on it.
|
your first job, and read this [regulatory and legal](https://docs.forgefirm.org/install/#regulatory-and-legal) section before installing. [This](https://docs.forgefirm.org/technical/machine/safing-chain/) page describes the hardware safety chain and the software gates ForgeFIRM stacks on it.
|
||||||
|
|
||||||
**THIS IS EXPERIMENTAL SOFTWARE
|
**THIS IS EXPERIMENTAL SOFTWARE
|
||||||
Use of this software
|
Use of this software
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# Glowforge Serial Port Access
|
|
||||||
Older Glowforges have a Micro-USB serial port connector on the control board (J13 located in the lower left of the board picture below). If you are fortunate enough to have one of those units, you can just plug that into your PC. It's a standard [FTDI FT230](https://ftdichip.com/products/ft230xq/) serial-to-USB adapter and the driver is included in most operating systems.
|
|
||||||
|
|
||||||
If you are not that fortunate, you can follow these instructions:
|
|
||||||
|
|
||||||
First, you will need a soldering iron. No getting around that one. Next, you need a FTDI [TTL-232RG-VREG1V8-WE](https://www.ftdichip.com/Products/Cables/USBTTLSerial.htm) USB to Serial Port adapter. It is important that you get the 1.8V model. If you get a 3.3V or 5V version you will irreversibly destroy your control board. You can purchase this directly from FTDI, Amazon, or most electronic supply shops.
|
|
||||||
|
|
||||||
When you receive your adapter, you'll need to prep it. We only require three of the lead wires. The others should be trimmed (to unequal lengths, lest they short out when you heatshrink/tape them up).
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
We'll be connecting to the microprocessor's serial console port test points: RXD: D3B, TXD: D3D.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
I recommend soldering the ground wire first to the ground pad located to the upper left of the microprocessor (see picture below). Clip the leads for the TXD and RXD to be no longer a millimeter before soldering. This helps to avoid unwanted contact with surrounding items.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
After you've soldered all the leads, secure the cable to the PCB with a tie wrap, as pictured. This is important. The test points will not stand much mechanical stress, and will rip off the board easily. Don't worry, if this happens to you, they are still accessible from the bottom of the board - second chance.
|
|
||||||
|
|
||||||
From there, connect the USB cable to your computer and fire up your favorite terminal (Putty works well). The serial settings are 115,200 8N1. Be sure to select a color terminal with UTF-8 encoding if you want the best experience from the OpenGlow firmware.
|
|
||||||
|
|
||||||
NOTE: You will need to keep the serial port adpater powered on at all times when you are operating the Glowforge. If it is not powered, it draws down the 1.8V bus on the control board and prevents it from booting. If you don't have always have a computer near by, you can plug it into a USB charging adapter.
|
|
||||||
+19
-25
@@ -12,13 +12,7 @@ Read together with:
|
|||||||
|
|
||||||
| Document | What it settles |
|
| Document | What it settles |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `kernel-module-glowforge/UAPI.md` | the pulse-stream feeder contract, sysfs attributes, sensor conversions |
|
| [docs.forgefirm.org](https://docs.forgefirm.org/) | the documentation site: safety, install, usage, the machine as built, how ForgeFIRM works (the kernel module and the pulse feeder contract, the forgectrl machine-services contract, the cooling engine, the video pipeline, cloud mode), and the developer pages: build, release flow, acceptance, tests, the bench runbook |
|
||||||
| `forgectrl/docs/SERVICES.md` | the machine-services contract: switch map, hardware ownership, cooling channels, mode supervision, pulse-device ownership, logging |
|
|
||||||
| `docs/SAFETY.md` | the hardware safing chain, decoded |
|
|
||||||
| `docs/VIDEO.md` | the cameras as users meet them: endpoints, delivered geometry, and what the sensors can do that ForgeFIRM does not send |
|
|
||||||
| `docs/LIGHTBURN.md`, `docs/UPDATE-SYSTEM.md`, `INSTALL.md` | sender setup, A/B update system, install |
|
|
||||||
| [docs.forgefirm.org/developers](https://docs.forgefirm.org/developers/) | build, release flow, tests, the bench runbook |
|
|
||||||
| `python3-gfhardware/forgefirm-app/docs/CLOUD.md` | cloud mode, including its own open items |
|
|
||||||
|
|
||||||
## Where the project stands
|
## Where the project stands
|
||||||
|
|
||||||
@@ -263,7 +257,7 @@ the dose-curve recorder streams the ladder job itself from one Record
|
|||||||
press (absolute from X0 Y0, refused while a sender is connected; the
|
press (absolute from X0 Y0, refused while a sender is connected; the
|
||||||
operator's button press starts the fire with every arm gate standing),
|
operator's button press starts the fire with every arm gate standing),
|
||||||
records the tube current and the head thermopile, fits the rungs, and
|
records the tube current and the head thermopile, fits the rungs, and
|
||||||
Apply writes the result (`forgectrl/docs/SERVICES.md`). Rasters hold their tonality down
|
Apply writes the result ([forgectrl](https://docs.forgefirm.org/technical/forgefirm/forgectrl/)). Rasters hold their tonality down
|
||||||
to ~14 pulse slots per pixel (508 DPI at 6000 mm/min): the dither
|
to ~14 pulse slots per pixel (508 DPI at 6000 mm/min): the dither
|
||||||
accumulator's cross-pixel averaging recovers the levels, with no
|
accumulator's cross-pixel averaging recovers the levels, with no
|
||||||
visible dither pattern.
|
visible dither pattern.
|
||||||
@@ -325,7 +319,7 @@ report, overrides, driver version, `ts_mono` for age; on change plus a
|
|||||||
`grbl` block only while it supervises a live GRBL controller, serves
|
`grbl` block only while it supervises a live GRBL controller, serves
|
||||||
the settings file at `GET /grbl/settings`, and the panel's GRBL card
|
the settings file at `GET /grbl/settings`, and the panel's GRBL card
|
||||||
renders it. Position stays out: it changes per segment and is served
|
renders it. Position stays out: it changes per segment and is served
|
||||||
from the kernel counters. Contract: `forgectrl/docs/SERVICES.md`.
|
from the kernel counters. Contract: [forgectrl](https://docs.forgefirm.org/technical/forgefirm/forgectrl/).
|
||||||
|
|
||||||
**Emission evidence.** `cnc/laser_on_sampled` (surfaced as `/status`
|
**Emission evidence.** `cnc/laser_on_sampled` (surfaced as `/status`
|
||||||
`laser.emission_samples`) is the reliable live-emission witness; emission
|
`laser.emission_samples`) is the reliable live-emission witness; emission
|
||||||
@@ -423,7 +417,7 @@ sysvinit script from the repo's `init/`; bench builds cross-compile with
|
|||||||
`forgefirm/scripts/bench/build-forgectrl.sh`. The **machine-services
|
`forgefirm/scripts/bench/build-forgectrl.sh`. The **machine-services
|
||||||
contract** — EV_SW switch map, sensor conversions, hardware single-writer
|
contract** — EV_SW switch map, sensor conversions, hardware single-writer
|
||||||
ownership, cooling channels, mode supervision, pulse-device ownership, logging
|
ownership, cooling channels, mode supervision, pulse-device ownership, logging
|
||||||
— is `forgectrl/docs/SERVICES.md`.
|
— is [forgectrl](https://docs.forgefirm.org/technical/forgefirm/forgectrl/) on the documentation site.
|
||||||
|
|
||||||
Every state-changing endpoint requires the first-boot bearer token in `/data`
|
Every state-changing endpoint requires the first-boot bearer token in `/data`
|
||||||
(embedded in the panel), a Host address-literal check, and
|
(embedded in the panel), a Host address-literal check, and
|
||||||
@@ -487,7 +481,7 @@ One ulfius daemon serves it all:
|
|||||||
snapshot answer 409 while the lid is open.
|
snapshot answer 409 while the lid is open.
|
||||||
- `GET /slots`, `POST /boot`, `POST /update/check|download|apply|upload`,
|
- `GET /slots`, `POST /boot`, `POST /update/check|download|apply|upload`,
|
||||||
`GET /update/status`, `POST /restore/factory`, `POST /system/reboot` — the
|
`GET /update/status`, `POST /restore/factory`, `POST /system/reboot` — the
|
||||||
A/B update manager (`docs/UPDATE-SYSTEM.md`). Upload is auth + idle + job
|
A/B update manager ([install and update](https://docs.forgefirm.org/technical/forgefirm/install-and-update/)). Upload is auth + idle + job
|
||||||
gated; a booted-slot write is refused under any `root=` spelling.
|
gated; a booted-slot write is refused under any `root=` spelling.
|
||||||
- `GET /logs`, `GET /logs/tail`, `POST /logs/export` — the logging tree
|
- `GET /logs`, `GET /logs/tail`, `POST /logs/export` — the logging tree
|
||||||
(below).
|
(below).
|
||||||
@@ -511,7 +505,7 @@ open lid refuses stream and snapshot with HTTP 409 and a lid opened mid-capture
|
|||||||
tears the pipeline down; `gfhardware.cam.capture()` enforces the same rule for
|
tears the pipeline down; `gfhardware.cam.capture()` enforces the same rule for
|
||||||
the cloud client's direct-V4L2 fallback and raises `LidOpen`. No setting
|
the cloud client's direct-V4L2 fallback and raises `LidOpen`. No setting
|
||||||
disables it, and the factory's lid-open focus hunt now fails as a result
|
disables it, and the factory's lid-open focus hunt now fails as a result
|
||||||
(`docs/VIDEO.md` §2, `forgectrl/docs/SERVICES.md`). Geometry, Bayer depth and
|
([the video pipeline](https://docs.forgefirm.org/technical/forgefirm/video-pipeline/)). Geometry, Bayer depth and
|
||||||
the manual control set come from a **sensor profile** chosen by whichever
|
the manual control set come from a **sensor profile** chosen by whichever
|
||||||
driver bound on that camera's I2C bus, so one image serves both the 5 MP
|
driver bound on that camera's I2C bus, so one image serves both the 5 MP
|
||||||
OV5648 (2592×1944) and the 8 MP OV8856 (3264×2448) — both 8-bit BGGR, so the
|
OV5648 (2592×1944) and the 8 MP OV8856 (3264×2448) — both 8-bit BGGR, so the
|
||||||
@@ -623,8 +617,8 @@ settings **applied at reboot** — the panel's Logs tab shows configured vs.
|
|||||||
effective and offers the reboot, plus a live viewer and a sanitized `tar.gz`
|
effective and offers the reboot, plus a live viewer and a sanitized `tar.gz`
|
||||||
export for issue reports (`POST /logs/export`; `src/sanitize.c` replaces
|
export for issue reports (`POST /logs/export`; `src/sanitize.c` replaces
|
||||||
serial, hostname, cloud credentials, panel token, SSID/PSK, IPs, MACs and
|
serial, hostname, cloud credentials, panel token, SSID/PSK, IPs, MACs and
|
||||||
e-mails with stable placeholders). Design and contract: `SERVICES.md`
|
e-mails with stable placeholders). Design and contract:
|
||||||
"Logging".
|
[logging](https://docs.forgefirm.org/technical/forgefirm/logging/).
|
||||||
|
|
||||||
## Release acceptance (forgetest, port 8090)
|
## Release acceptance (forgetest, port 8090)
|
||||||
|
|
||||||
@@ -710,7 +704,7 @@ is committed.
|
|||||||
GPIO 7 to GND. The lid contact (NC) goes in series with the lid-switch
|
GPIO 7 to GND. The lid contact (NC) goes in series with the lid-switch
|
||||||
loop at J4.12/13; the button contact (NO) across the front button input
|
loop at J4.12/13; the button contact (NO) across the front button input
|
||||||
at J5 (BTN and its 12 V); the interlock contact (NC) in the remote
|
at J5 (BTN and its 12 V); the interlock contact (NC) in the remote
|
||||||
interlock loop at J8 (SAFETY.md; J6 is the speaker). The machine's 3.3 V
|
interlock loop at J8 ([the safing chain](https://docs.forgefirm.org/technical/machine/safing-chain/); J6 is the speaker). The machine's 3.3 V
|
||||||
rail carries the three coils with room to spare. The DevKit and the
|
rail carries the three coils with room to spare. The DevKit and the
|
||||||
machine share a ground through the modules, so the DevKit is powered from
|
machine share a ground through the modules, so the DevKit is powered from
|
||||||
a USB wall adapter. The interposer harness itself is bench-local and is
|
a USB wall adapter. The interposer harness itself is bench-local and is
|
||||||
@@ -744,7 +738,7 @@ is committed.
|
|||||||
every fan held to a measured floor with a fault, not a pause, for the
|
every fan held to a measured floor with a fault, not a pause, for the
|
||||||
session; a coolant critical line above the ceiling's pause; the board
|
session; a coolant critical line above the ceiling's pause; the board
|
||||||
temperatures watched per job; and the rest of the envelope declared, tag by
|
temperatures watched per job; and the rest of the envelope declared, tag by
|
||||||
tag, in `CLOUD.md` "The pulse header".
|
tag, on [the factory firmware](https://docs.forgefirm.org/technical/machine/factory-firmware/) under "The pulse header".
|
||||||
- **Board temperatures at idle** (room ~22 C, machine on for hours): the
|
- **Board temperatures at idle** (room ~22 C, machine on for hours): the
|
||||||
chassis LM75 reads **29.0 C**, `pic/pwr_temp` reads **589 raw** (the
|
chassis LM75 reads **29.0 C**, `pic/pwr_temp` reads **589 raw** (the
|
||||||
unverified guess `raw * 0.08715 - 21` would make that 30.3 C), the SoC die
|
unverified guess `raw * 0.08715 - 21` would make that 30.3 C), the SoC die
|
||||||
@@ -879,7 +873,7 @@ is committed.
|
|||||||
magnitude to spare. Bounded queue depth plus `SCHED_FIFO` for the feeder is
|
magnitude to spare. Bounded queue depth plus `SCHED_FIFO` for the feeder is
|
||||||
the design; RT is worth revisiting only if the underrun bench ever
|
the design; RT is worth revisiting only if the underrun bench ever
|
||||||
contradicts this arithmetic.
|
contradicts this arithmetic.
|
||||||
- Byte layout and stream rules: see the UAPI.md feeder contract
|
- Byte layout and stream rules: see [the pulse feeder contract](https://docs.forgefirm.org/technical/forgefirm/pulse-feeder-contract/)
|
||||||
(authoritative).
|
(authoritative).
|
||||||
- **Z**: bit 6 SET = lens UP = +Z (hardware-verified). Home = hall trigger at
|
- **Z**: bit 6 SET = lens UP = +Z (hardware-verified). Home = hall trigger at
|
||||||
TOP; usable travel ≈ 30 half-steps ≈ 10.6 mm ≈ 0.417"; 0.3534 mm/half-step.
|
TOP; usable travel ≈ 30 half-steps ≈ 10.6 mm ≈ 0.417"; 0.3534 mm/half-step.
|
||||||
@@ -964,7 +958,7 @@ is committed.
|
|||||||
33 °C, resume 31 °C (factory job-header CMrx/…); the factory's low side
|
33 °C, resume 31 °C (factory job-header CMrx/…); the factory's low side
|
||||||
(floors ≈1.0/4.0 °C, ~16 °C warm-up gate) is not implemented yet. The
|
(floors ≈1.0/4.0 °C, ~16 °C warm-up gate) is not implemented yet. The
|
||||||
coolant thermistor conversion is the factory B-equation recovered from the
|
coolant thermistor conversion is the factory B-equation recovered from the
|
||||||
v2.6.0 binary — derivation in `kernel-module-glowforge/UAPI.md`; the old
|
v2.6.0 binary — derivation on [sensors](https://docs.forgefirm.org/technical/machine/sensors/); the old
|
||||||
UAPI "best guess" linear formula was 3–5 °C high and everything derived from
|
UAPI "best guess" linear formula was 3–5 °C high and everything derived from
|
||||||
it had to be re-derived. The flow check's bands hold from 19 to 27 C, the
|
it had to be re-derived. The flow check's bands hold from 19 to 27 C, the
|
||||||
loop heater's ceiling in a 20 C room, with the margin widening warm; above
|
loop heater's ceiling in a 20 C room, with the margin widening warm; above
|
||||||
@@ -1104,7 +1098,7 @@ is committed.
|
|||||||
LOW through a run — the same physical behavior, inverted). The DTS now
|
LOW through a run — the same physical behavior, inverted). The DTS now
|
||||||
declares it active-low, and the former `estop_halts_motion` /
|
declares it active-low, and the former `estop_halts_motion` /
|
||||||
`MOTION.ESTOP_HALTS_MOTION` opt-in is gone: a real e-stop belongs in the
|
`MOTION.ESTOP_HALTS_MOTION` opt-in is gone: a real e-stop belongs in the
|
||||||
lid-switch chain (`docs/SAFETY.md`). Doors/door1/door2 stay stable during
|
lid-switch chain ([the safing chain](https://docs.forgefirm.org/technical/machine/safing-chain/)). Doors/door1/door2 stay stable during
|
||||||
motion.
|
motion.
|
||||||
- **Factory job behavior on the lid and the button, measured on 2.6.0-2228**
|
- **Factory job behavior on the lid and the button, measured on 2.6.0-2228**
|
||||||
(bench session 2026-08-16; this is what ForgeFIRM's parity policy
|
(bench session 2026-08-16; this is what ForgeFIRM's parity policy
|
||||||
@@ -1118,7 +1112,7 @@ is committed.
|
|||||||
hunt is not lid-gated.
|
hunt is not lid-gated.
|
||||||
- **The hardware button latch is what makes the armed window honest.** A lid
|
- **The hardware button latch is what makes the armed window honest.** A lid
|
||||||
open SETs it (set-dominant), and it stays SET until the lid is closed, the SoC
|
open SETs it (set-dominant), and it stays SET until the lid is closed, the SoC
|
||||||
lock is released **and the button is pressed** (`docs/SAFETY.md`). So a policy
|
lock is released **and the button is pressed** ([the safing chain](https://docs.forgefirm.org/technical/machine/safing-chain/)). So a policy
|
||||||
that cancels the job on a lid open and re-arms only through a fresh button
|
that cancels the job on a lid open and re-arms only through a fresh button
|
||||||
press keeps software and hardware in agreement by construction; one that
|
press keeps software and hardware in agreement by construction; one that
|
||||||
resumes a job after a lid open leaves the beam blocked in hardware while
|
resumes a job after a lid open leaves the beam blocked in hardware while
|
||||||
@@ -1190,7 +1184,8 @@ is committed.
|
|||||||
|
|
||||||
## Next work
|
## Next work
|
||||||
|
|
||||||
Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
Open items only. Anything closed is in `CAMPAIGN-LOG.md`. Open items (bugs,
|
||||||
|
feature requests, enhancements) will eventually be tracked as GitHub issues.
|
||||||
|
|
||||||
1. **Limit-switch homing.** The planned second homing method (`$22` stays 0
|
1. **Limit-switch homing.** The planned second homing method (`$22` stays 0
|
||||||
until it lands).
|
until it lands).
|
||||||
@@ -1222,9 +1217,8 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
|||||||
first GitHub release, per the site (Developers, "Release flow"), once
|
first GitHub release, per the site (Developers, "Release flow"), once
|
||||||
ready to publish. Repoint the core submodule to
|
ready to publish. Repoint the core submodule to
|
||||||
upstream if the `step_us_min` sizing fix merges.
|
upstream if the `step_us_min` sizing fix merges.
|
||||||
5. **Update system Phase 5 — recovery refresh.** The remaining phase of
|
5. **Update system — recovery refresh.** A refreshed recovery image in
|
||||||
`docs/UPDATE-SYSTEM.md` (a refreshed recovery image in boot0); Phases 0–4
|
boot0; the design is on [install and update](https://docs.forgefirm.org/technical/forgefirm/install-and-update/).
|
||||||
are done.
|
|
||||||
6. **Head IRQ (exploratory).**
|
6. **Head IRQ (exploratory).**
|
||||||
Owed for the head IRQ, only if a coarse hardware interrupt is wanted
|
Owed for the head IRQ, only if a coarse hardware interrupt is wanted
|
||||||
instead of the poll: arm the accel bit in the head MCU (reg 0x03/0x04),
|
instead of the poll: arm the accel bit in the head MCU (reg 0x03/0x04),
|
||||||
@@ -1249,7 +1243,7 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
|||||||
(`cloud_pause_backtrack_ticks` 2000, `cloud_resume_lead_ticks` 1950), and
|
(`cloud_pause_backtrack_ticks` 2000, `cloud_resume_lead_ticks` 1950), and
|
||||||
the kernel offers the same mechanism to a live feed, bounded by the ring's
|
the kernel offers the same mechanism to a live feed, bounded by the ring's
|
||||||
retained history (`cnc/max_backtrack`; the facts bank "SDMA pulse engine"
|
retained history (`cnc/max_backtrack`; the facts bank "SDMA pulse engine"
|
||||||
and `UAPI.md`). What is not settled is the bookkeeping above it: a
|
and [the pulse feeder contract](https://docs.forgefirm.org/technical/forgefirm/pulse-feeder-contract/)). What is not settled is the bookkeeping above it: a
|
||||||
backward run moves the head and the kernel's counters while grblHAL's
|
backward run moves the head and the kernel's counters while grblHAL's
|
||||||
planner still holds a partly executed block, so borrowing the mechanism
|
planner still holds a partly executed block, so borrowing the mechanism
|
||||||
means reconciling the two, and a GRBL cut runs a much shorter queue than a
|
means reconciling the two, and a GRBL cut runs a much shorter queue than a
|
||||||
|
|||||||
-488
@@ -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 |
@@ -189,8 +189,8 @@ TOOLS = [
|
|||||||
["watch", "point", "fit", "supply-watch", "supply-point", "supply-fit"]),
|
["watch", "point", "fit", "supply-watch", "supply-point", "supply-fit"]),
|
||||||
_arg("value", "str", None, "point: the thermometer reading in C; watch: seconds (default 60)")],
|
_arg("value", "str", None, "point: the thermometer reading in C; watch: seconds (default 60)")],
|
||||||
"desc": "Pairs a measured temperature with averaged raw readings; fits a per-machine line. The coolant "
|
"desc": "Pairs a measured temperature with averaged raw readings; fits a per-machine line. The coolant "
|
||||||
"sensors, or the power supply's (thermometer on its heatsink, against the unverified guess in "
|
"sensors, or the power supply's (thermometer on its heatsink, against the documented unverified "
|
||||||
"UAPI.md). Points accumulate in the bench data directory."},
|
"guess). Points accumulate in the bench data directory."},
|
||||||
{"id": "fan-test", "title": "Fan/coolant bench", "script": "fan_test.py",
|
{"id": "fan-test", "title": "Fan/coolant bench", "script": "fan_test.py",
|
||||||
"safety": "dry", "where": "board", "ported": True, "args": [],
|
"safety": "dry", "where": "board", "ported": True, "args": [],
|
||||||
"desc": "Snapshots fan PWMs/tachs/temps, drives M8 -> cut fans, M9 -> cooldown -> idle; the tach "
|
"desc": "Snapshots fan PWMs/tachs/temps, drives M8 -> cut fans, M9 -> cooldown -> idle; the tach "
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from ..catalog import test
|
|||||||
from .. import hw
|
from .. import hw
|
||||||
from ..runner import Failed
|
from ..runner import Failed
|
||||||
|
|
||||||
# interlock_circuit bits (UAPI.md): bit 3 = the driven latch line, set = locked.
|
# interlock_circuit bits (docs.forgefirm.org, the kernel module page): bit 3 = the driven latch line, set = locked.
|
||||||
LATCH_BIT = 1 << 3
|
LATCH_BIT = 1 << 3
|
||||||
|
|
||||||
TICK_HZ = 10000
|
TICK_HZ = 10000
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
# ForgeFIRM — kas build configuration (factory Glowforge control board)
|
# ForgeFIRM — kas build configuration (factory Glowforge control board)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# The forgefirm repo is the BASE: it controls the build, the output firmware
|
# The forgefirm repo is the BASE: it controls the build, the output firmware
|
||||||
# images land here (build/tmp/deploy/images/glowforge/), and the install docs
|
# images land here (build/tmp/deploy/images/glowforge/), and the status docs
|
||||||
# live here (INSTALL.md, SERIAL.md). The build and release procedure is on the
|
# live here (docs/BRINGUP.md, docs/CAMPAIGN-LOG.md). The install, build and
|
||||||
# documentation site: https://docs.forgefirm.org/developers/
|
# release procedures are on the documentation site: https://docs.forgefirm.org/
|
||||||
#
|
#
|
||||||
# Target : Yocto Scarthgap (5.0 LTS) + linux-fslc 6.12 (mainline LTS)
|
# Target : Yocto Scarthgap (5.0 LTS) + linux-fslc 6.12 (mainline LTS)
|
||||||
# Machine: glowforge (i.MX6 Solo SOM inside Basic/Plus/Pro)
|
# Machine: glowforge (i.MX6 Solo SOM inside Basic/Plus/Pro)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ page's takeover does that; from a host, stop them first.
|
|||||||
| `flow_characterize.py` | Coolant flow characterization using the factory temperature curve (board or host; forgectrl and controller stopped): baseline → flow → no-flow → recovery, printing the ΔT bands and their separation. Takes the heater duty as an argument (`flow_characterize.py 30`); aborts if downstream passes 45 °C. |
|
| `flow_characterize.py` | Coolant flow characterization using the factory temperature curve (board or host; forgectrl and controller stopped): baseline → flow → no-flow → recovery, printing the ΔT bands and their separation. Takes the heater duty as an argument (`flow_characterize.py 30`); aborts if downstream passes 45 °C. |
|
||||||
| `flow_matrix.py` | **The flow-detection design matrix** (board or host; forgectrl and controller stopped; with `flow_sampler.py` from `/usr/share/forgetest/bench/`): duty × duration × flow/no-flow, every run from a common cooled baseline, interleaved repeats. One heating trace yields the metric at every candidate duration, so cost and precision come from the same 60 runs. Prints a cost table, a precision table (mean±sd, worst-case margin, d′) and a ranked shortlist. `flow_matrix.py [duties] [repeats]` (or env `FM_DUTIES`, `FM_REPEATS`, `FM_RESULTS`); results/log in the bench data directory, resumable. |
|
| `flow_matrix.py` | **The flow-detection design matrix** (board or host; forgectrl and controller stopped; with `flow_sampler.py` from `/usr/share/forgetest/bench/`): duty × duration × flow/no-flow, every run from a common cooled baseline, interleaved repeats. One heating trace yields the metric at every candidate duration, so cost and precision come from the same 60 runs. Prints a cost table, a precision table (mean±sd, worst-case margin, d′) and a ranked shortlist. `flow_matrix.py [duties] [repeats]` (or env `FM_DUTIES`, `FM_REPEATS`, `FM_RESULTS`); results/log in the bench data directory, resumable. |
|
||||||
| `flow_sustained.py` | Long-run test of the real re-check cadence via M8 (board or host; controller running): counts verdicts/false faults against the configured `cool_flow_rise` and tracks whether the loop accumulates heat. `flow_sustained.py [minutes]`. |
|
| `flow_sustained.py` | Long-run test of the real re-check cadence via M8 (board or host; controller running): counts verdicts/false faults against the configured `cool_flow_rise` and tracks whether the loop accumulates heat. `flow_sustained.py [minutes]`. |
|
||||||
| `temp_calibrate.py` (`supply-*` modes) | The power supply's sensor (`pic/pwr_temp`, raw) against a thermometer on its heatsink: `supply-watch`, `supply-point <C>`, `supply-fit`; the fit is printed beside `UAPI.md`'s unverified guess. Three points during a long cut settle it. |
|
| `temp_calibrate.py` (`supply-*` modes) | The power supply's sensor (`pic/pwr_temp`, raw) against a thermometer on its heatsink: `supply-watch`, `supply-point <C>`, `supply-fit`; the fit is printed beside the documented unverified guess ([sensors](https://docs.forgefirm.org/technical/machine/sensors/)). Three points during a long cut settle it. |
|
||||||
| `critical_tier_drill.py` | The coolant critical tier on a rising temperature (board or host): sets the ceiling, the resume gate and the critical line a few tenths above the live upstream reading and lets the engine's own flow-check heater warm the loop through them inside one `M8` session, expecting `OVERTEMP` at the ceiling and then `CRITICAL` (fire blocked, hold, no resume) with the fault ending at `M9`; restores the settings and cycles a session so the engine re-reads them. Results as JSON in the bench data directory. |
|
| `critical_tier_drill.py` | The coolant critical tier on a rising temperature (board or host): sets the ceiling, the resume gate and the critical line a few tenths above the live upstream reading and lets the engine's own flow-check heater warm the loop through them inside one `M8` session, expecting `OVERTEMP` at the ceiling and then `CRITICAL` (fire blocked, hold, no resume) with the fault ending at `M9`; restores the settings and cycles a session so the engine re-reads them. Results as JSON in the bench data directory. |
|
||||||
| `aa_offset_check.py` | Coolant offset correction under the run airflow (board; controller running; dark, no press): M8 brings the fans to the run profile while the raw coolant counts, `/status` and the engine's readings are averaged before, during and after; with `cool_aa_offset_counts` at the machine's value the readings hold still while the raw counts step, at zero they drop by about a degree. `aa_offset_check.py [dwell_s]`. |
|
| `aa_offset_check.py` | Coolant offset correction under the run airflow (board; controller running; dark, no press): M8 brings the fans to the run profile while the raw coolant counts, `/status` and the engine's readings are averaged before, during and after; with `cool_aa_offset_counts` at the machine's value the readings hold still while the raw counts step, at zero they drop by about a degree. `aa_offset_check.py [dwell_s]`. |
|
||||||
| `offset_probe.py` | Coolant-sensor offset probe (board; forgectrl idle; dark, no press): switches one actuator at a time (exhaust at 100/50/25 %, intakes, air assist, purge, heater, pump, TEC, lid lamp, then all run fans) with both thermistors sampled at 25 Hz and scores the common-mode step at every edge and the level toggling inside every dwell; `offset_probe.py ladder` runs the air-assist duty ladder alone. Every value is restored on exit. JSON record in the bench data directory. |
|
| `offset_probe.py` | Coolant-sensor offset probe (board; forgectrl idle; dark, no press): switches one actuator at a time (exhaust at 100/50/25 %, intakes, air assist, purge, heater, pump, TEC, lid lamp, then all run fans) with both thermistors sampled at 25 Hz and scores the common-mode step at every edge and the level toggling inside every dwell; `offset_probe.py ladder` runs the air-assist duty ladder alone. Every value is restored on exit. JSON record in the bench data directory. |
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ board(cmd) run a shell command on the machine and return its stdout
|
|||||||
(local: sh -c; host: ssh, or the client named by GF_SSH,
|
(local: sh -c; host: ssh, or the client named by GF_SSH,
|
||||||
e.g. GF_SSH='wsl -d <distro> -- ssh').
|
e.g. GF_SSH='wsl -d <distro> -- ssh').
|
||||||
degc(raw) the factory B-equation coolant conversion
|
degc(raw) the factory B-equation coolant conversion
|
||||||
(kernel-module-glowforge/UAPI.md).
|
(docs.forgefirm.org, the sensors page).
|
||||||
data_path(f) where a tool keeps its data files: FORGETEST_BENCH_DATA
|
data_path(f) where a tool keeps its data files: FORGETEST_BENCH_DATA
|
||||||
when set (the bench page passes <data>/bench/), else next
|
when set (the bench page passes <data>/bench/), else next
|
||||||
to the tool.
|
to the tool.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ full download with the GF1 header (magic at [1:4], total header length at
|
|||||||
[4:8], then 8-byte key/value records). Header settings (STfr, XSmm) override
|
[4:8], then 8-byte key/value records). Header settings (STfr, XSmm) override
|
||||||
the --rate/--mode defaults when present.
|
the --rate/--mode defaults when present.
|
||||||
|
|
||||||
Byte layout (kernel-module-glowforge/UAPI.md, hardware-verified):
|
Byte layout (docs.forgefirm.org, the step engine page; hardware-verified):
|
||||||
bit7 set -> laser power byte (low 7 bits = power, no steps this tick)
|
bit7 set -> laser power byte (low 7 bits = power, no steps this tick)
|
||||||
bit0 X_STEP, bit1 X_DIR (set = -X)
|
bit0 X_STEP, bit1 X_DIR (set = -X)
|
||||||
bit2 Y_STEP, bit3 Y_DIR (set = +Y)
|
bit2 Y_STEP, bit3 Y_DIR (set = +Y)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"""Coolant temperature spot-check helper (runs on the board or from a
|
"""Coolant temperature spot-check helper (runs on the board or from a
|
||||||
host; gfbench: GF_HOST).
|
host; gfbench: GF_HOST).
|
||||||
|
|
||||||
The raw->Celsius conversion in UAPI.md is the factory B-equation (10k
|
The documented raw->Celsius conversion (docs.forgefirm.org, sensors) is the factory B-equation (10k
|
||||||
B3380 NTC in a 10k divider behind a 1.3x gain stage, 10-bit ADC). This
|
B3380 NTC in a 10k divider behind a 1.3x gain stage, 10-bit ADC). This
|
||||||
tool collects reference points - a measured real temperature paired with
|
tool collects reference points - a measured real temperature paired with
|
||||||
the machine's raw ADC readings - and fits a per-machine line to
|
the machine's raw ADC readings - and fits a per-machine line to
|
||||||
@@ -17,7 +17,7 @@ Usage:
|
|||||||
temp_calibrate.py supply-watch [seconds] the same for the power supply
|
temp_calibrate.py supply-watch [seconds] the same for the power supply
|
||||||
temp_calibrate.py supply-point <C> [note] sensor (pic/pwr_temp, raw):
|
temp_calibrate.py supply-point <C> [note] sensor (pic/pwr_temp, raw):
|
||||||
temp_calibrate.py supply-fit thermometer on the heatsink vs
|
temp_calibrate.py supply-fit thermometer on the heatsink vs
|
||||||
the raw count; UAPI.md's guess
|
the raw count; the documented guess
|
||||||
raw * 0.08715 - 21 is shown
|
raw * 0.08715 - 21 is shown
|
||||||
beside it. Points go to
|
beside it. Points go to
|
||||||
supply_calibration.json.
|
supply_calibration.json.
|
||||||
@@ -40,7 +40,7 @@ SUPPLY_STORE = data_path('supply_calibration.json')
|
|||||||
|
|
||||||
|
|
||||||
def supply_guess_c(raw):
|
def supply_guess_c(raw):
|
||||||
"""UAPI.md's unverified guess for pic/pwr_temp."""
|
"""The documented unverified guess for pic/pwr_temp."""
|
||||||
return raw * 0.08715 - 21
|
return raw * 0.08715 - 21
|
||||||
|
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ def supply_raw(samples=5, delay=1.0):
|
|||||||
|
|
||||||
|
|
||||||
def uapi_c(raw):
|
def uapi_c(raw):
|
||||||
"""The UAPI.md factory conversion (B-equation NTC behind divider + gain)."""
|
"""The documented factory conversion (B-equation NTC behind divider + gain)."""
|
||||||
return degc(raw)
|
return degc(raw)
|
||||||
|
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ def supply_main(mode):
|
|||||||
if slope is None:
|
if slope is None:
|
||||||
print('points share one raw value; no fit')
|
print('points share one raw value; no fit')
|
||||||
return 1
|
return 1
|
||||||
print('fit: degC = %.5f * raw + %.2f (UAPI.md guess: 0.08715 * raw - 21)' % (slope, offset))
|
print('fit: degC = %.5f * raw + %.2f (documented guess: 0.08715 * raw - 21)' % (slope, offset))
|
||||||
worst = max(abs(p['measured_c'] - supply_guess_c(p['raw'])) for p in pts)
|
worst = max(abs(p['measured_c'] - supply_guess_c(p['raw'])) for p in pts)
|
||||||
print('the guess is off by at most %.1f C at these points' % worst)
|
print('the guess is off by at most %.1f C at these points' % worst)
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
Reference in New Issue
Block a user