diff --git a/.github/workflows/forgetest-ci.yml b/.github/workflows/forgetest-ci.yml index a2475e5..14e61c1 100644 --- a/.github/workflows/forgetest-ci.yml +++ b/.github/workflows/forgetest-ci.yml @@ -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 diff --git a/.github/workflows/yocto-cold-build.yml b/.github/workflows/yocto-cold-build.yml index 1aafcc7..c19f8b2 100644 --- a/.github/workflows/yocto-cold-build.yml +++ b/.github/workflows/yocto-cold-build.yml @@ -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) diff --git a/BUILD.md b/BUILD.md deleted file mode 100644 index aaa8ad3..0000000 --- a/BUILD.md +++ /dev/null @@ -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). diff --git a/README.md b/README.md index f4f0253..9607118 100644 --- a/README.md +++ b/README.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 diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md deleted file mode 100644 index edce36f..0000000 --- a/docs/ACCEPTANCE.md +++ /dev/null @@ -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/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 (`-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://: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/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 `.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-.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 ` builds the release image, reads -`/etc/forgefirm-manifest.json` out of the release rootfs and runs - - scripts/acceptance-gate.py releases/v/acceptance.json - -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 [--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/ the committed artifacts diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 2249f74..3b79007 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -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/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. diff --git a/docs/CAMPAIGN-LOG.md b/docs/CAMPAIGN-LOG.md index 4d0130d..13cb58d 100644 --- a/docs/CAMPAIGN-LOG.md +++ b/docs/CAMPAIGN-LOG.md @@ -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 diff --git a/docs/UPDATE-SYSTEM.md b/docs/UPDATE-SYSTEM.md index 37875b6..9ed1d8b 100644 --- a/docs/UPDATE-SYSTEM.md +++ b/docs/UPDATE-SYSTEM.md @@ -153,7 +153,7 @@ demonstrably untouched.* factory-era fwup (0.14.2) verification of the packed archive, and the **release acceptance gate**: `releases/v/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 = diff --git a/forgetest/README.md b/forgetest/README.md index 82428bf..f26eac9 100644 --- a/forgetest/README.md +++ b/forgetest/README.md @@ -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 diff --git a/kas/README.md b/kas/README.md deleted file mode 100644 index 4f033b8..0000000 --- a/kas/README.md +++ /dev/null @@ -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 `. A - component's `SRCREV` (and the `PV` that moves with it) lives in - `-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 ` 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/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` = - `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`. diff --git a/kas/forgefirm-glowforge.yml b/kas/forgefirm-glowforge.yml index ff24aed..8751e30 100644 --- a/kas/forgefirm-glowforge.yml +++ b/kas/forgefirm-glowforge.yml @@ -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%" diff --git a/releases/README.md b/releases/README.md index 80fd106..5a3c4b1 100644 --- a/releases/README.md +++ b/releases/README.md @@ -3,4 +3,4 @@ One directory per release, `v/`, 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/. diff --git a/scripts/bench/README.md b/scripts/bench/README.md index 72c3776..ad44495 100644 --- a/scripts/bench/README.md +++ b/scripts/bench/README.md @@ -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://: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 diff --git a/scripts/release.sh b/scripts/release.sh index b7382a3..6cd0490 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -24,7 +24,8 @@ # RELEASE_STAGING_DIR where release assets are staged # (default: /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: == FORGEFIRM_RELEASE in forgefirm-image.bb # == /etc/forgefirm-version ("v") 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 <