The acceptance catalog gains the test behind forgectrl 5a31d66: an
exhaust floor at the legal maximum must trip AIRFLOW after the grace and
three ticks (hold, fire blocked, no resume, the exhaust gate TRIPPED and
named in the reason); a purge current floor at the ADC rail must trip
the same way; an exhaust floor of zero must read off in gates_off and
trip nothing; restored, the next session must run OK with every fan at
or above its floor. Three host cases against a scripted engine that
models the grace and the readings. The covers map names src/airflow.*.
COOLING.md gains 3a (the airflow gates) and the five settings with their
ranges and bands; SERVICES.md the gate rule, the rows, the AIRFLOW
verdict and fan_gates; SAFETY.md names the floors among the verdict's
inputs; BRINGUP item 19 records the gates as in with provisional floors
and the catalog at 44. Pins: forgectrl 5a31d66, forgefirm-app 81027ff
(0.1.15+git); fetch-verified.
The pulse header's envelope now reaches the cooling engine: the cloud
client (python3-gfhardware c34faa1) derives the coolant window and the
fans' minimum speeds from the header and rides them on every report,
and the engine (forgectrl 57f6064) resolves each as the stricter of its
setting and the job's, never looser and never overruling an off gate.
cloud.pause-resume, which runs a real print, now also reads the client's
"job limits from the header:" line and the engine's "effective limits:"
line from the two logs; two host cases cover the failure paths. The
needle guard lists the engine's phrases as not the app's.
COOLING.md section 2 explains what a cloud job brings with it; BRINGUP
item 19 records the pass-through as landed. Pins: forgectrl 57f6064,
forgefirm-app c34faa1 (0.1.14+git); fetch-verified.
The full acceptance campaign on dev image 20260821181036, the first built on
the <recipe>-pin.inc layout, satisfied 42 of 42 and its export authorizes a
release, so the status header, the acceptance section and item 12 describe
that rather than a campaign still owed; the catalog count is 42, not 35.
CAMPAIGN-LOG records the completion. Item 7 stops listing the cloud work the
campaign closed (the oversize live print, the progress bar watched from the
app, packaged-path cloud boot, the lid flash), records the operator's
declines (SPKI pinning, emulator parity, the ten-event pause machine) and the
reasoned memory guards, and item 19 no longer lists MCsn and PDfm as
unchecked: the client refuses both, so seventeen mandatory tags are dropped,
not nineteen.
forgefirm-app pin moves to b82d379 (python3-gfhardware): CLOUD.md and three
present-tense comments, no behavior change. No catalog consequence beyond the
pin bump itself, which the manifest scopes to the component.
Both recipes pin the same repository and the pin files say to move them
together. Documentation only on this side: the cloud doc now records what
the factory enforces from the pulse header, and what it does not.
cloud.lid-abort now proves the park with the machine's own counters
(cloud clears them at every job start, so a completed park reads back
at (0,0)); stale ring bytes replayed ahead of the park would not.
The lid/button/interlock parity change (BRINGUP item 16): the arm wait
and a running job cancel on lid or interlock the factory's way, the
button pauses/resumes, lid_policy and the cloud pause tick settings.
Cloud tests cloud.* and the new motion/laser lid tests cover the
components; the python3-gfhardware / gfutilities pins move with this in
meta-openglow.
A component pin bump counted as a platform change: the layer content hash
in the platform identity covered the recipe carrying the SRCREV, the
platform is folded into every acceptance fingerprint, so every image
that carried any component update invalidated the whole catalog (dev
image 20260816191951: every test domain-changed after a one-line
forgectrl bump; the two manifests differ only in
platform.layers.meta-forgefirm). The component entry already identifies
the pinned source file by file; the pin double-counted it.
Component pins now live in <recipe>-pin.inc (SRCREV and the PV that
moves with it, nothing else) - forgectrl, grblhal-glowforge and
forgefirm-app here, the BSP components in meta-openglow - and
forgefirm-image-manifest.bbclass leaves *-pin.inc out of the layer
content (FORGEFIRM_MANIFEST_PIN_SUFFIX). Recipe bodies, patches, config
fragments, init scripts and third-party pins with no manifest entry stay
layer content; a pin written into a recipe body still hashes (the safe
direction). manifest-from-tree.py mirrors the rule and reads pins
through the recipe's requires; test_tree_manifest.py proves both
(pin bump: hash unchanged; recipe body or inline pin: changed).
Bitbake resolves the same SRCREV/PV for every pinned recipe.
Docs: ACCEPTANCE.md (what layer content is), kas/README.md (the pin
files in the push order), BRINGUP.md (the finding and the bench
consequence: the first image built with the pin files is itself a
platform change, so its campaign is a full one; pin bumps inherit
after it).
No catalog consequence: nothing in the image's behavior changes; the
change is to the acceptance identity computation, proven by the unit
tests and the CI lint on the tree manifest.
Every image records the identity of its build inputs. forgefirm-manifest.bbclass
gives a component recipe a per-file source fingerprint (git ls-tree blob ids,
submodules recursed; file:// sources hashed with git hash-object); the kernel
and glowforge.ko publish theirs through do_deploy because kernel-module-split
leaves the module recipe's main package empty. forgefirm-image-manifest.bbclass
assembles the entries with the platform identity - machine, modules directory,
device tree hashes, layer content hashes (meta-forgefirm and the meta-openglow
layers by content, the kas-managed layers by revision) - into
/etc/forgefirm-manifest.json and deploys a copy next to the image. Layer
revisions and dirty flags stay outside the identity, so a rebuild from an
unchanged tree keeps its content hash and the release and dev images of one
build share it. The acceptance tool and the release gate read this file.
rsyslog replaces busybox syslogd/klogd (VIRTUAL-RUNTIME_base-utils-syslog,
trimmed PACKAGECONFIG) and becomes the only log writer: the appended
/etc/rsyslog.conf sets the inputs and the ff_line format and includes
the per-logger rules that `forgectrl --render-syslog` renders from the
machine settings at boot. forgefirm-logrotate becomes forgefirm-logging:
render before rsyslog starts (S19), sweep the pre-syslog log files into
/data/forgefirm/legacy-logs once, and rotate the tree at boot and hourly
by rename + HUP instead of copytruncate. Pins bumped to the pushed
forgectrl (syslog emitter, Logs tab, export), grblHAL-glowforge (syslog
emitter) and python3-gfhardware apps (syslog handlers, capture dir);
the CI harnesses set FFLOG_STDERR=1 so failure diagnostics keep the
controller's log lines. BRINGUP carries the bench validation checklist
(Next work item 14); this is an image change and rides the next flash.
SAFETY.md and the safing figure name GPIO4_06 for what it is - the
readback of the chain's HV_ENABLE output through U24 (the factory net
label E-STOP is kept as a note); BRINGUP records the rename, the
device-tree polarity flip that makes bit 4 read as HV_ENABLE itself,
the removal of the estop_halts_motion opt-in, and the bench check for
the flash that ships it. Recipe pins move to forgectrl 801f1f3,
grblHAL-glowforge b629c18 and python3-gfhardware c3d1790 (PV 0.1.5).
forgectrl ed2934b, grblHAL da4c8eb, forgefirm-app (gfhardware) 6c7534a -
all pushed and fetch-verified, license population checked for the
multi-license recipes.
forgectrl aa99d30, grblHAL-glowforge e74f53f, and the forgefirm-app
sources at python3-gfhardware c9d602a (PV 0.1.3) - the Phase 1-9
remediation work, pushed and pinned.
- Controllers stop at K80, before forgectrl at K90: runlevel 0/6 no
longer tears down the cooling engine, fire gates, and broker while a
controller may still be executing a job.
- The grblhal/gfcloud init scripts are real emergency levers: stop
routes through the supervisor (POST /controller/stop - a bare pkill
was safed and respawned seconds later), start resumes supervision,
status exists, and the pkill fallback matches full executable paths
instead of truncated names or bare substrings.
- slotmigrate: the partition grow gets the same 2048-sector tolerance
as the filesystem branch (an exact compare rewrote the MBR at S02 on
every boot on disks where the grow cannot land on the last sector),
verifies it made progress, and the resize2fs retry is bounded at
three attempts with the counter kept on p3 itself.
- Installer: archive product/platform are verified after the signature,
and a validly signed OLDER release now requires an explicit yes
instead of installing as a silent downgrade. All predictable /tmp
paths in the installer and ffboot are mktemp now.
- release.sh rejects multiple positional versions (the last one used to
win silently) and a release without factory-era verification dies
unless explicitly bypassed; mkfw.sh refuses to pack when the public
key for the post-sign self-check is missing.
- forgefirm-logrotate: size-capped rotation (boot + hourly) for the
/data logs - a full /data breaks settings, update staging, and the
controllers own writes.
- Bench build scripts derive every path from their own location or
FF_SRC_TOP/FF_BUILD_TOP and log to mktemp files.
The cloud-action-surface branches are merged: forgectrl builds from main
(same revision), and the forgefirm-app include pins the python3-gfhardware
master head (adds docs/CLOUD.md and the clean-shutdown gfhome).
gfhome.py, ffmachine.py, and gfcloud.py (with the gfcloud init script
and gfhome conf sample) live in the python3-gfhardware repository's
forgefirm-app/ directory. The three recipes fetch that repo through a
shared include (forgefirm-app.inc) carrying a single pinned SRCREV,
replacing the local file:// copies. Package names, dependencies, and
installed paths are unchanged.