mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
Move the build, release, and acceptance docs to the documentation site
The documentation site, docs.forgefirm.org, is the one home for the developer documentation. BUILD.md, kas/README.md, and docs/ACCEPTANCE.md are on the site under Developers, and they are deleted here. Every reference points at the site: README.md, BRINGUP.md, UPDATE-SYSTEM.md, the kas configuration comments, release.sh, the CI workflow comments, and the releases, forgetest, and bench READMEs. BRINGUP.md: "Next work" item 16, step timing under CPU contention, is closed; the video work resolved it. The items after it are renumbered. CAMPAIGN-LOG.md records the closure. Documentation and comments only. No layer content changes, so the manifest identity of the next image does not change. No catalog consequence.
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
# - coverage lint: every source path of every component pinned by the
|
||||
# recipes must be selected by some catalog test's coverage globs (the
|
||||
# tree manifest is generated from the pins with git - no Yocto build);
|
||||
# enforced: an uncovered path fails the job (docs/ACCEPTANCE.md).
|
||||
# enforced: an uncovered path fails the job (https://docs.forgefirm.org/developers/acceptance/).
|
||||
|
||||
name: forgetest-ci
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
path: forgefirm
|
||||
|
||||
# The kas config references meta-openglow as a local sibling
|
||||
# (kas/README.md "Push & release order" step 4 flips it to the
|
||||
# (the release flow on the documentation site flips it to the
|
||||
# pinned-remote block at release time). Every source repo the recipes
|
||||
# build is fetched by pinned SRCREV; no other sibling is needed.
|
||||
- name: Checkout meta-openglow (sibling)
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
# Build
|
||||
|
||||
ForgeFIRM is built with [**kas**](https://kas.readthedocs.io/), which manages
|
||||
the Yocto layers and pins their versions. The **forgefirm** repo is the base
|
||||
of the build.
|
||||
|
||||
Builds run on a Linux host, or on **WSL2** (officially supported by Yocto —
|
||||
keep the tree on the native ext4 filesystem, not `/mnt/c`).
|
||||
|
||||
## Host setup
|
||||
|
||||
On Ubuntu/Debian (including WSL2), install the Yocto host packages and kas:
|
||||
|
||||
```console
|
||||
sudo apt-get install -y gawk wget git diffstat unzip texinfo gcc build-essential \
|
||||
chrpath socat cpio python3 python3-pip python3-pexpect xz-utils debianutils \
|
||||
iputils-ping python3-git python3-jinja2 python3-subunit zstd liblz4-tool file \
|
||||
locales libacl1 lz4 rsync
|
||||
sudo locale-gen en_US.UTF-8
|
||||
pipx install kas # use pipx — Ubuntu 24.04 (PEP 668) blocks `pip install --user`
|
||||
```
|
||||
|
||||
For other distros, see the
|
||||
[Yocto Project Quick Build](https://docs.yoctoproject.org/brief-yoctoprojectqs/index.html).
|
||||
Do not build as root (the Yocto sanity checks refuse it).
|
||||
|
||||
## Get the sources
|
||||
|
||||
Clone the two repos as siblings (the kas config references `meta-openglow`,
|
||||
branch `scarthgap`, at `../meta-openglow`; kas fetches the upstream Yocto
|
||||
layers itself, and every ForgeFIRM source repo — the kernel module, the
|
||||
controller, the daemon, the cloud apps — is fetched by its recipe at a pinned
|
||||
revision):
|
||||
|
||||
```console
|
||||
git clone https://github.com/ScottW514/forgefirm.git
|
||||
git clone -b scarthgap https://github.com/ScottW514/meta-openglow.git
|
||||
```
|
||||
|
||||
```
|
||||
openglow-forgefirm/
|
||||
├── forgefirm/ ← base repo, build runs here
|
||||
└── meta-openglow/
|
||||
```
|
||||
|
||||
## Build the image
|
||||
|
||||
```console
|
||||
cd forgefirm
|
||||
kas build kas/forgefirm-glowforge.yml
|
||||
```
|
||||
|
||||
kas fetches the upstream layers into `forgefirm/layers/`, builds in
|
||||
`forgefirm/build/`, and produces the bootable image at:
|
||||
|
||||
```
|
||||
forgefirm/build/tmp/deploy/images/glowforge/forgefirm-image-glowforge.rootfs.wic.gz
|
||||
```
|
||||
|
||||
(The `u-boot-glowforge.imx` also deployed there is **reference-only**: every
|
||||
supported install/boot flow keeps the factory bootloader on the eMMC. Its env
|
||||
Kconfig now matches the factory layout — 0x80000 primary / 0x82000 redundant —
|
||||
but it is not wired into any install path and flashing it is unsupported.)
|
||||
|
||||
For exact, reproducible layer versions, generate a lockfile once:
|
||||
|
||||
```console
|
||||
kas lock kas/forgefirm-glowforge.yml
|
||||
```
|
||||
|
||||
See [`kas/README.md`](kas/README.md) for details, the container-build option,
|
||||
and the Scarthgap migration backlog.
|
||||
|
||||
## Third-party firmware licensing
|
||||
|
||||
The i.MX6 BSP installs NXP firmware blobs (VPU, EPDC) that are distributed
|
||||
under NXP's firmware EULA. The build config accepts it
|
||||
(`ACCEPT_FSL_EULA = "1"`), and the image ships the license text alongside the
|
||||
blobs at `/usr/share/licenses/firmware-imx/EULA` — keep it there in any
|
||||
redistributed image. The SDMA firmware comes from `linux-firmware`, which
|
||||
carries its own license package.
|
||||
|
||||
## Write to an SD card
|
||||
|
||||
```console
|
||||
cd build/tmp/deploy/images/glowforge
|
||||
sudo zcat forgefirm-image-glowforge.rootfs.wic.gz | dd of=/dev/sdX bs=1M
|
||||
```
|
||||
|
||||
To install onto the factory eMMC (into the unused A/B slot, with the factory
|
||||
firmware archived first — one OS runs at a time), see
|
||||
[`INSTALL.md`](INSTALL.md).
|
||||
@@ -15,13 +15,13 @@ panel, and a standard Grbl interface.
|
||||
|
||||
* [Latest Release](https://github.com/ScottW514/forgefirm/releases)
|
||||
* [Installation Instructions](https://github.com/ScottW514/forgefirm/blob/master/INSTALL.md)
|
||||
* [Build Instructions](https://github.com/ScottW514/forgefirm/blob/master/BUILD.md)
|
||||
* [Build Instructions](https://docs.forgefirm.org/developers/building/)
|
||||
* [Connecting LightBurn](https://github.com/ScottW514/forgefirm/blob/master/docs/LIGHTBURN.md)
|
||||
* [How motion and the laser are driven](https://github.com/ScottW514/forgefirm/blob/master/docs/MOTION.md)
|
||||
* [How cooling and airflow work](https://github.com/ScottW514/forgefirm/blob/master/docs/COOLING.md)
|
||||
* [The cameras and the video stream](https://github.com/ScottW514/forgefirm/blob/master/docs/VIDEO.md)
|
||||
* [How the laser safing works](https://github.com/ScottW514/forgefirm/blob/master/docs/SAFETY.md)
|
||||
* [How a release is tested before being accepted](https://github.com/ScottW514/forgefirm/blob/master/docs/ACCEPTANCE.md)
|
||||
* [How a release is tested before being accepted](https://docs.forgefirm.org/developers/acceptance/)
|
||||
* [Community Support](https://community.openglow.org)
|
||||
|
||||
## What it does
|
||||
|
||||
@@ -1,448 +0,0 @@
|
||||
# Release acceptance
|
||||
|
||||
A ForgeFIRM release is signed and published only when the **acceptance
|
||||
catalog** passes on the bench machine and the result is committed with
|
||||
the release. This document is the contract: what the gate is, how a
|
||||
campaign runs, how a result stays valid across builds, and what the
|
||||
release pipeline checks.
|
||||
|
||||
## The pieces
|
||||
|
||||
| Piece | Where | What it does |
|
||||
|---|---|---|
|
||||
| **forgetest** | `forgetest/` in this repo; on the **dev image** as a daemon on HTTP **:8090** | Runs the catalog against the machine from a self-contained web page, keeps the append-only result log under `/data/forgetest/`, exports the release artifact, and carries the bench diagnostics page. Never on a release image. |
|
||||
| **Image manifest** | `/etc/forgefirm-manifest.json` in every image (`meta-forgefirm/classes/forgefirm-manifest.bbclass`, `forgefirm-image-manifest.bbclass`) | The build's inputs: for every component the pinned revision and one `[path, blob-id]` pair per source file, plus the platform identity (machine, kernel revision + config hash, device tree hashes, layer content hashes). |
|
||||
| **Artifact** | `releases/v<version>/acceptance.json` (+ `acceptance.md`) committed to this repo, and attached to the GitHub release | What forgetest exported: per catalog test the winning PASS, the fingerprint it ran under, and whether it was inherited. Self-hashed. |
|
||||
| **Gate** | `scripts/acceptance-gate.py`, called by `scripts/release.sh` | Recomputes every test's fingerprint from the manifest inside the release rootfs and requires the recorded PASS to match. |
|
||||
|
||||
## The catalog
|
||||
|
||||
Every test declares, in code (`forgetest/forgetest/suite/*.py`):
|
||||
|
||||
- **kind** - `auto` (no operator), `operator` (prompts, no emission), or
|
||||
`live` (laser emission possible: the page requires the eye-protection /
|
||||
fire-watch / exhaust acknowledgment, and the physical arm press is
|
||||
required through the controller's normal path - forgetest never touches
|
||||
the laser latch);
|
||||
- **hardware** - `api` (forgectrl and the controller stay up) or
|
||||
`takeover` (forgectrl is stopped for the duration; a marker file makes a
|
||||
crash recoverable at the next start);
|
||||
- **mode** - the controller mode the test needs live when it starts
|
||||
(`grbl` or `cloud`), or none. The runner switches the machine there
|
||||
before the test (through `POST /mode`, settled and with the Grbl port
|
||||
answering) and leaves it there; a test with no mode runs in whatever
|
||||
mode it finds, or manages the mode itself (the `cloud.*` job tests,
|
||||
through `enter_cloud`, which also waits for the service session);
|
||||
- **covers** - the source paths whose content the test stands for, as
|
||||
`(component, glob)` pairs. Globs anchor at the component's repository
|
||||
root (`forgefirm-app/gfcloud.py`, not `gfcloud.py`), and a glob that
|
||||
selects nothing is a lint failure. Non-behavioral paths (docs, CI, the
|
||||
components' own unit tests, licenses; the list is `NON_BEHAVIORAL` in
|
||||
`forgetest/forgetest/manifest.py`) are outside every fingerprint, so a
|
||||
README edit re-requires nothing;
|
||||
- **requires** - tests that must be satisfied first (the emission tests
|
||||
require the motion and readback tests). This orders the runs; it is not
|
||||
a release condition of its own (the release needs every test satisfied
|
||||
anyway). The page's **Ignore prerequisites** switch lets any test start
|
||||
alone; a run started that way records the unmet prerequisites in its
|
||||
`evidence.prerequisites` and its log, and the prerequisites stay
|
||||
required;
|
||||
- **always** - membership in the **always-required core**, which is run
|
||||
in every campaign and is never inherited: image health, the kernel
|
||||
latch/safety readbacks, and one live emission witness with the
|
||||
armed-window disarm;
|
||||
- **actions** - the machine actions the test asks for by name (`lid`,
|
||||
`interlock`, `button`; see "The operator's part"). An `auto` test
|
||||
declares none. The page lists them before a start; a bench actuator
|
||||
that covers a channel can perform them;
|
||||
- **precheck** - a condition the machine must meet for the test to start
|
||||
at all (`kernel.fire-line` needs HV not reporting good, the kernel's
|
||||
rule for a zero-duty latch unlock; `cloud.mode-switch` needs
|
||||
`homing_mode = gfcloud`). A start the precheck refuses is not a result:
|
||||
the page says why, a queue skips the test with the reason and carries
|
||||
on, and nothing is recorded. Neither field is part of the gate-visible
|
||||
definition.
|
||||
|
||||
`GET /catalog` on the tool lists the definitions; the page shows them under
|
||||
each test's *details*.
|
||||
|
||||
## Domain fingerprints and inheritance
|
||||
|
||||
A test's **domain fingerprint** is the hash of the `(component, path,
|
||||
blob-id)` triples its coverage globs select in the image manifest, plus the
|
||||
platform identity, plus the hash of the test's own implementation: its
|
||||
function (decorator included) together with the code its suite module
|
||||
shares among its tests, everything outside the module's `@test`
|
||||
functions. A PASS recorded under fingerprint F applies to any build whose
|
||||
recomputed fingerprint is F - the same code computes it on the board and
|
||||
in the gate.
|
||||
|
||||
Consequences:
|
||||
|
||||
- A change to a covered file invalidates exactly the tests that cover it.
|
||||
A panel-only change reruns the core plus the panel tests, not the
|
||||
cooling drills.
|
||||
- A platform change (kernel, device tree, a layer's content) invalidates
|
||||
everything. Layer content is every file under `meta-forgefirm`,
|
||||
`meta-glowforge-bsp` and `meta-openglow-core` except documentation
|
||||
(`*.md`) and the component pin files (`<recipe>-pin.inc`, holding only a
|
||||
component's `SRCREV` and the `PV` that moves with it). A pin bump is the
|
||||
component's change, and the component entry already carries it file by
|
||||
file, so it invalidates the tests that cover the component - not the
|
||||
bench. A recipe-body change (build flags, patches, config fragments,
|
||||
init scripts, a third-party pin with no manifest entry) is layer content
|
||||
and invalidates everything; so does a pin written into a recipe body
|
||||
instead of its pin file (the safe direction).
|
||||
- A change inside a test's body invalidates that test's earlier passes
|
||||
and no other; a change to a helper its module shares invalidates the
|
||||
tests of that module.
|
||||
- "Touched" is computed from content hashes carried in the image, never
|
||||
declared by hand.
|
||||
|
||||
## Campaigns
|
||||
|
||||
A **campaign** is bound to one image (manifest content hash) and one
|
||||
catalog (catalog hash). The first Start on an image opens one. It stays
|
||||
open until a **FAIL** (or an erroring test), an **invalidate-all**, an
|
||||
explicit **reset**, or a different image or catalog. Reboots into the same
|
||||
image continue it.
|
||||
|
||||
For every test, in order:
|
||||
|
||||
1. a PASS in the open campaign with the current fingerprint satisfies it;
|
||||
2. otherwise, if it is not core, the newest PASS anywhere in the history
|
||||
with the current fingerprint and newer than the last invalidate-all is
|
||||
**inherited** (its origin - run time, image, campaign - is kept and
|
||||
exported);
|
||||
3. otherwise it is **required** (reason: `always`, `never-passed`, or
|
||||
`domain-changed`).
|
||||
|
||||
**Release authorized** = a campaign is open and every catalog test is
|
||||
satisfied. There is no SKIP: a test the bench cannot run means the release
|
||||
cannot be authorized (that is a catalog change, not a skip).
|
||||
|
||||
**Invalidate all** (page footer, reason required) records that the bench
|
||||
itself changed - new tube, driver swap, cable work, a judgment call - and
|
||||
forces a full campaign; nothing before it can be inherited.
|
||||
|
||||
**Inheritance is local.** The history a bench inherits from is its own
|
||||
results log; there is no import of a published `acceptance.json`. A second
|
||||
bench, or one whose `/data` has been wiped, starts from a full campaign.
|
||||
|
||||
## Running a campaign
|
||||
|
||||
1. Boot the dev image on the bench (`forgefirm-image-dev`), open
|
||||
`http://<machine>:8090/`.
|
||||
2. The banner shows the image, the manifest identity, and *Release
|
||||
authorized*. Tests marked **required** need to run; **inherited** ones
|
||||
do not.
|
||||
3. Start the required tests. `operator` tests ask questions in the run
|
||||
pane; `live` tests need the acknowledgment and the physical arm press;
|
||||
`takeover` tests stop forgectrl for the duration. A test whose
|
||||
prerequisites are not satisfied is locked until they are - or until
|
||||
the **Ignore prerequisites** switch in the Campaign card is on, which
|
||||
unlocks every Start (the switch is remembered by the browser; a run
|
||||
started under it says so in its record).
|
||||
The `cloud.*` job tests run **in cloud mode and stay there**: the first
|
||||
one switches from GRBL mode (once, its connect-time hunt waited out)
|
||||
and the following ones reuse the live session; nothing switches back
|
||||
after them. The tests that need GRBL mode (`motion.*`, `laser.*`,
|
||||
`cooling.fans-quiet-after-motion`, `cloud.mode-switch`) declare it,
|
||||
and the runner switches back the moment one of them starts - so the
|
||||
mode changes only where the next test asks for it, never between
|
||||
tests of the same mode. `cloud.mode-switch` is the one round trip, and
|
||||
it carries the two service-driven motions with it: the connect-time
|
||||
hunt run with the lid open, and the web-service homing (`$H` with
|
||||
`homing_mode = gfcloud`) after the switch back.
|
||||
The cloud tests split by what they prove. The service protocol (sign-in,
|
||||
the firmware check, the WebSocket, the hunt, the image uploads, a
|
||||
print's download and lifecycle as the app sees them) is
|
||||
`cloud.service-protocol`: the cloud client restarted as gfutilities'
|
||||
emulator in this machine's identity under the `/run/gfcloud-emulate`
|
||||
marker, answering the real service with the dev image's canned frames
|
||||
and running the print from the app without hardware, so only the app
|
||||
has to be driven (by a person or an agent, anywhere). The service and
|
||||
the machine together are `cloud.mode-switch` and one real print,
|
||||
`cloud.pause-resume` (progress, the button wait, the job's limits
|
||||
reaching the engine). The machine's print behavior (the lid and
|
||||
interlock aborts, the button-wait cancel, a paused print ended by the
|
||||
lid, a print longer than the ring with the app's cancel) runs under the
|
||||
**offline service** (`enter_offline`: the cloud client restarted with
|
||||
the `/run/gfcloud-offline` marker, no account, no network; the test
|
||||
hands it a synthesized job over `/run/gfcloud-offline.sock` and reads
|
||||
the machine's events back, see `forgetest/puls.py` and the cloud
|
||||
client's `docs/CLOUD.md`). Those jobs carry no laser command, so
|
||||
nothing is on the bed and nothing burns, but the arm still unlocks the
|
||||
latch, so they stay `live`. The offline client is left running; the
|
||||
next test that needs the service restarts it (`enter_cloud` does), as
|
||||
does a mode switch or a controller restart.
|
||||
**The coverage maps follow the split.** The protocol test stands for
|
||||
the web session, the emulator and its fixtures; the offline tests for
|
||||
the run loop, the hardware it drives, the offline dispatch and the
|
||||
pulse path; `cloud.mode-switch` for the homing path (the session, the
|
||||
whole hardware library, `gfhome`); and every one of them for the
|
||||
client's common ground (`gfcloud.py`, `ffmachine.py`, the config, the
|
||||
identity, the cooling reporter, gfutilities' core and its transport
|
||||
helpers). The one real print keeps the coarse maps, all three cloud
|
||||
components whole: it is the integration, and the floor the lint needs,
|
||||
so whatever the finer maps leave out still re-requires it. A sign-in
|
||||
change therefore re-requires the protocol test and the print; a feeder
|
||||
change the offline tests and the print; a camera change the mode
|
||||
switch and the print.
|
||||
**The service's connect-time hunt is paid only where it is the
|
||||
subject.** A cloud client the tool starts for anything else (the real
|
||||
client back after the emulator, a mode the runner switches to or hands
|
||||
back, a controller it restarts) comes up under the `/run/gfcloud-nohunt`
|
||||
marker: its first settings report is the reconnect form, and the
|
||||
service keeps the head position it has instead of homing. The hunt
|
||||
tests (`cloud.mode-switch`, `cloud.service-protocol`) get theirs, and
|
||||
so does the one real print: `enter_cloud` reuses a running session
|
||||
only when that client has hunted the machine itself (never the
|
||||
emulator's, never a no-hunt start), otherwise it restarts the client
|
||||
with the hunt, because a print placed on a head position the service
|
||||
only believes can run the gantry into a rail. The same holds outside
|
||||
the tool: a machine left in cloud mode by a campaign may not have
|
||||
hunted since GRBL mode moved the head, so open and close the lid (the
|
||||
service re-hunts) or restart the controller before printing from the
|
||||
app. Every marker is one start: the client that reads it takes it
|
||||
down.
|
||||
4. Or hand the whole list to a queue. **Run what is left** offers two:
|
||||
**Unattended** takes every `auto` test the campaign does not already
|
||||
count as satisfied, and needs nobody in the room; **Operator and live**
|
||||
takes the `operator` and `live` ones, and needs somebody at the
|
||||
machine, since it prompts and it fires the laser. Each button says how
|
||||
many it would run, and asks before it starts: the live queue names the
|
||||
tests that fire and takes the acknowledgment once, for all of them.
|
||||
A queue runs one test at a time in prerequisite order and stops on the
|
||||
first result that is not a PASS, because a FAIL closes the campaign. A
|
||||
test it cannot start is skipped with the reason on the page and the
|
||||
rest carry on, which is what happens to an `auto` test waiting on an
|
||||
`operator` one: run the attended queue, then the unattended one again.
|
||||
**Stop the queue** cancels what is still waiting and lets the run in
|
||||
progress finish; **Abort** ends that one too. The queue lives in the
|
||||
runner, so closing the page or reloading it does not disturb the run.
|
||||
5. When *Release authorized: YES*, **Export release artifact**, download
|
||||
`acceptance.json` and `acceptance.md`, and commit them as
|
||||
`releases/v<version>/acceptance.json` and `.md`.
|
||||
|
||||
The raw log (`/data/forgetest/results.jsonl`, `Raw log` in the footer) is
|
||||
the bench's own record; the artifact is the release's. The runner's own
|
||||
events - a queue opening, skipping or stopping, a takeover recovered at
|
||||
start-up, the leftovers a baseline pass found - go to the **journal**
|
||||
(`Runner journal` in the footer: the daemon's `daemon.log` under the
|
||||
data directory, also syslog under the `forgetest` name, and the log of
|
||||
the run in progress), never to the page's campaign card.
|
||||
|
||||
### The operator's part
|
||||
|
||||
The run card shows **what you will do** before anything is asked: the
|
||||
running test's `steps`, the attended tests still waiting in a queue, or
|
||||
the test whose title you clicked while the machine is idle. What follows
|
||||
during the run is those steps, taken in turn, in one of four forms:
|
||||
|
||||
- a **Ready** prompt pre-announces a timed step: what happens on the
|
||||
click and what you do during it ("On Ready the head starts an 8 s move;
|
||||
press the button once while it moves"). Nothing moves until you click;
|
||||
- a **notice** is a standing instruction with no button. The test shows
|
||||
it and watches the machine for the result - the lid switch reading
|
||||
open, the interlock loop reading open, the controller entering Hold
|
||||
after the press, the client's log line - and takes it down when it sees
|
||||
it. There is nothing to answer and nothing to race;
|
||||
- a machine **action** (`ctx.act("lid", "open")`, `("interlock",
|
||||
"close")`, `("button", "press", until=...)`) is a notice the runner
|
||||
manages: the wording is the action's own, the test adds its context,
|
||||
the machine's reading proves it done, and the result's
|
||||
`evidence.actions` records each one with who performed it. This is the
|
||||
seam the bench actuator plugs into (below): a `fixture` covering a
|
||||
channel performs the action instead of the notice, and a test reads
|
||||
the same either way;
|
||||
- a **confirm** is a yes/no the evidence cannot answer. One is left in
|
||||
the catalog: the mark `laser.emission-witness` leaves on the scrap, the
|
||||
once-per-campaign calibration of the sensor witnesses (the head's beam
|
||||
detector, the HV current, the kernel's LASER_ON count), plus the app's
|
||||
own display in `cloud.pause-resume`. The head accelerometer stands in
|
||||
for "did the gantry move", the button LEDs for "is the button dark",
|
||||
the lid lamp toggled between two snapshots for "is the camera live".
|
||||
|
||||
### The bench actuator
|
||||
|
||||
`forgefixture` (`fixture/`, its own README) is an ESP32-S3 on the bench
|
||||
network driving three relays at the machine's connectors: a normally
|
||||
closed contact in the lid-switch loop, another in the interlock loop, a
|
||||
normally open contact across the button input that is only ever pulsed.
|
||||
Unpowered, unplugged or rebooting, it leaves a stock machine. The tool
|
||||
finds it through `/data/forgetest/fixture.json` (bench-local, mode
|
||||
0600: the hostname, the API key, an optional `ip` override, the
|
||||
`channels` wired, `arm_press`), resolves `<hostname>.local` itself (the
|
||||
image carries no mDNS resolver), and probes it before every run and at
|
||||
most every 30 s otherwise. What holds:
|
||||
|
||||
- **Every action is still proven by the machine.** The fixture does not
|
||||
read the switches; `ctx.act` asks it and then waits for forgectrl's
|
||||
reading exactly as it waits for an operator's hand. An action the box
|
||||
fails to perform falls back to the operator's notice, and the record
|
||||
says so (`evidence.actions[].by`, `fixture_error`); in an unattended
|
||||
run there is nobody to fall back to, and the test ends ERROR naming
|
||||
the refusal. Two button presses are spaced by the tool (the last
|
||||
pulse's end plus 300 ms), so the controller sees the release between
|
||||
them and not one long press.
|
||||
- **An operator test the fixture can run alone runs unattended.** A test
|
||||
declares its actions and, with `hands=(...)`, whatever else it asks of
|
||||
a person ("app" for a job in the Glowforge app). An `operator` test
|
||||
whose actions the fixture covers and whose `hands` are empty is routed
|
||||
into the unattended queue and out of the attended one; its Ready gates
|
||||
pass (the fixture performs the timed step), and a prompt it raises
|
||||
anyway is a FAIL naming the undeclared step, never a wait for nobody.
|
||||
`live` never moves: the fire watch and the acknowledgment are a
|
||||
person's.
|
||||
- **The button channel needs the jumper.** With the fixture's enable
|
||||
jumper out the button is not covered, and tests that press it stay
|
||||
attended. The arm press of a live test stays a person's unless the
|
||||
bench config says `arm_press: true`: then, and only with the jumper in,
|
||||
the fixture presses when the button lights, recorded as its own.
|
||||
- **What the box still holds after a run is released** and recorded
|
||||
(`evidence.fixture.released`), before the baseline's post pass, so a
|
||||
lid left open by a failed test never reaches the next one.
|
||||
|
||||
### Every run starts from, and leaves, the fresh-boot idle state
|
||||
|
||||
The runner brackets every test and bench tool with a **baseline** pass
|
||||
(`baseline.py`): before the run it verifies the machine against the
|
||||
fresh-boot idle state and restores anything off it; after the run - on
|
||||
every exit path, pass, fail, or abort - it restores again. Two kinds of
|
||||
items: **fixed** resting values the boot establishes (the kernel module
|
||||
defaults, forgectrl's start-up writes, the GRBL controller's init writes:
|
||||
`motor_lock=8`, `x/y_mode=8`, `x/y_decay=1`, `step_freq=28160`,
|
||||
`ramp_rate=125000`, `streaming=0`, `state=idle`, latch locked, hold
|
||||
currents, head lamp and button LEDs off, heater and TEC off, the lid lamp
|
||||
at forgectrl's `lid_lamp_idle` setting; forgectrl: the controller running
|
||||
with motion verified, no diagnostic, the camera engine and cooling engine
|
||||
idle), and **preserved** state with no resting policy that a run must
|
||||
hand back as it found it (the position counters, the settings map, the
|
||||
controller mode). The mode in force decides what the baseline owns: in
|
||||
cloud mode the cloud client's own configuration (the GRBL controller's
|
||||
init values, which it rewrites from every pulse header; the lid lamp,
|
||||
its lid-image level; the position counters, re-zeroed at every service
|
||||
action) is left to it, and the safety readbacks, latch, ring, module
|
||||
defaults, and forgectrl's engines are checked as always. The mode itself
|
||||
is preserved unless the run declared the change (`ctx.mode_changed()`,
|
||||
the cloud tests entering cloud mode) or the test declared a `mode`, in
|
||||
which case the runner makes the switch in the pre pass, before the
|
||||
preserved state is captured, and the post pass keeps the mode the test
|
||||
asked for; the persisted `controller_mode` setting is never written back
|
||||
as a bare setting - only the switch keeps it in step with the live mode. Deviations are
|
||||
**leftovers**: logged in the run pane, kept in the result's `evidence`
|
||||
(`baseline.pre` / `baseline.post`), and surfaced in the page's message
|
||||
line - a leftover found before a run is attributed to the previous run; one
|
||||
found after is the run's own defect. Takeover runs additionally capture
|
||||
the controller-owned kernel attributes on entry and write them back before
|
||||
forgectrl restarts, so the supervisor's liveness probe runs on the machine
|
||||
it expects. The runner waits for forgectrl's supervisor to settle (motion
|
||||
verified, or the ladder's verdict) before and after every takeover.
|
||||
|
||||
**Power-cycle before a campaign.** forgetest takes a **fresh-boot
|
||||
reference** once per boot (`/data/forgetest/boot-<boot_id>.json`, taken
|
||||
only within the first ten minutes after boot, after the supervisor
|
||||
settles): the whole idle picture of this machine as the image boots it,
|
||||
the check on the fixed values, and the record a leftover is judged
|
||||
against. Take it after a **power cycle**, not a warm `reboot` - the
|
||||
machine's true fresh state is the powered-on one (the PIC's own lamp and
|
||||
sensor defaults, then forgectrl's start-up writes on top).
|
||||
A displaced head is jogged back along its own path by the kernel-measured
|
||||
X/Y delta (bounded to 100 mm; Z is never touched); beyond that the
|
||||
counters are reported and the run must be fixed. A run that legitimately
|
||||
re-zeroes the counters (cloud mode's connect) tells the runner so
|
||||
(`ctx.counters_rezeroed()`) and hands the head back itself.
|
||||
|
||||
## The gate
|
||||
|
||||
`scripts/release.sh <version>` builds the release image, reads
|
||||
`/etc/forgefirm-manifest.json` out of the release rootfs and runs
|
||||
|
||||
scripts/acceptance-gate.py releases/v<version>/acceptance.json <manifest>
|
||||
|
||||
which requires: the artifact self-hash intact; `authorized: true`; the
|
||||
catalog in the tree identical to the artifact's; for every test a recorded
|
||||
PASS whose fingerprint equals the one recomputed from the release manifest;
|
||||
inherited results not core and newer than the invalidate epoch. Any
|
||||
problem dies before signing. `FORGEFIRM_ACCEPTANCE_SKIP=1` bypasses the gate
|
||||
deliberately and prints a loud warning; it is never the default. The
|
||||
artifact is staged and attached to the GitHub release next to
|
||||
`forgefirm.fw`.
|
||||
|
||||
Because the dev image and the release image are built from the same tree
|
||||
in one `bitbake` invocation, their manifests share the same identity; a pin
|
||||
bumped after the campaign shows up as a fingerprint mismatch on exactly the
|
||||
tests that cover it.
|
||||
|
||||
## Coverage currency rule
|
||||
|
||||
Every change is evaluated against the catalog, in addition to its unit
|
||||
tests:
|
||||
|
||||
1. does an existing test exercise the changed behavior - if not, add or
|
||||
extend one in the same change;
|
||||
2. does that test's `covers` map name the files touched - if not, widen it
|
||||
in the same change.
|
||||
|
||||
A gate or a limit is exercised through the settings API (a value a healthy
|
||||
machine cannot meet, re-read by the engine at the next run start, restored
|
||||
by the test's own teardown), never through `GFCOOL_*` environment overrides,
|
||||
which need a daemon restart and stay bench-only.
|
||||
|
||||
A behavior change with no catalog consequence needs a sentence of
|
||||
justification in the commit message. Coverage gaps are defects: under the
|
||||
domain model an uncovered path lets an inherited PASS stay valid across a
|
||||
change that should have invalidated it.
|
||||
|
||||
The mechanical floor is the coverage lint,
|
||||
|
||||
python3 -m forgetest.coverage --manifest <manifest.json> [--enforce]
|
||||
|
||||
which lists every manifest path no test covers, minus the non-behavioral
|
||||
paths (docs, CI, tests, licenses), and every coverage entry that selects
|
||||
nothing. CI (`forgetest-ci.yml`) runs it on a manifest generated from the
|
||||
recipe pins with `scripts/manifest-from-tree.py` (no Yocto build needed)
|
||||
and fails the job on any uncovered path. On the board, run it against
|
||||
`/etc/forgefirm-manifest.json`. The lint proves a
|
||||
file is *fingerprinted*; whether the test *exercises* the change is the
|
||||
change author's judgment (rule 1).
|
||||
|
||||
## Bench diagnostics page
|
||||
|
||||
The same daemon serves `#bench`: the registry of the bench tools
|
||||
(`scripts/bench`, installed under `/usr/share/forgetest/bench/`), each with
|
||||
its safety class, argument form, and last run. The classes: `dry` (reads
|
||||
or dry motion, forgectrl stays up), `takeover` (forgectrl and the
|
||||
controller stopped for the run, the pulse device free, the same wrapper
|
||||
the takeover tests use), `scope` (a takeover whose result only means
|
||||
something with the named instrument on the bench), `live` (emission
|
||||
possible; the operator acknowledgment and the physical arm press). A tool
|
||||
runs as a subprocess with the output on the page and, on the board, the
|
||||
machine as `GF_HOST=127.0.0.1`, the panel token in `GF_TOKEN`, and its data
|
||||
files under `/data/forgetest/bench/` (`FORGETEST_BENCH_DATA`); the same
|
||||
scripts run from a LAN host with `GF_HOST` set (`scripts/bench/gfbench.py`,
|
||||
`scripts/bench/README.md`). Every board-runnable tool is ported; the entries
|
||||
that stay unported are the CI harnesses of the null-sink controller and the
|
||||
factory `.puls` decoder, which do not run against the machine at all - they
|
||||
are listed so the catalog of what exists is complete. Bench runs are
|
||||
recorded in `/data/forgetest/bench.jsonl` and never enter a campaign.
|
||||
|
||||
## Layout
|
||||
|
||||
forgetest/forgetest/ the package (stdlib only)
|
||||
manifest.py manifest, globs, fingerprints, coverage report
|
||||
catalog.py @test registry, catalog hash
|
||||
campaign.py the rules (pure functions)
|
||||
artifact.py export + gate verification
|
||||
runner.py one run at a time, prompts, abort, takeover, queues
|
||||
baseline.py the fresh-boot idle state around every run
|
||||
server.py / page.py / ui/ HTTP API + the page (forgectrl's access rules;
|
||||
Bootstrap and the OpenGlow theme shared with the panel)
|
||||
bench.py / coverage.py bench registry + subprocess runner; the lint
|
||||
suite/ the catalog, one module per subsystem
|
||||
forgetest/tests/ host unit tests (python3 -m unittest discover -s tests)
|
||||
scripts/bench/ the bench tools (+ gfbench.py, the board/host helper)
|
||||
scripts/acceptance-gate.py the gate
|
||||
scripts/manifest-from-tree.py manifest from the recipe pins (CI, workstation)
|
||||
releases/v<version>/ the committed artifacts
|
||||
+16
-61
@@ -1,6 +1,6 @@
|
||||
# ForgeFIRM bring-up status & cold-start runbook
|
||||
|
||||
Last updated: **2026-08-17**.
|
||||
Last updated: **2026-08-26**.
|
||||
|
||||
This is the present state of the machine, the bench runbook, the measured
|
||||
hardware facts, and the authoritative list of open work. **The dated record —
|
||||
@@ -15,9 +15,9 @@ Read together with:
|
||||
| `kernel-module-glowforge/UAPI.md` | the pulse-stream feeder contract, sysfs attributes, sensor conversions |
|
||||
| `forgectrl/docs/SERVICES.md` | the machine-services contract: switch map, hardware ownership, cooling channels, mode supervision, pulse-device ownership, logging |
|
||||
| `docs/SAFETY.md` | the hardware safing chain, decoded |
|
||||
| `docs/ACCEPTANCE.md` | the release acceptance contract |
|
||||
| `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`, `BUILD.md`, `kas/README.md` | sender setup, A/B update system, install, build |
|
||||
| `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
|
||||
@@ -88,7 +88,8 @@ dev image (eMMC slot 1 = factory 2024, slot 2 = ForgeFIRM v0.1.0, archives in
|
||||
batched**, and a `.ko` or overlay change is validated on the image that ships
|
||||
it, never hot-swapped onto a board about to be reflashed.
|
||||
- **Build host**: a Linux build environment (a WSL2 distro works) holding the
|
||||
`forgefirm` + `meta-openglow` sibling checkout (`BUILD.md`); the ForgeFIRM
|
||||
`forgefirm` + `meta-openglow` sibling checkout (the site, Developers,
|
||||
"Build"); the ForgeFIRM
|
||||
source repos are fetched by pinned `SRCREV`. Build:
|
||||
`cd forgefirm && kas shell kas/forgefirm-glowforge.yml -c 'bitbake
|
||||
forgefirm-image forgefirm-image-dev'`. Artifacts:
|
||||
@@ -304,7 +305,7 @@ factory 2.6.0-2228 session; measured numbers in the facts bank).
|
||||
alike: the retrace is sized to `cnc/max_backtrack` and the lead follows it,
|
||||
so a pause with little history behind it shortens both rather than failing.
|
||||
GRBL mode uses feed hold / cycle start, so a resumed GRBL cut picks up where
|
||||
the deceleration ended (item 18). A pause is not a cancel: the latch
|
||||
the deceleration ended (item 17). A pause is not a cancel: the latch
|
||||
stays unlocked and the window open across it. There is no resume dwell: the
|
||||
safing chain re-arms ~216 ms before the first step (facts bank).
|
||||
- **`lid_policy = hold`** selects stock grblHAL door behavior instead (park in
|
||||
@@ -579,7 +580,7 @@ e-mails with stable placeholders). Design and contract: `SERVICES.md`
|
||||
|
||||
The release acceptance tool — catalog, campaigns, domain fingerprints,
|
||||
inheritance, the always-required core, invalidate-all, the release gate and the
|
||||
coverage currency rule — is specified in `docs/ACCEPTANCE.md`; the tool lives in
|
||||
coverage currency rule — is specified on the site (Developers, "Acceptance"); the tool lives in
|
||||
`forgetest/` and ships only on the dev image (`/etc/init.d/forgetest`, HTTP
|
||||
:8090). It is **bench-validated**: the full campaign on dev image
|
||||
`20260824230512` (`c-20260824231028-b7ca`) satisfied 45 of 45 from nothing,
|
||||
@@ -607,7 +608,7 @@ is committed.
|
||||
isolation. 28 are `auto`, 9 `operator`, 9 `live`; with the bench actuator up,
|
||||
eight of the operator tests run in the unattended queue.
|
||||
- **The operator's part is asked for by name, not by popup**
|
||||
(`docs/ACCEPTANCE.md` "The operator's part"): a Ready prompt before a
|
||||
(the site, Developers, "Acceptance", "The operator's part"): a Ready prompt before a
|
||||
timed step, a standing notice the test takes down when the machine shows
|
||||
the action done (`ctx.act("lid", "open")` and its kin, the seam a bench
|
||||
actuator will plug into), and one confirm by eye left in the catalog (the
|
||||
@@ -1097,8 +1098,7 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
until it lands); printable brackets are in `3d-models/`. Also: calibrate
|
||||
`gfcloud_home_x/y` against a jog to a known reference if the factory corner
|
||||
offset matters.
|
||||
6. **Cameras.** Lens calibration / bed alignment (the fisheye needs LightBurn's
|
||||
camera calibration pass); **first light on an 8 MP (OV8856) machine** — the
|
||||
6. **Cameras.** **First light on an 8 MP (OV8856) machine**: the
|
||||
whole path is written but nothing has run on one, and only that hardware can
|
||||
answer whether the 2-lane RAW8 full-resolution mode locks the D-PHY at
|
||||
720 Mbps/lane and what exposure/gain the sensor wants; the details, the
|
||||
@@ -1211,8 +1211,8 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
release is item 13.
|
||||
13. **Publish.** The first release: `releases/v<version>/acceptance.json`
|
||||
from the authorized export, `scripts/release.sh`, the kas flip and the
|
||||
first GitHub release, per `kas/README.md` ("Pins, pushes, and the release
|
||||
flow"), once ready to publish. Repoint the core submodule to
|
||||
first GitHub release, per the site (Developers, "Release flow"), once
|
||||
ready to publish. Repoint the core submodule to
|
||||
upstream if the `step_us_min` sizing fix merges.
|
||||
14. **Update system Phase 5 — recovery refresh.** The remaining phase of
|
||||
`docs/UPDATE-SYSTEM.md` (a refreshed recovery image in boot0); Phases 0–4
|
||||
@@ -1233,52 +1233,7 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
log EV_SW head-bit edges plus `head/beam_detect_digital|_analog` while
|
||||
firing.
|
||||
|
||||
16. **Step timing under CPU contention.** The board runs one core. The producer
|
||||
thread advances virtual time and stamps every step onto the pulse grid, so
|
||||
any interval it is kept off the CPU is an interval the grid does not
|
||||
advance; events after it map behind the ship cursor, where
|
||||
`gf_stream_pulse` clamps them forward and the backlog ships one step per
|
||||
machine tick — 28 160 steps/s against the 1 778 that 2000 mm/min asks for,
|
||||
a ~16× velocity burst no motor follows. `cnc/underruns` reads 0 throughout,
|
||||
because the ring never goes dry: the stream is continuous and only its
|
||||
timing is wrong, which is exactly what the kernel counters cannot see.
|
||||
|
||||
The margin absorbing a stall is **not** the 200 ms queue depth. The
|
||||
shipper's due index carries the same `+ gf.depth` the producer's base
|
||||
starts at, so the two cancel and the producer's lead over the cursor is the
|
||||
only slack there is. It was 2 ms. It is now `GFSINK_LEAD_MS`, default 10,
|
||||
and the per-run `LOG_DEBUG` line reports the measured `min margin` in ms
|
||||
against it rather than leaving it to be derived. The ceiling is the
|
||||
cycle-churn path: `gf_stream_wakeup` re-bases production onto the wall
|
||||
cursor only when the cursor has passed it, so a lead that survives an idle
|
||||
gap skips the re-base and accumulates as dark padding — 2 and 10 ms give an
|
||||
identical 64 790-byte churn stream, 15 ms and above inflate it to ~225 k.
|
||||
**Owed:** make the re-base reclaim the overshoot, which is what unlocks a
|
||||
lead beyond 10 ms.
|
||||
|
||||
Done: the producer runs `SCHED_FIFO` one below the shipper, `core_mx`
|
||||
carries priority inheritance to bound the inversion that promotion would
|
||||
otherwise create, and the clamp count is reported per run at `WARNING`.
|
||||
**Still owed: gate or throttle the camera while a job runs.** Priority
|
||||
alone does not cover it — bench runs 90 s apart on one image show a nice-5
|
||||
CPU hog passing clean (20 legs, 0 clamps) while the camera streaming
|
||||
clamped 7 runs, because its per-frame cache maintenance over a 4.8 MB
|
||||
non-coherent capture buffer is kernel-context work no userspace priority
|
||||
can preempt. Measured stall: 3.9–4.4 ms. Capture resolution is the lever
|
||||
that shortens it (the mainline `ov5648` offers 1280×960 and 640×480 binned
|
||||
modes, 4.1× and 16.4× fewer bytes); frame rate only spaces the stalls out,
|
||||
and the existing `FORGECTRL_STREAM_FPS` cap skips demosaic and encode but
|
||||
still dequeues every frame. Shares the bench slot with item 8.
|
||||
|
||||
The kernel is now UP (no SMP locking) with the performance governor as
|
||||
the only governor (no 396 MHz idle floor, no ondemand sampling delay),
|
||||
and the video offload's hardware frame skip halves the dequeues the
|
||||
cache maintenance rides on. On that image the campaign's
|
||||
`motion.step-timing-under-load` (a nice-5 hog against a job) passed with
|
||||
no clamped events. Still to measure: the same drill with the stream live
|
||||
(the run's `clamped` count and `min margin`), which decides whether the
|
||||
camera gate is still owed or the item closes.
|
||||
17. **Laser power model: dose by FIRE-bit density.** grblHAL maps S onto the
|
||||
16. **Laser power model: dose by FIRE-bit density.** grblHAL maps S onto the
|
||||
analog PWM duty (`$30`/`$31` → `$35`/`$36`, written raw into PWMSAR against
|
||||
the 127-count period). `$35` now ships at 16, the measured lasing
|
||||
threshold (facts bank), which keeps M4's velocity-scaled power out of the
|
||||
@@ -1417,7 +1372,7 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
byte in the stream today, and the feeder contract forbids back-to-back
|
||||
power bytes, while under FIRE dithering the duty is a constant sent once
|
||||
per run and a per-pixel level change costs no stream byte at all.
|
||||
18. **Gapless pause and resume in GRBL mode (planned).** A pause leaves a mark
|
||||
17. **Gapless pause and resume in GRBL mode (planned).** A pause leaves a mark
|
||||
in the cut. With laser mode on, the core stops the beam at the start of the
|
||||
hold (`disable_laser_during_hold`, on by default), so the head travels the
|
||||
whole deceleration dark, and the resume re-accelerates from a standstill at
|
||||
@@ -1451,7 +1406,7 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
line does to it, and how it composes with the armed window's disarm grace
|
||||
across a long hold.
|
||||
|
||||
19. **Head crash and rail-contact detector (planned).** The head
|
||||
18. **Head crash and rail-contact detector (planned).** The head
|
||||
accelerometer is the motion-liveness probe and nothing more; the
|
||||
factory runs two tiers off the same sensor (a per-axis alert that
|
||||
pauses, a per-axis abort), and its thresholds arrive in every pulse
|
||||
@@ -1463,7 +1418,7 @@ Open items only. Anything closed is in `CAMPAIGN-LOG.md`.
|
||||
are established. A pause on contact, on the factory's shape, would be
|
||||
the first use.
|
||||
|
||||
20. **Image trims not taken.** Two rootfs reductions the kernel review left
|
||||
19. **Image trims not taken.** Two rootfs reductions the kernel review left
|
||||
on the table, each wanting a check before it lands. The `python3`
|
||||
meta-package installs `python3-modules` (tkinter, idle, 2to3, pydoc,
|
||||
ensurepip, venv, the debugger, doctest, asyncio, multiprocessing,
|
||||
@@ -1490,7 +1445,7 @@ covers the warm-up hold), the supply temperature window (the service sends
|
||||
the whole ADC range and the factory binds it to nothing; the supply is
|
||||
watched per job instead), the head, lid, interconnect and fused temperature
|
||||
ceilings (no sensor at those locations; the chassis is watched per job), the
|
||||
head accelerometer thresholds (item 19), the lid IR thresholds (item 4), the
|
||||
head accelerometer thresholds (item 18), the lid IR thresholds (item 4), the
|
||||
HV current caps (the sampled emission witness covers the idle case, and HV
|
||||
current is ranged per job), the thermal report upload conditions and the
|
||||
pump flag. Beam detect stays with item 15.
|
||||
|
||||
@@ -4125,6 +4125,19 @@ A bench note for the next hot install: a file copied to the board with `scp`
|
||||
lands without its execute bit, and busybox `cp` keeps that, so the supervisor
|
||||
loops on exit 127 until a `chmod 755`.
|
||||
|
||||
## 2026-08-26: step timing under CPU contention closed
|
||||
|
||||
The operator closed BRINGUP "Next work" item 16, step timing under CPU
|
||||
contention: the video work resolved it. The basis is above (2026-08-24, "the
|
||||
SoC under a full core"): the kernel runs UP with the performance governor as
|
||||
the only governor, the hardware frame skip of the video offload halves the
|
||||
dequeues that the cache maintenance rides on, and the catalog test
|
||||
`motion.step-timing-under-load` passed on that image with no clamped events.
|
||||
The stream-live re-measure and the camera gate that the item still listed
|
||||
are not owed. The item is removed from BRINGUP, and the items after it are
|
||||
renumbered: 17 to 20 are now 16 to 19. `GFSINK_LEAD_MS` (default 10) and
|
||||
the per-run margin report stay as shipped.
|
||||
|
||||
## Superseded status notes
|
||||
|
||||
### Shared machine services — remaining polish, as listed 2026-08-13
|
||||
|
||||
@@ -153,7 +153,7 @@ demonstrably untouched.*
|
||||
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 `docs/ACCEPTANCE.md` - the gate recomputes every catalog test's
|
||||
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 =
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ decides which results still apply to the image that is running, exports
|
||||
the release artifact `scripts/release.sh` gates on, and serves the bench
|
||||
diagnostics page. The contract - catalog, campaigns, fingerprints,
|
||||
inheritance, the gate, the coverage rule - is
|
||||
[`docs/ACCEPTANCE.md`](../docs/ACCEPTANCE.md).
|
||||
[the Acceptance page of the documentation site](https://docs.forgefirm.org/developers/acceptance/).
|
||||
|
||||
## Run the host tests
|
||||
|
||||
|
||||
-148
@@ -1,148 +0,0 @@
|
||||
# Building ForgeFIRM with kas
|
||||
|
||||
The **forgefirm** repo is the base of the project: it controls the build,
|
||||
the resulting firmware images land here, and all build/install docs live here.
|
||||
It uses [**kas**](https://kas.readthedocs.io/) to manage Yocto layers and drive
|
||||
the build.
|
||||
|
||||
## Baseline
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Yocto release | **Scarthgap 5.0 LTS** |
|
||||
| Kernel | **linux-fslc 6.12** (mainline LTS, from meta-freescale) |
|
||||
| Machine | `glowforge` (i.MX6 Solo SOM; Basic/Plus/Pro) |
|
||||
| Distro | `forgefirm` |
|
||||
| Image | `forgefirm-image` |
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
openglow-forgefirm/
|
||||
├── forgefirm/ ← THIS repo, the base
|
||||
│ ├── kas/
|
||||
│ │ ├── forgefirm-glowforge.yml ← build entry point
|
||||
│ │ └── README.md ← this file
|
||||
│ ├── meta-forgefirm/ ← the forgefirm layer (this repo)
|
||||
│ ├── BUILD.md / INSTALL.md / SERIAL.md
|
||||
│ ├── layers/ ← kas-cloned upstreams (gitignored)
|
||||
│ ├── build/ ← bitbake output incl. images (gitignored)
|
||||
│ ├── downloads/ sstate-cache/ ← caches (gitignored)
|
||||
│ └── .gitignore
|
||||
└── meta-openglow/ ← Glowforge BSP layers (local sibling checkout)
|
||||
```
|
||||
|
||||
`meta-openglow` is referenced as a **local sibling** (`../meta-openglow`), so
|
||||
its in-place edits are what gets built. The commented pinned-remote block in
|
||||
`forgefirm-glowforge.yml` makes the forgefirm repo fully self-contained when
|
||||
flipped on. The source repos the recipes build (`kernel-module-glowforge`,
|
||||
`grblHAL-glowforge`, `forgectrl`, `python3-gfhardware`, `Glowforge-Utilities`)
|
||||
are fetched by pinned `SRCREV` and are not needed as local checkouts.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
A Linux build host, or **WSL2** on Windows (officially supported by Yocto).
|
||||
|
||||
> **WSL2 note:** keep this whole tree on the WSL2 *native* ext4 filesystem
|
||||
> (e.g. `~/dev/openglow-forgefirm`), **not** under `/mnt/c/...`. The Windows
|
||||
> mount breaks case-sensitivity/permissions and is very slow for Yocto. Give the
|
||||
> WSL2 VM plenty of RAM and disk in `.wslconfig`.
|
||||
|
||||
```bash
|
||||
pipx install kas # or: pip install kas
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Run from the **forgefirm repo root** so outputs land inside it:
|
||||
|
||||
```bash
|
||||
cd forgefirm
|
||||
kas build kas/forgefirm-glowforge.yml # fetch layers + full build
|
||||
kas shell kas/forgefirm-glowforge.yml # interactive bitbake environment
|
||||
kas dump kas/forgefirm-glowforge.yml # print the resolved config
|
||||
```
|
||||
|
||||
The bootable image lands in `build/tmp/deploy/images/glowforge/`. Flashing /
|
||||
dual-boot install steps are in [`../INSTALL.md`](../INSTALL.md).
|
||||
|
||||
### Container build (optional, reproducible host)
|
||||
|
||||
```bash
|
||||
cd forgefirm
|
||||
kas-container build kas/forgefirm-glowforge.yml
|
||||
```
|
||||
|
||||
## Pinning exact versions (reproducible builds)
|
||||
|
||||
The config tracks the `scarthgap` **branch** of each upstream layer. To lock
|
||||
every layer to an exact commit:
|
||||
|
||||
```bash
|
||||
kas lock kas/forgefirm-glowforge.yml # writes kas/forgefirm-glowforge.lock.yml
|
||||
```
|
||||
|
||||
kas auto-loads the lockfile on subsequent runs. Commit it; refresh deliberately.
|
||||
|
||||
## Pins, pushes, and the release flow
|
||||
|
||||
The build is reproducible only when recipe pins, layer branches, and the kas
|
||||
config move in the right order.
|
||||
|
||||
- **Every source repo is pinned.** `kernel-module-glowforge`,
|
||||
`python3-gfhardware`, `Glowforge-Utilities`, `grblHAL-glowforge` and
|
||||
`forgectrl` are fetched from GitHub at an exact `SRCREV`; there is no
|
||||
`AUTOREV` anywhere. When a source repo changes: push it, then bump the pin
|
||||
deliberately (BSP recipes in `meta-openglow`, ForgeFIRM components in
|
||||
`meta-forgefirm`) and re-verify with `bitbake -c fetch <recipe>`. A
|
||||
component's `SRCREV` (and the `PV` that moves with it) lives in
|
||||
`<recipe>-pin.inc` next to the recipe, and nothing else goes in that file:
|
||||
the image manifest leaves `*-pin.inc` out of the layer content hash, so a pin
|
||||
bump changes the component's fingerprint and only that
|
||||
(`docs/ACCEPTANCE.md`). A pin written into the recipe body still builds, but
|
||||
counts as a platform change and forces a full acceptance campaign.
|
||||
- **`meta-openglow` lives on its `scarthgap` branch** (Yocto layer convention;
|
||||
the Dunfell-era `master` is untouched). Development happens on the local
|
||||
sibling checkout; `scarthgap` is pushed as work lands.
|
||||
- **The upstream layers are locked** by `kas lock` (poky, meta-openembedded,
|
||||
meta-freescale, meta-freescale-distro); the lockfile is committed and
|
||||
refreshed deliberately.
|
||||
- **At release time**: flip `meta-openglow` in `forgefirm-glowforge.yml` from
|
||||
the local-sibling block to the pinned-remote block (the commented block in
|
||||
the file), refresh `kas lock`, tag all repos, and prove self-containment by
|
||||
building from a fresh clone. Then `scripts/release.sh <version>` gates
|
||||
(version single-source, rootfs-vs-slot size, installer-embedded pubkey vs the
|
||||
signing key, factory-era fwup verification, and the acceptance gate: the
|
||||
committed `releases/v<version>/acceptance.json` from the bench campaign must
|
||||
authorize the built rootfs, `docs/ACCEPTANCE.md`), builds, packs and signs
|
||||
`forgefirm.fw`, stages the assets with `sha256sums.txt`, and prints the
|
||||
`gh release create` command. Assets and their exact names (the installer and
|
||||
the update manager download them verbatim): `forgefirm.fw`,
|
||||
`sha256sums.txt`, `forgefirm-image-glowforge.rootfs.wic.gz`, plus
|
||||
`acceptance.json` and `acceptance.md`. The release tag `v<version>` =
|
||||
`FORGEFIRM_RELEASE` = the rootfs `/etc/forgefirm-version` = the `.fw`
|
||||
meta-version; `release.sh` enforces the agreement.
|
||||
|
||||
For fast iteration on a source repo, bump its pin per iteration, or add a
|
||||
**local, untracked** `externalsrc` bbappend pointing at a working checkout;
|
||||
never commit one, or released images stop matching the pins.
|
||||
|
||||
## Build-time facts
|
||||
|
||||
- `ACCEPT_FSL_EULA = "1"` is set in the kas config: the image carries NXP's
|
||||
VPU firmware blob, and `firmware-imx-lic` ships the EULA text beside it.
|
||||
- Every `LICENSE` string in the layers (`meta-forgefirm`, `meta-glowforge-bsp`,
|
||||
`meta-openglow-core`) is SPDX; recipes for third-party components with more
|
||||
than one license (`wlconf`, `python3-gfhardware`) declare each with a
|
||||
checksum on its license text.
|
||||
- `forgefirm-image-dev` is a strict superset of `forgefirm-image` (the bench
|
||||
image, `docs/ACCEPTANCE.md`); every build produces both.
|
||||
- The kernel is `linux-fslc` with the board's device tree, config fragment and
|
||||
layer patches in `meta-openglow/meta-glowforge-bsp/recipes-kernel/linux/`;
|
||||
the bbappend header lists the patches and `glowforge.cfg` documents the
|
||||
config. The bootloader recipe is `u-boot_2020.01.bb` in `recipes-bsp`.
|
||||
|
||||
Design facts (the pulse ring, real-time choices, hardware measurements) are in
|
||||
`docs/BRINGUP.md` ("Hardware facts bank") and
|
||||
`kernel-module-glowforge/UAPI.md`; bench status and open work are
|
||||
`docs/BRINGUP.md`; the dated record is `docs/CAMPAIGN-LOG.md`.
|
||||
@@ -2,8 +2,9 @@
|
||||
# ForgeFIRM — kas build configuration (factory Glowforge control board)
|
||||
# ============================================================================
|
||||
# The forgefirm repo is the BASE: it controls the build, the output firmware
|
||||
# images land here (build/tmp/deploy/images/glowforge/), and the build/install
|
||||
# docs live here (BUILD.md, INSTALL.md, SERIAL.md, kas/README.md).
|
||||
# images land here (build/tmp/deploy/images/glowforge/), and the install docs
|
||||
# live here (INSTALL.md, SERIAL.md). The build and release procedure is on the
|
||||
# documentation site: https://docs.forgefirm.org/developers/
|
||||
#
|
||||
# Target : Yocto Scarthgap (5.0 LTS) + linux-fslc 6.12 (mainline LTS)
|
||||
# Machine: glowforge (i.MX6 Solo SOM inside Basic/Plus/Pro)
|
||||
@@ -83,8 +84,8 @@ repos:
|
||||
meta-openglow-core:
|
||||
meta-glowforge-bsp:
|
||||
#
|
||||
# Pinned-remote alternative for a fully self-contained clone (see
|
||||
# kas/README.md "Push & release order"):
|
||||
# Pinned-remote alternative for a fully self-contained clone (see the site,
|
||||
# Developers, "Release flow"):
|
||||
# meta-openglow:
|
||||
# url: https://github.com/ScottW514/meta-openglow.git
|
||||
# branch: scarthgap # pin via kas lock / a tag at release
|
||||
@@ -105,7 +106,7 @@ local_conf_header:
|
||||
|
||||
# The kernel defaults to linux-fslc 6.12 in conf/machine/glowforge.conf
|
||||
# (with the factory drivers forward-ported — EPIT/SDMA/OV5648/glowforge.ko —
|
||||
# see kas/README.md backlog #2). This explicit pin is redundant but harmless.
|
||||
# see the site, Developers, "Build"). This explicit pin is redundant but harmless.
|
||||
kernel: |
|
||||
PREFERRED_PROVIDER_virtual/kernel = "linux-fslc"
|
||||
PREFERRED_VERSION_linux-fslc = "6.12%"
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
One directory per release, `v<version>/`, holding the `acceptance.json` and
|
||||
`acceptance.md` that forgetest exported on the bench for that release.
|
||||
`scripts/release.sh` refuses to sign a release whose artifact does not
|
||||
authorize the built rootfs; see `docs/ACCEPTANCE.md`.
|
||||
authorize the built rootfs; see https://docs.forgefirm.org/developers/acceptance/.
|
||||
|
||||
@@ -4,7 +4,7 @@ Hardware-verification tools for the ForgeFIRM bench. All run ON the
|
||||
target board (dev image, python3 present) unless noted. The dev image
|
||||
installs them under `/usr/share/forgetest/bench/`, and the acceptance
|
||||
tool's **Bench diagnostics** tab (`http://<machine>:8090/#bench`,
|
||||
`docs/ACCEPTANCE.md`) runs them with their arguments and the output on
|
||||
the site, Developers, "Acceptance") runs them with their arguments and the output on
|
||||
the page - takeover and scope tools get forgectrl and the controller
|
||||
stopped and started around the run, live tools need the operator
|
||||
acknowledgment; the acceptance catalog itself is built from ports of
|
||||
|
||||
+7
-4
@@ -24,7 +24,8 @@
|
||||
# RELEASE_STAGING_DIR where release assets are staged
|
||||
# (default: <repo>/release-staging)
|
||||
# FORGEFIRM_ACCEPTANCE_SKIP set to 1 to bypass the acceptance gate
|
||||
# deliberately (never the default; docs/ACCEPTANCE.md)
|
||||
# deliberately (never the default; see the site,
|
||||
# Developers, "Acceptance")
|
||||
#
|
||||
# Version contract: <version> == FORGEFIRM_RELEASE in forgefirm-image.bb
|
||||
# == /etc/forgefirm-version ("v<version>") in the built rootfs == .fw
|
||||
@@ -142,7 +143,8 @@ STAMP=$(debugfs -R "cat /etc/forgefirm-version" "$EXT4" 2>/dev/null)
|
||||
# Acceptance gate: the committed acceptance artifact must authorize THIS
|
||||
# build. scripts/acceptance-gate.py recomputes every catalog test's domain
|
||||
# fingerprint from the manifest inside the release rootfs and requires the
|
||||
# recorded PASS to match (docs/ACCEPTANCE.md). A release is never signed
|
||||
# recorded PASS to match (https://docs.forgefirm.org/developers/acceptance/).
|
||||
# A release is never signed
|
||||
# without it; FORGEFIRM_ACCEPTANCE_SKIP=1 bypasses deliberately and loudly.
|
||||
ART="$REPO/releases/v$VERSION/acceptance.json"
|
||||
if [ -n "${FORGEFIRM_ACCEPTANCE_SKIP:-}" ]; then
|
||||
@@ -202,7 +204,8 @@ fi
|
||||
|
||||
echo "== stage assets =="
|
||||
cp -L "$DEPLOY/forgefirm-image-glowforge.rootfs.wic.gz" "$STAGE/forgefirm-image-glowforge.rootfs.wic.gz"
|
||||
# The acceptance artifact travels with the release (docs/ACCEPTANCE.md).
|
||||
# The acceptance artifact travels with the release (see the site,
|
||||
# Developers, "Acceptance").
|
||||
ASSETS="forgefirm.fw sha256sums.txt forgefirm-image-glowforge.rootfs.wic.gz"
|
||||
if [ -f "$ART" ]; then
|
||||
cp "$ART" "$STAGE/acceptance.json"
|
||||
@@ -219,7 +222,7 @@ cat <<EOF
|
||||
|
||||
== release v$VERSION staged ==
|
||||
|
||||
Pre-publish checklist (kas/README.md "Push & release order" step 4):
|
||||
Pre-publish checklist (docs.forgefirm.org, Developers, "Release flow"):
|
||||
- meta-openglow pushed; kas config flipped to the pinned-remote block
|
||||
- kas lock refreshed
|
||||
- self-containment proven from a fresh clone
|
||||
|
||||
Reference in New Issue
Block a user