Gates (clean tree, version single-source across FORGEFIRM_RELEASE / rootfs stamp / .fw meta-version / tag, rootfs-vs-slot size with early warning, installer-embedded pubkey must match the signing key, factory-era fwup verification of the packed archive), then build, pack, sign, checksum, and stage forgefirm.fw + sha256sums.txt + forgefirm-image-glowforge.rootfs.wic.gz with the gh publish command (--publish runs it where gh is authenticated). release.sh --dev packs a dev-key-signed forgefirm-dev.fw from the release rootfs for the GUI upload path. Signing keys are always passed explicitly - no defaults. kas/README release order and the plan doc updated to match.
13 KiB
ForgeFIRM install, update & recovery system — implementation plan
Phased plan for moving ForgeFIRM from the legacy carve-out-a-partition
install to the factory's own A/B slot scheme, with signed .fw
packaging, a GUI update manager, factory restore, and a refreshed
recovery image. The measured ground truth this builds on (eMMC layout,
boot0/boot1 maps, saved-env location, factory .fw/updater internals)
is in BRINGUP.md → "eMMC boot & recovery architecture".
Settled decisions
- Factory partition scheme, unmodified: ForgeFIRM lives in the two
200 MiB rootfs slots (
mmcblk2p1/p2);/data(p3) keeps its full factory size. No repartitioning at install, ever. - Single-OS, not dual-boot: a machine runs ForgeFIRM or factory firmware, with a clean migration each way. The factory updater's behavior toward foreign slot contents is irrelevant because we never operate both long-term.
- fwup is the universal package/apply format (the factory's own
mechanism): ForgeFIRM upgrades, factory restore, and provisioning all
use signed
.fwarchives applied to the inactive slot, followed by a U-Boot env flip — exactly the factory update flow. - Factory firmware is archived to
/databefore any factory slot is overwritten. Restore-to-factory never depends on Glowforge's servers; the cloud path (GET /update/current, already implemented in gfutilities) is the optional "restore to latest" upgrade. - Release artifacts are built and signed locally, uploaded as GitHub releases. The Ed25519 private key never leaves the build host, so GitHub is untrusted hosting: machines verify signatures before applying. CI does compile checks only, never artifacts.
- Reinstalling ForgeFIRM from factory = run the installer again, until the recovery refresh (Phase 5) subsumes it.
- Recovery refresh is squashfs-only in v1: factory U-Boot, DTB and the 3.14.28 recovery kernel stay in place; only the recovery userspace is replaced.
Invariants (every flash path, every phase)
- Never write the active (running) slot.
- Machine idle; one flash operation at a time (lock file); no flash/reboot while a job runs.
- Archive factory content before the write that would destroy the last copy of it (rootfs slots; boot0/boot1 before a recovery refresh).
- Env flips are atomic: one
fw_setenv -stransaction setting all four ofmmcdev/mmchwpart/mmcpart/mmcroot. - Automatic paths (release updater, cloud restore) require a valid signature — ours or Glowforge's respectively. Manual uploads may be unsigned behind an explicit "unsigned dev image" warning.
- The image must fit the 200 MiB slot; the build fails past the size gate rather than producing an unflashable release.
- Boot selection refuses targets that fail the content probe (no kernel / no recognizable rootfs).
- Verify a written slot (fwup on-the-fly hashes, or an explicit readback/mount check for raw writes) before flipping boot to it.
Phase 0 — enablers (no eMMC flashing)
- 0.1 Slot-agnostic images. Goal: one image boots unmodified from
p1, p2, or SD, steered only by the saved env (U-Boot's
mmcargsalready takesroot=${mmcroot}from the env). Audit what our/boot/uEnv.txtcurrently sets; strip it to entries that are not per-location (fdt_fileetc.); bench-verify by flipping env alone. This removes the mount-and-sed step from every flash path. Exit: the same built image boots from two locations with no per-slot edit. - 0.2 fwup toolchain + keys. Yocto recipe for fwup (target) and a
host-side pack step. Generate the ForgeFIRM Ed25519 keypair
(custody: offline on the build host, passphrase-protected, backed
up). Compatibility gates, both directions: (a) a
.fwwe pack must apply with the factory's fwup 0.14.2 (the installer runs on factory firmware; fall back to shipping a static armv7 fwup with the installer if archive-format drift bites), and (b) our shipped fwup must apply a factory.fwverified against the GF pubkeys (carried from the factory image) for cloud restore. - 0.3 Build outputs.
forgefirm-imageadditionally emits the raw ext4 rootfs and a packed+signedforgefirm-<ver>.fwwithupgrade.a/upgrade.btasks in the factory pattern (partition-relative raw writes, unmounted-destination + on-the-fly-verify options); size gate enforced here. Acompletefull-provisioning task joins with the Phase 5 recovery work. The wic stays for SD/dev burns.
Phase 1 — ffboot v2 + slot probe
- Atomic env flip (invariant 4) — fixes the existing gaps: three
separate
fw_setenvcalls today, andmmchwpartnever set (relies on the saved 0). ffboot -l(or a sibling tool): inventory every candidate — eMMC p1/p2, legacy p4, SD — by read-only mount: factory/etc/versionor/etc/forgefirm-version, kernel presence; plus the current env selection. Machine-parsable output; this is the probe the GUI and the installer both reuse.- Exit: bench-verified flips SD ↔ eMMC slots; inventory correct for factory / ForgeFIRM / empty slots.
Phase 2 — slot installer (factory → ForgeFIRM)
Rewrite install-forgefirm.sh as a single-stage script run from
factory firmware:
- Sanity: factory 3-partition layout, both slots 200 MiB, active slot
detected (
rdev), enough/dataspace. - Archive: every factory slot version not already archived —
dd | gzipto/data/forgefirm/archive/factory-rootfs-<ver>.img.gzwith a manifest line (slot, version, date, md5); also dump boot0/boot1 (32 MiB) into the archive now, ahead of Phase 5. With both slots archived, any later overwrite needs no second archive step. - Fetch
forgefirm.fwfrom GitHub releases (fixed asset name — thereleases/latest/download/URL needs one; the version lives in the fwup metadata and the release tag), or take a local file argument for offline/dev installs. Verify the signature against the ForgeFIRM pubkey embedded in the installer (raw 32-byte form for the factory's fwup; a dev key until the production ceremony). - Apply to the inactive slot (fwup + our pubkey). The booted factory install stays bootable in the other slot.
- Atomic env flip (embed the flip logic — the factory rootfs has no ffboot v2), reboot.
No repartitioning, no /data backup/restore dance, no stage 2.
Rewrite INSTALL.md accordingly (serial console procedure stays).
Exit: a factory machine converts in one pass; ffboot returns it to
the intact factory slot; /data (calibration, credentials, logs)
demonstrably untouched.
Phase 2b — legacy p4 migration
- Boot-time init script (before
/datamounts), gated on: booted frommmcblk2p1/p2(never SD, never p4) AND legacy geometry present (p4 exists, or p3 ends short of the disk). Actions: delete p4, extend p3's end to the disk (starts unchanged),resize2fs. Idempotent and power-safe: every step keyed off actual disk state, re-runnable after interruption. - Existing p4 users reach the new scheme by running the new installer from their running ForgeFIRM (same flow as Phase 2; both factory slots intact → archive newer, overwrite older), then the boot-time check reclaims p4/p3 on first slot boot.
- Exit: a legacy-layout machine migrates with
/datacontents intact and grown to full size; re-boot is a no-op.
Phase 3 — release pipeline
scripts/release.sh(build host): gates → kas build → pack.fw→ sign →sha256sums.txt→ staged assets +gh release createcommand (--publishruns it where gh is authenticated). Gates: clean tree, version single-source, rootfs-vs-slot size (warn ≥ 170 MiB / fail ≥ 195 MiB, under bitbake's own hard cap), installer-embedded pubkey must match the signing key, and factory-era fwup (0.14.2) verification of the packed archive.- One version source:
FORGEFIRM_RELEASE= git tag =/etc/forgefirm-version=.fwmeta-version; the script enforces agreement. release.sh --devpacks a dev-key-signedforgefirm-dev.fwfrom the release rootfs for the GUI upload path (decides open question 4: dev archives are signed with the dev key, never unsigned — the GUI exercises the same verification path either way).- GitHub Actions: per-push compile checks for grblHAL-glowforge and
forgectrl (minutes, no Yocto); optional
workflow_dispatchcold-Yocto reproducibility build whose only product is a checksum.
Phase 4 — forgectrl update manager (GUI)
Backend endpoints + a panel page (OpenGlow visual identity):
- Inventory: slot contents (Phase 1 probe), current/next boot selection, archive presence/version.
- Update check against the GitHub releases API (manual button + periodic while idle; offline-tolerant, rate-limit friendly).
- Apply release: download
.fwto/data, verify signature, apply to inactive slot, verify, then flip only on explicit user confirmation, prompt reboot. - Upload: streamed multipart to
/data(never RAM-buffered); accepts.fw(verify; warn if unsigned) and.wic.gz/.ext4.gz(dev; size + superblock sanity checks). - Boot selector incl. SD, with warnings — most prominently on switch-to-factory: the factory updater may auto-update and overwrite the other slot. Refuses unprobeable targets.
- Factory restore: from the
/dataarchive (offline) or cloud latest (gfutilities device auth → GF-signed.fw→ verify with GF pubkeys) → inactive slot → flip. Optional cleanup of ForgeFIRM residue in/datafor true factory condition. - Interlocks throughout: idle-only, update lock, never the active slot, rollback = flip back to the previous slot.
Exit: full loop on the bench — GUI upgrade, rollback via boot selector, factory restore and return — without touching a shell.
Phase 5 — recovery refresh (reserved; build after 0–4 land)
- v1 scope: replace only the boot0 recovery squashfs (boot1
/usronly if needed). Never write below offset 0xC0000 in boot0 — U-Boot is physically untouchable by the refresh tool. Factory DTB and kernel 3.14.28 stay. - Userspace: static busybox + fwup + a small C webapp (ulfius) + hostapd/wpa_supplicant. No Python. Must carry 3.14.28-matched WiFi modules (decision gate: lift from the factory recovery vs rebuild from Glowforge's published GPL kernel source).
- Functions: button-hold → AP + web UI (factory UX): upload a
.fw(verified against our and GF pubkeys — either firmware installable), install from the/dataarchive, set boot target, export logs. - Flash tool: boot0/boot1 archived first (Phase 2 already does),
force_rounlock, write high regions only, readback verify; if both partitions are written, boot1 first, boot0 last. - First flashes bench-gated on an attached serial console.
- Documented recovery ladder from then on: other slot → button-hold recovery → SD card → serial console.
Contracts
- Artifacts (consumers: installer, GUI updater, recovery):
forgefirm.fw(fixed asset name; signed; version in the fwup metadata = release tagv<semver>; tasksupgrade.a/upgrade.b,completefrom Phase 5),sha256sums.txt,forgefirm-image-glowforge.rootfs.wic.gz(SD burns). - Env: SD =
0/0/1//dev/mmcblk1p1; slot N =1/0/N//dev/mmcblk2pN(mmcdev/mmchwpart/mmcpart/mmcroot, always one transaction). - Archive layout:
/data/forgefirm/archive/—factory-rootfs-<ver>.img.gz,boot0.img,boot1.img,manifest(slot versions, dates, checksums).
Open questions / decision gates
- Exact minimal
uEnv.txtcontents (Phase 0.1 spike decides). - fwup archive compatibility with the factory 0.14.2 binary — else ship a static fwup alongside the installer.
- Size-gate thresholds (proposal: warn > 170 MiB, fail > 190 MiB image vs the ~195 MiB usable slot).
- Dev-image signing policy: dedicated dev key vs unsigned-with-warning only.
- Periodic GUI update check default-on vs opt-in (it pings the GitHub API; proposal: on by default, apply always manual, config switch to disable).
- Recovery kernel modules: carried from the factory image vs rebuilt from GPL source (Phase 5 gate).
- U-Boot bootcount/auto-revert: out of scope — the recovery ladder covers bad flips; revisit only if field incidents say otherwise.
Sequencing
0 → 1 → 2 + 2b → 3 → 4 → 5, strictly: everything after Phase 0 assumes
slot-agnostic images and working .fw round-trips; the GUI (4) reuses
the probe (1) and pipeline (3); recovery (5) is an independent
mini-project once the slot scheme is provenly stable. First
user-visible milestone is after Phase 2: a converted machine with
instant offline factory switchback.