Skip to content

Sprint 4 Design Pack — GWP-175

Parent catalog: GWP-163 (post-v0.1 deferred) Cross-ref: GWP-227 (firmware-update-model umbrella; consumes pieces of this tree) Depth: Design pack only — NO implementation Status: Planning → In Progress (design only). Implementation gated by GWP-152 ship per the GWP-163 catalog rule.


GWP-163 says: “Design planning (ADR / gameplay spec / UI wireframe) for deferred items CAN happen now in parallel with v0.1 execution. Implementation cannot.” This pack is design-only. The PR ships this document plus an optional kn86-firmware/README.md skeleton with an explicit “design-only — implementation gated by GWP-152” notice. No .c, .h, .rs, Cargo.toml, CMakeLists.txt, or systemd-unit files are added under kn86-firmware/ in this PR.


The directory kn86-firmware/ is already named in two Accepted ADRs (ADR-0011 §“Configuration location” and §“Action items”; ADR-0020 §F3 / GWP-227 plumbing) but does not yet exist on disk. Today the device-side concerns are scattered across three trees:

  1. tools/sd-provision/pi-gen-stages/stage-kn86-{base,runtime,firmware}/ — pi-gen overlay stages that drop systemd units, udev rules, device-tree overlays, config.txt snippets, and a /opt/nosh/ skeleton into the rootfs at image-build time. These stages are correctly classified as packaging, not as the source of the things they package. (Confirmed by reading stage-kn86-runtime/00-kn86-runtime/01-run.sh, which says verbatim “The real nOSh binary install is a separate sprint task. This sub-stage only lays down the directory tree + a placeholder marker.”)
  2. tools/kn86fw/ — the Rust crate that assembles a built slot’s bootfs+rootfs into the .kn86fw field-update payload (per ADR-0020). This is the packaging tool for field updates, not the source of any binary inside that payload.
  3. kn86-emulator/ — the canonical home of the nOSh runtime source, but compiled for the desktop emulator target (SDL3 on macOS/Linux). The same source tree is intended to cross-compile to the Pi target, but no Pi-target build descriptor exists in-repo today.

The gap GWP-175 exists to close: there is no canonical home for the Pi-target build of nOSh runtime, the boot-time updater gate (kn86-updater-gate daemon + updater_config.h per ADR-0011 §“Configuration location”), or the updater initramfs (kn86-firmware/updater/ per ADR-0011 Phase 2 / ADR-0020 F3). Today an engineer asked “where does the Pi build of nOSh live?” would correctly answer “nowhere yet.” kn86-firmware/ is that home.

The sibling-tree topology already establishes the naming convention:

TreeTarget hardwareBuild toolWhat it produces
pico2-firmware/RP2350 bare-metal (Pico 2 coprocessor)CMake + Pico SDKkn86_coproc.uf2
firmware/kn86-keyboard/RP2040-class (KB2040 keyboard)QMKkn86-keyboard.uf2
kn86-emulator/desktop (macOS/Linux)CMake + SDL3kn86emu desktop binary
kn86-firmware/ (this proposal)Pi Zero 2 W (Linux userland + initramfs)CMake + cross-toolchain (recommended)nosh ELF for /opt/nosh/bin/, kn86-updater-gate daemon, updater_config.h, updater-initramfs cpio

The naming inconsistency between pico2-firmware/ (top-level) and firmware/kn86-keyboard/ (under firmware/) is pre-existing and not in scope to fix here; this design adopts the top-level form (kn86-firmware/) because (a) ADR-0011 already commits the path verbatim and (b) a project-wide tree rename is its own separate ticket if Josh wants the four firmwares co-located.

Terminology — what “firmware” means here

Section titled “Terminology — what “firmware” means here”

Per CLAUDE.md “Terminology”: on Pi Zero 2 W, the literal Pi firmware (VideoCore GPU bootloader) is vendor-supplied and we don’t touch it. So kn86-firmware/ is not firmware in the bare-metal sense the way pico2-firmware/ is. It is the device-side userland sources — the nOSh runtime binary built for the Pi target, plus a small set of early-boot binaries (the updater gate daemon, the updater-mode runtime) that behave firmware-adjacent because they own the boot flow before the rootfs is fully up. The README skeleton landed by this PR is explicit about that distinction so future readers don’t have to re-derive it.

If the name kn86-firmware/ proves misleading in practice (open question 1 below), the alternative is kn86-runtime/ or kn86-pi/. Recommendation: keep kn86-firmware/ because two Accepted ADRs already cite the path; renaming costs more in stale-reference cleanup than the precision gain is worth.


I evaluated whether kn86-firmware/ would duplicate tools/sd-provision/pi-gen-stages/stage-kn86-runtime/. It does not. The two trees have orthogonal responsibilities:

ConcernLives in pi-gen stageLives in kn86-firmware/
Source code that compiles to a binaryNo (stage is shell + static files only)Yes — nOSh runtime cross-compiled for Pi, updater-gate daemon, updater-mode binaries
systemd unit .service / .path / .timer filesYes (files/etc/systemd/system/*)No (units stay where they are; this tree only produces the binaries the units ExecStart=)
udev rulesYes (files/etc/udev/rules.d/*)No
Device-tree overlay binaries / config.txt snippetsYes (stage-kn86-firmware/)No
01-run.sh install hooks (chroot-aware)YesNo (this tree’s CI publishes binaries; the pi-gen stage’s 01-run.sh install -v -m’s them into the rootfs)
Updater initramfs cpio sourceNoYes (kn86-firmware/updater/ per ADR-0011 §“Action items” Wave 2)
Boot-time attention-gesture configuration headerNoYes (kn86-firmware/include/updater_config.h per ADR-0011 §“Configuration location”)

The contract between the two trees is one-way: kn86-firmware/ produces release artifacts (binaries + cpio); tools/sd-provision/pi-gen-stages/stage-kn86-runtime/01-run.sh consumes them via install -v -m calls into ${ROOTFS_DIR}. This is the same contract pico2-firmware/ already has with stage-kn86-firmware/ (which deposits kn86-pico.uf2 at /lib/firmware/).

Conclusion: no merge or rename of the pi-gen stages. Create kn86-firmware/ as a new sibling tree.

GWP-227 cross-reference: ADR-0020 §“Action items” assigns the updater-image work (kn86-firmware/updater/) and the updater_config.h header to that umbrella’s Wave 2. GWP-175 (this ticket) defines the container tree those subtasks land in; GWP-227 is the umbrella that schedules the work that fills it. No scope conflict — GWP-175 ships the directory layout + README; GWP-227 fills the directories with content when v0.1 ships.


kn86-firmware/ # Pi Zero 2 W device userland sources
├── README.md # this PR — design-only marker
├── CMakeLists.txt # top-level CMake, Pi-target cross-compile
├── cmake/
│ └── pi-zero-2-w-arm64.toolchain.cmake # cross-toolchain for arm64 / Cortex-A53
├── include/
│ └── updater_config.h # ADR-0011 §"Configuration location" header
├── nosh/ # Pi-target build of the nOSh runtime
│ ├── CMakeLists.txt
│ └── (no sources — sources live in kn86-emulator/src/; this is a build descriptor
│ that pulls them in with a Pi-target compile profile and SDL3-on-Pi link)
├── updater/ # ADR-0011 Phase 2 Wave 2 — updater image
│ ├── CMakeLists.txt
│ ├── gate/ # kn86-updater-gate daemon (early-boot)
│ ├── runtime/ # kn86-updater binary + g_mass_storage orchestrator
│ ├── initramfs/ # cpio assembly script + busybox-style root
│ └── kernel-cmdline.txt # updater kernel cmdline (dwc2 dr_mode=peripheral)
├── splash/ # boot-splash assets (ADR-0011-aligned amber/black)
│ └── (PNG / RGB565 / KMS console fbcon snippet)
└── tests/
└── (host-side unit tests for updater_config.h key-combo parsing,
initramfs assembly determinism, etc.)
  • Not the home of nosh source code. Sources stay in kn86-emulator/src/. kn86-firmware/nosh/CMakeLists.txt is a Pi-target build descriptor that references the sibling tree. This avoids forking the source — same .c files, two compile profiles. (If that proves awkward at implementation time, the alternative is to lift kn86-emulator/src/ to a top-level nosh-src/ and have both kn86-emulator/ and kn86-firmware/nosh/ reference it. Open question 3.)
  • Not the home of any pi-gen stage scripts. Those stay in tools/sd-provision/pi-gen-stages/.
  • Not the home of tools/kn86fw/. The Rust packager stays where it is; it consumes the .img produced by pi-gen (which contains the binaries this tree built).
  • Not the home of QMK keyboard sources (firmware/kn86-keyboard/) or Pico 2 coprocessor sources (pico2-firmware/). Those are bare-metal targets with their own toolchains.

Recommendation: CMake. Three targets considered:

OptionProConVerdict
CMakeAlready the build system for kn86-emulator/ and pico2-firmware/. Cross-toolchain files are a well-trodden path on Pi targets. Same engineers can context-switch trivially.Marginally heavier than shell for the initramfs-assembly subtree.Pick.
Cargo (Rust)Aligns with tools/kn86fw/ and the Tauri flasher (ADR-0011 §“Desktop flasher architecture”).The runtime sources are C11; rewriting them to Rust is out-of-scope for GWP-175 and would re-litigate ADR-0001. The updater gate daemon could be Rust, but the rest cannot be without huge scope creep.Reject — wrong scope.
Shell (Makefile)Lowest ceremony for the initramfs subtree.Doesn’t compose with C cross-compile; would force a bifurcated build (make for initramfs, CMake for binaries).Reject — fragmentation.

The toolchain file (cmake/pi-zero-2-w-arm64.toolchain.cmake) targets arm64 Cortex-A53 with the Debian 13 trixie sysroot per ADR-0026. Cross-compile from x86_64 Linux in the same --platform=linux/amd64 Docker container the pi-gen build already uses (docs/device/os/system-image-build.md §“Build-host setup”). No second build environment.

CMake top-level builds three artifacts:

TargetOutputConsumed by
kn86-firmware-noshnosh ELF (arm64)stage-kn86-runtime/00-kn86-runtime/01-run.sh install’s it to /opt/nosh/bin/nosh
kn86-firmware-updater-gatekn86-updater-gate ELF (arm64)New stage hook in stage-kn86-base or similar — early-boot init wiring
kn86-firmware-updater-imagekn86-updater-initramfs.cpio.gzBootfs slot installer (lives next to the kernel)

Versioning: each artifact gets KN86_BUILD_ID baked in via the same env var the pi-gen stage already uses (stage-kn86-runtime/00-kn86-runtime/01-run.sh line ~70). Same release-CI plumbing, no new secrets.


+--------------------------------------------+
| kn86-emulator/src/ (canonical nOSh src) |
+-------------+------------------------------+
|
+-----------------+-----------------+
| |
v v
+---------------------+ +-------------------------------+
| kn86-emulator/ | | kn86-firmware/nosh/ |
| (desktop CMake | | (Pi-target cross-compile |
| build, SDL3) | | CMake build, SDL3-on-Pi) |
+---------------------+ +---------------+---------------+
|
| produces /opt/nosh/bin/nosh ELF
v
+-----------------+-----------------+
| tools/sd-provision/pi-gen-stages/ |
| stage-kn86-runtime/01-run.sh |
| (install -v -m to ROOTFS_DIR) |
+-----------------+-----------------+
|
v
+-----------------+-----------------+
| tools/sd-provision/build-image.sh |
| pi-gen → .img with all binaries |
+-----------------+-----------------+
|
v
+-----------------+-----------------+
| tools/kn86fw/ (Rust packager) |
| .img slot → .kn86fw payload |
+-----------------------------------+

The dotted line “this is design-only” is everywhere in this diagram today: the Pi-target nOSh build does not exist, the updater binaries do not exist, the install hooks for them in stage-kn86-runtime/01-run.sh use a placeholder file. None of that changes with this PR. This PR only writes the design doc + a README skeleton announcing the tree’s scope.


Migration path (when implementation eventually unblocks)

Section titled “Migration path (when implementation eventually unblocks)”

When GWP-152 ships and GWP-227 schedules the implementation work, the migration is additive — nothing existing moves:

  1. kn86-firmware/include/updater_config.h — new file. ADR-0011 already specifies the contents (combo keys, hold ms, scan window).
  2. kn86-firmware/cmake/pi-zero-2-w-arm64.toolchain.cmake — new file. Standard CMake cross-toolchain pattern.
  3. kn86-firmware/nosh/CMakeLists.txt — new file. References ../../kn86-emulator/src/ for sources. The kn86-emulator/CMakeLists.txt does not change in this step.
  4. kn86-firmware/updater/ — new subtree. Per ADR-0011 §“Action items” Phase 2 Wave 2.
  5. tools/sd-provision/pi-gen-stages/stage-kn86-runtime/00-kn86-runtime/01-run.sh — replace the placeholder install ... files/opt/nosh/PLACEHOLDER ... with install -v -m 755 ${KN86_FIRMWARE_BUILD}/nosh /opt/nosh/bin/nosh plus equivalents for the updater binaries. This is a ~10-line edit, not a stage rewrite.
  6. No file in any sibling tree moves. kn86-emulator/src/ stays put. tools/kn86fw/ stays put. pico2-firmware/ stays put. firmware/kn86-keyboard/ stays put.

The only rename / move candidate is the long-term question of co-locating the four firmware trees (open question 2). That is not part of GWP-175; it would be its own ticket.


  1. Design pack lands at docs/plans/sprints/2026-04-30-gwp-175-kn86-firmware-design.md with all sections present (story, anti-duplication, layout, build target, relationship map, migration, open questions). Pass when this file is committed.
  2. Optional kn86-firmware/README.md skeleton carries an explicit “design-only — implementation gated by GWP-152” notice, names the three artifact targets the tree will eventually produce, links to ADR-0011 §“Configuration location” and ADR-0020 §F3, and references CLAUDE.md “Canonical Hardware Specification” for Pi Zero 2 W details (does not restate). Pass when the README exists with those four things and contains zero source files in sibling subdirectories.
  3. No production firmware code is added. git diff main -- kn86-firmware/ shows only README.md (and this design doc, which is not under kn86-firmware/). Pass on visual review of the PR diff.
  4. Anti-duplication confirmed. Reviewers can grep the existing tools/sd-provision/pi-gen-stages/ tree and confirm none of the file types proposed for kn86-firmware/ already exist there. Pass on the table in the “Anti-duplication check” section above being verifiable by find.
  5. ADR cross-references are accurate. ADR-0011 line 131 (kn86-firmware/include/updater_config.h), line 425 (kn86-firmware/updater/), line 446 (include path), line 447 (updater path) all point at the layout this design proposes. Verified by grep -n kn86-firmware docs/adr/ADR-0011-device-firmware-update-system.md.
  6. Notion task moves to Review with PR URL on the GitHub PR property before this design pack is considered done. PM (Josh or the orchestrator agent) takes Review → Done after PR merges.

  1. Engineer assumes kn86-firmware/nosh/ is a separate fork of the runtime. Mitigation: README skeleton is explicit that the directory is a build descriptor only and that source lives in kn86-emulator/src/. The CMakeLists.txt eventually landed there will reference the sibling tree path-relatively. Without this notice, the natural reading of “nosh subdirectory in the firmware tree” is “this is where nosh source lives,” which would invite a fork.
  2. pi-gen stage 01-run.sh runs before kn86-firmware/ has produced its artifacts. Mitigation: release-CI ordering. The build pipeline runs kn86-firmware/ cross-compile before invoking tools/sd-provision/build-image.sh, the same way it runs pico2-firmware/ build before pi-gen pulls kn86-pico.uf2 into /lib/firmware/. Documented in docs/device/os/release-setup.md (currently emulator-only — gets extended when implementation lands).
  3. Top-level firmware/ directory already exists for the keyboard. Mitigation: this design adds a top-level kn86-firmware/ (matching pico2-firmware/’s placement, and matching the path baked into ADR-0011). The naming inconsistency with firmware/kn86-keyboard/ is pre-existing and explicitly out-of-scope (see open question 2). Reviewers should not request a rename of the existing keyboard tree as part of this PR.
  4. updater_config.h ABI breakage. When GWP-227 lands the actual header, any future change to the combo keys is a one-line header edit (per ADR-0011 §“Why a header, not a config file”) — but this means the value of KN86_UPDATER_COMBO_KEYS is part of the deck’s behavioral contract with the operator. Treat header edits as user-visible changes and route them through ADR amendments to ADR-0011, not silently. This is an authoring-convention edge case worth calling out in the README skeleton.

No engineering brief — this is a design pack only. When GWP-227 schedules implementation:

  • Owner: Platform Engineering (per CLAUDE.md “Agent Roles” — Linux system image, systemd units, kiosk mode).
  • Pre-reqs: GWP-152 ships (v0.1 unblocks). ADR-0026 trixie pin is final. tools/sd-provision/pi-gen-pin.env carries the validated pin.
  • First implementation PR: scaffold cmake/pi-zero-2-w-arm64.toolchain.cmake + kn86-firmware/CMakeLists.txt + kn86-firmware/nosh/CMakeLists.txt. Verify cross-compile produces a runnable arm64 ELF. No updater work yet.
  • Second implementation PR: kn86-firmware/include/updater_config.h + kn86-firmware/updater/gate/ daemon. Wire into stage-kn86-base early-boot.
  • Third PR: updater initramfs (kn86-firmware/updater/runtime/ + kn86-firmware/updater/initramfs/). This is the bulk of GWP-227 F3.
  • Test strategy: every PR adds a kn86-firmware/tests/ host-side test that runs in CI. The first PR’s test verifies the cross-toolchain finds a sysroot. The updater-gate PR’s test verifies updater_config.h parses cleanly under both arm64 (target) and x86_64 (host, for unit tests of the combo-key matcher).

No code in this PR. The PR diff contains exactly two new files: this design doc and kn86-firmware/README.md.


  1. Confirm tree name. Three options: kn86-firmware/ (matches ADR-0011 + ADR-0020 verbatim; preferred), kn86-runtime/ (more accurate per CLAUDE.md “Terminology” — “firmware” is misleading on Linux userland), or kn86-pi/ (parallels pico2-firmware/ by hardware target). Recommendation: kn86-firmware/ — renaming costs more in stale-ADR-reference cleanup than the precision gain. Decidable yes/no.
  2. Defer or address the firmware-tree co-location question. Today: pico2-firmware/ (top-level), firmware/kn86-keyboard/ (under firmware/), proposed kn86-firmware/ (top-level). A future cleanup ticket could move all three under firmware/{pi,pico2,keyboard}/. Recommendation: defer. This PR matches the ADR-0011 path; a separate ticket can do the rename if Josh wants the tidy hierarchy. Decidable: file follow-up ticket now, or leave the inconsistency standing.
  3. Source-tree placement of the nOSh runtime. Two options: (a) leave sources in kn86-emulator/src/ and have kn86-firmware/nosh/CMakeLists.txt reference them path-relatively (proposed); (b) lift sources to a top-level nosh-src/ (or similar) and have both kn86-emulator/ and kn86-firmware/nosh/ reference it as a peer. Recommendation: (a) for v1, revisit if the cross-tree reference proves awkward. Decidable: pick (a) or (b) for the implementation PRs.
  4. Pico 2 firmware UF2 staging path. Today stage-kn86-firmware deposits kn86-pico.uf2 at /lib/firmware/kn86-pico.uf2 (per system-image-build.md line 40). Should kn86-firmware/ also host the build descriptor for the Pico 2 UF2 (i.e., become the single Pi-side pre-image build), or keep pico2-firmware/ independent and let release-CI orchestrate both? Recommendation: keep them independent. They are different targets (Cortex-A53 + Linux vs RP2350 bare-metal) and forcing them into one CMake top-level adds complexity. Decidable: yes (independent) / no (consolidate later).