Skip to content

ADR-0011: Pi Zero 2 W Firmware Update System

Depends on: None (greenfield for the KN-86 Deckline platform) Related:

  • docs/plans/2026-04-21-pi-zero-firmware-update-research.md (primary source brief for this ADR)
  • Repo root CLAUDE.md (Canonical Hardware Specification)
  • GWP-144 (tools/kn86fw/ image builder — canonical .kn86fw format owner, Wave 1)
  • ADR-0003 (archived; earlier Pico 2 / RP2350 exploration, superseded by this ADR after the Pico target was dropped)

Notion task: GWP-145


The KN-86 Deckline runs on a Raspberry Pi Zero 2 W driving an Elecrow 7” IPS display (see CLAUDE.md Canonical Hardware Specification). This ADR specifies the firmware update architecture for that hardware.

Today the device has no user-facing update path — firmware is pushed to the bench rig via scp over the lab network, which is fine for development but does nothing for the consumer-grade update UX the platform needs.

We need a Panic Playdate-style “plug into computer, drag file, done” update ritual that works with the KN-86’s hardware envelope (Pi Zero 2 W + SD card + full Linux). The research brief at docs/plans/2026-04-21-pi-zero-firmware-update-research.md surveyed the USB transport options, prior art (Playdate, Analogue Pocket, Miyoo Mini, Steam Deck), framework choices, and hardware risks. This ADR codifies the decisions from that brief; it does not re-research. Where decisions require inline rationale, the brief is cited by section.


For the Pi Zero 2 W, we will ship a Panic Playdate-style plug-in update flow: the device exposes the inactive SD slot as USB mass storage via g_mass_storage, the user drops a .kn86fw image onto the drive (or clicks one button in the Kinoshita-branded Tauri desktop flasher), and the device reboots into the new slot under Pi firmware tryboot so a bad update auto-rolls back on the next power cycle. The attention gesture to enter updater mode is SYS+LINK held for ~1.5 seconds during boot — detected by a small early-boot daemon after the USB HID keyboard has enumerated but before nOSh launches. The desktop flasher is the primary UX; raw drag-drop on the mounted MSC volume is the always-available fallback.


The KN-86 SD card uses a six-partition A/B layout, lifted from research brief §“Proposed minimal architecture.” This is the canonical layout for every KN-86 SD image.

#DeviceFilesystemSizePurpose
p1/dev/mmcblk0p1FAT32~64 MBCommon boot region: autoboot.txt, bootcode, shared stage-1 files. Never A/B-swapped.
p2/dev/mmcblk0p2FAT32~256 MBBootfs slot A (kernel, cmdline.txt, config.txt, initramfs, device-tree blobs)
p3/dev/mmcblk0p3FAT32~256 MBBootfs slot B (mirror of p2 for the inactive slot)
p4/dev/mmcblk0p4ext4(remaining / 2)Rootfs slot A (Pi OS Lite + nOSh binary + assets)
p5/dev/mmcblk0p5ext4(remaining / 2)Rootfs slot B (mirror of p4 for the inactive slot)
p6/dev/mmcblk0p6ext4~512 MB/home/shared — persistent deck state, deckstate.bin, cartridge saves. Never touched by flasher.

Rationale for sizes:

  • p1 (64 MB) — generous headroom for Pi firmware stage-1 plus autoboot.txt / tryboot metadata. Tiny compared to the card; we favour legibility over tight packing.
  • p2/p3 (256 MB each) — each bootfs holds a kernel, initramfs, device tree, plus updater-image payload (see “Updater image architecture” below). 256 MB leaves room for the updater rootfs to be included in initramfs.
  • p4/p5 (half of remaining each) — Pi OS Lite base + nOSh binary + Cipher voice assets + cartridge runtime fits comfortably under 2 GB; the split scales with whatever SD capacity the bench rig ships with.
  • p6 (~512 MB) — deckstate + saves + cartridge-local persistence. Isolated so A/B slot swaps never reset player progress. See research brief §Risks item 5.

The SD provisioning script (Phase 2 action item below) partitions a fresh card with parted, formats each partition, and dd’s the first rootfs/bootfs pair into slot A. Slot B remains empty on first boot and is populated by the first successful update.


The KN-86’s update payload is a single .kn86fw file: a small header plus a gzipped slot image (concatenated bootfs + rootfs) that the flasher extracts and writes to the inactive slot’s bootfs + rootfs partitions.

The canonical source of truth for the .kn86fw header layout is tools/kn86fw/format/kn86fw.h, being built under GWP-144. Per Spec Hygiene Rule 1, this ADR does not restate byte offsets or field layouts — field documentation, integrity checks (SHA-256), version semantics, and magic bytes are owned by the tools/kn86fw/ README and the kn86fw.h header file. See GWP-144 for the complete specification.

The .kn86fw grammar is designed to outlast this particular hardware target: the header format, integrity fields, and version semantics are hardware-agnostic. The Pi Zero 2 W payload underneath the header is a gzipped slot image; the format has enough room for other payload shapes if a future hardware target ever warrants one (see “Future Considerations” below). Signing is deferred — this ADR ships SHA-256 integrity only; see “Risks + mitigations” for the threat-model rationale.


The KN-86 SD card carries one kernel + rootfs per slot plus a small updater payload shared by both. The “updater image” is not a separate SD partition; it is an initramfs-bundled minimal userspace that gets kexec’d into when the attention gesture is detected. This keeps the slot layout clean (no seventh rescue partition) while giving us the dedicated-updater-image architecture the research brief recommends (§“Pi Zero 2 W USB options”).

power-on
-> Pi firmware stage-1 (bootcode)
-> reads /autoboot.txt from p1 (tryboot state, active slot)
-> loads kernel + initramfs from the active bootfs slot (p2 or p3)
-> kernel boots
-> systemd-early unit "kn86-updater-gate" runs BEFORE nOSh:
- waits for USB HID keyboard enumeration (QMK-compatible controller per ADR-0018)
- opens /dev/input/event* and scans for SYS+LINK for ~2s
- if combo detected: writes /run/kn86-enter-updater sentinel
- exits 0 either way
-> next systemd unit checks for the sentinel:
- if present: kexec into updater kernel (bundled in initramfs)
- if absent: exec nOSh as normal

The initramfs (stored alongside the kernel in the active bootfs slot) contains:

  • Minimal busybox-style userspace (init, udev, mount, kexec-tools).
  • kn86-updater-gate binary (reads evdev for the attention gesture).
  • kn86-updater binary — the MSC-mode orchestrator.
  • Updater kernel image (separate from the main slot kernel; pinned to a known-good Zero 2 W kernel — see research brief §Risks item 1).
  • A minimal rootfs squashed into the initramfs for the updater to pivot into post-kexec.

Once kexec completes, the updater kernel boots into the minimal updater rootfs, and the kn86-updater binary:

  1. Identifies the inactive slot by reading autoboot.txt (if currently booted from A, inactive is B, and vice versa).
  2. Loads g_mass_storage with the inactive slot’s rootfs partition device as the backing file (e.g., modprobe g_mass_storage file=/dev/mmcblk0p5 removable=1 ro=0).
  3. Configures USB gadget mode via dtoverlay=dwc2,dr_mode=peripheral in the updater kernel’s cmdline.txt (baked into the initramfs, not runtime-switched).
  4. Presents a full-screen status on the Elecrow display:
    UPDATER MODE
    CONNECT TO HOST
    SLOT: B (inactive)
    AWAITING UPDATE...
  5. Polls for the .kn86fw write to complete (mtime-stable heuristic, or eject signal from the desktop flasher via an optional g_serial side channel; see “Desktop flasher architecture”).
  6. On completion: verifies SHA-256 of the received image, extracts the gzipped slot payload, writes bootfs partition, writes rootfs partition, updates autoboot.txt with tryboot_a_b=1 + new slot, triggers reboot "0 tryboot".

Per research brief §“Pi Zero 2 W USB options,” MSC mode runs only while nOSh is not running, which is exactly what the kexec’d updater provides. The user’s saved state on p6 is never touched during any of this.


The attention gesture to enter updater mode is SYS+LINK held for ~1.5 seconds during boot.

LINK thematically signals host connection (TRRS / serial port), which matches the updater’s USB-to-host flow — entering updater mode is linking the device to a host computer, and the legend on the key reinforces the mental model. Both SYS and LINK are firmware-family keys (gray legend), so holding them together at boot reads as a system-level ritual consistent with entering an OS-managed mode rather than a cartridge-level action. The combo is at the corners of the function-key cluster, which makes it a deliberate two-handed gesture rather than something an operator can fall into by resting a thumb on a key during power-on.

The canonical location for this configuration is kn86-firmware/include/updater_config.h. This file does not exist yet — it is spec’d here as the Phase 2 creation target. Having the combo in a header means changing it (for usability tuning or to work around key conflicts) is a one-line edit, not a hunt through scripts.

Pseudocode for the header:

/* updater_config.h -- attention-gesture configuration for the boot-time
* updater gate. Values are consumed by kn86-updater-gate in the initramfs.
*/
#define KN86_UPDATER_COMBO_KEY_COUNT 2
#define KN86_UPDATER_COMBO_KEYS { KEY_SYS, KEY_LINK }
#define KN86_UPDATER_COMBO_HOLD_MS 1500
/* Scan budget after USB HID keyboard enumeration. If the combo is not held
* within this window, boot continues into nOSh. */
#define KN86_UPDATER_GATE_SCAN_MS 2000
  • One-line change. Tuning the hold duration (1.5s may feel too long or too short after bench testing) is trivial.
  • Compile-time validation. A KEY_* typo is a compile error, not a runtime “gesture never fires” surprise.
  • Single source of truth. Both the kn86-updater-gate daemon and any in-firmware documentation strings (“Hold SYS+LINK to enter updater mode”) consume the same constants.

Why the scan must run after USB enumeration

Section titled “Why the scan must run after USB enumeration”

Per research brief §Risks item 3, the USB HID keyboard is not GPIO (per ADR-0018 the keyboard is a custom mech keeb with a QMK-compatible controller enumerating over USB through an internal hub IC). The key combo cannot be detected by an early GPIO scan because the keyboard is simply not connected to the Pi’s GPIO header. The scan must run after the USB stack has come up and evdev has created /dev/input/event* nodes. This pushes the gate later in the boot sequence than would be typical for a rescue-mode gesture on a bare-metal board, but still ahead of nOSh.

Phasing note: the 2-second scan window after USB enumeration adds ~2s of perceived boot time in the “not holding the combo” case. If this is judged too costly during bench testing, the research brief flags this as a spike candidate (a different early-boot scan strategy, perhaps keyboard-firmware-assisted).


The desktop flasher is Tauri 2.x — Rust backend + webview UI — per research brief §“Framework recommendation with rationale.” It is the primary user-facing update UX. Raw drag-drop on the mounted MSC volume is the fallback and always works without the app.

Two binaries ship together:

  1. kn86-flasher (Tauri UI) — Kinoshita-branded amber/black webview, device detection, progress UI, state machine.
  2. kn86-flasher-helper (elevated CLI) — performs raw block-device writes. Invoked by the Tauri UI via:
    • Windows: runas / UAC prompt (via the elevated-command crate).
    • Linux: pkexec (polkit).
    • macOS: Authorization Services / SMJobBless-style helper install.

Cross-platform elevation is covered by the elevated-command crate and the approach documented in Tauri GitHub Discussion #4201 — both referenced in the research brief §Sources. The split-binary architecture mirrors what balena-etcher does with Electron and what rpi-imager does with Qt.

The flasher detects a KN-86 device in updater mode by:

  1. USB VID/PID filter — the g_mass_storage gadget advertises a stable VID/PID assigned to the KN-86. See “Open questions” for PID allocation path.
  2. MSC volume label — the backing partition is formatted with a volume label KN86_UPDATE so the flasher can positively confirm “yes, this is the right drive” and refuse to write to a user’s unrelated USB drive that happens to share the VID/PID.
IDLE
| (USB device with matching VID/PID + volume label detected)
v
DEVICE_DETECTED
| (user clicks "Update" in UI)
v
CONFIRM
| (user confirms destination slot + image version)
v
FLASHING ----> (helper binary writes .kn86fw + fsync)
|
v
VERIFY ----> (helper binary reads back, SHA-256 checks)
|
+----> COMPLETE ("You can unplug now")
|
+----> ERROR (actionable message + retry CTA)

The flasher lives at tools/flasher/ — Wave 2. This sibling to tools/kn86fw/ keeps desktop tooling centralised. Both crates can share a workspace-level kn86-fw-format crate that encapsulates the .kn86fw header parsing (see “Emulator parity” below).

The flasher does not check for new firmware on a server — the user brings a .kn86fw file (downloaded from the KN-86 website or built locally). Automated discovery is out of scope for Phase 2; call it a Phase 3 nicety.


The A/B switch is handled entirely by Pi firmware’s tryboot support plus a 60-second boot-success sentinel written by nOSh on first healthy boot of a new slot. No RAUC; per research brief §Risks item 7, RAUC is overkill for this platform.

autoboot.txt lives on p1. It is the canonical A/B switch record. The flasher rewrites it atomically on update completion.

[all]
tryboot_a_b=1
boot_partition=3 # points at the new slot's bootfs (p3 = B, p2 = A)

When tryboot_a_b=1, Pi firmware boots the indicated slot once. If nOSh does not write the boot-success sentinel before the next reboot, Pi firmware automatically falls back to the other slot on the subsequent power cycle. This is native Pi firmware behaviour — no custom bootloader required.

On first healthy boot of a new slot, nOSh (after ~60 seconds of successful operation) writes a sentinel file to p1:

/boot/kn86-boot-success

The sentinel’s presence at next boot signals that the new slot is trusted. At that point, the flasher / updater commits the switch (clears the tryboot_a_b=1 flag, so subsequent reboots go directly to the new slot without the try/commit dance).

The 60-second window is deliberate: it gives the user time to notice catastrophic issues (boot loop, display fails to initialise, audio silent) and power-cycle to trigger auto-revert, while being short enough that a user who says “looks fine” and puts the device down will let the commit happen automatically.

power-on
|
v
+------------------+
| read autoboot.txt |
+--------+---------+
|
+------------+------------+
| |
tryboot_a_b=0 tryboot_a_b=1
(committed) (pending commit)
| |
v v
boot committed slot boot pending slot
(p2 or p3 as set by (slot just written by flasher)
boot_partition) |
| v
v +--------------------+
run nOSh | nOSh runs ~60s? |
| +---------+----------+
v |
[sentinel +--------+--------+
already yes no /
written or (healthy) power-cycle
irrelevant] | before
v sentinel
+------------------+ |
| write | v
| /boot/kn86-boot- | Pi firmware
| success | auto-reverts
+---------+--------+ to other slot
| on next boot
v (tryboot
+------------------+ contract)
| flasher clears |
| tryboot_a_b=1 |
| on next connect; |
| new slot is now |
| committed |
+------------------+

Who writes what, when: this flow has three actors (Pi firmware, nOSh, the flasher). The exact sequencing — especially when the flasher gets to clear tryboot_a_b=1 vs. nOSh doing it itself on first healthy boot — is flagged as an open question below.


Phase 0 — .kn86fw image builder (Wave 1, in progress). GWP-144 is building tools/kn86fw/ this wave: the canonical header parser, SHA-256 verifier, and a CLI that produces .kn86fw images from a built slot tarball. Status: in progress.

Phase 1 — skipped. No intermediate milestone; we go from the image builder straight to the full update system in Wave 2. Rationale: the image builder is a narrow, reusable primitive with an independent release surface, so it earns its own wave. Everything else on this ADR is platform-specific to the Pi Zero 2 W and lands as one coherent chunk.

Phase 2 — Full implementation (Wave 2). Ships:

  • SD provisioning script (partitions, initial slot A install).
  • Updater image (initramfs + key-combo gate daemon + kexec path + g_mass_storage orchestrator).
  • updater_config.h + early-boot scan daemon.
  • Desktop Tauri flasher (UI + Rust backend + elevated helper, cross-platform).
  • A/B tryboot logic + autoboot.txt update path + sentinel write hook in nOSh boot.
  • Bench-test matrix (USB stability, Windows re-plug, battery behaviour, 50-cycle update loop).

See “Action items” for the individual day-sized tasks within Phase 2.


Lifted from research brief §Risks and promoted to first-class status:

  1. g_mass_storage kernel regressions on Zero 2 W. Forum reports of flaky MSC behaviour vs. the original Zero W on recent kernels (research brief §Risks item 1 + Sources: Pi Forum regressions thread).

    • Mitigation: pin to a validated kernel version, bake it into the initramfs / updater image, and gate kernel upgrades behind explicit approval.
    • Test bar: 50-cycle write/read/verify matrix across macOS, Windows, and Linux hosts before trusting in any external hands.
  2. Windows USB VID/PID caching. Windows caches device IDs per USB port; moving the device to a different port can trigger driver-install dialogs or stale-ID conflicts (research brief §Risks item 2).

    • Mitigation: request a stable PID (pid.codes or Pi Foundation allocated range — see “Open questions”), ship the flasher code-signed, document the Windows udev/driver cert path where applicable, and re-plug test across every USB port on a reference Windows box.
  3. Early-boot key-scan timing. The custom mech-keeb keyboard (ADR-0018) is USB HID, not GPIO (research brief §Risks item 3), so the scan must run after USB stack enumeration — later than a typical early-boot rescue gesture. This adds perceived boot latency and depends on udev timing.

    • Mitigation: dedicated systemd-early unit reading /dev/input/event* for ~2s before nOSh starts. If bench testing reveals timing fragility, spin out a spike to evaluate keyboard-firmware-assisted signalling (e.g., QMK-compatible controller sets a GPIO pin high during key-hold, Pi samples GPIO at any time).
  4. Battery/power path under update. PowerBoost 1000C is supposed to pass USB power through to both the Pi and the charger, but the interaction between USB-gadget peripheral mode and charge negotiation on the same port has not been bench-confirmed (research brief §Risks item 4).

    • Mitigation: bench test with current meter before trusting. The desktop flasher should warn on low battery (optional g_serial side channel as a status query) but not block updates — USB-cable-delivered power is the fundamental power contract during update.
  5. Saves partition isolation. /home/shared (p6) carries the player’s identity. A flasher bug that writes across partition boundaries would be catastrophic (research brief §Risks item 5).

    • Mitigation: the flasher never opens p6, never mounts p6, and never references any device node other than the inactive slot’s bootfs + rootfs. Policy enforcement is in kn86-flasher-helper: allowed device list is hard-coded, anything else aborts.
    • Deckstate schema migrations are nOSh’s job on first boot of a new slot — never the flasher’s. If a firmware version bumps the deckstate schema, nOSh reads the old format on first boot, migrates, writes the new format back to p6.
  6. Signing deferred. This ADR ships SHA-256 integrity only — no Ed25519 signing on the Pi-side update path.

    • Mitigation: call it out explicitly in docs and on the flasher UI (“Integrity-checked, not signed”). Revisit when the threat model shifts (e.g., any broader external distribution).
    • Why defer: keys and key ceremony are a discipline unto themselves; for the current KN-86 deployment, the attention gesture + MSC-only-when-attended + physical cable requirement is already a high enough barrier for the threat model (no over-the-wire update = no remote attack surface).

The desktop emulator is a separate compile target (SDL2 on macOS/Linux, 80x25 amber-on-black). It does not simulate:

  • USB-MSC exposure.
  • g_mass_storage or any USB gadget behaviour.
  • tryboot / autoboot.txt switching.
  • The updater kexec flow.

These are hardware-target concerns. The emulator’s update story stops at .kn86fw header parsing: the same Rust kn86-fw-format crate that the flasher uses (see GWP-144) can be dropped into the emulator’s tooling so cart authors or QA can validate .kn86fw payload structure from the dev loop without needing a Pi on the bench.

For the KN-86, emulator parity is header-only: the update runtime itself is a hardware-target concern and is not mirrored in the emulator.


During active development, the fast path is scp to the bench rig — seconds, no update protocol needed. From the dev machine:

scp build/nosh pi@kn86-proto.local:~/kn86/
ssh pi@kn86-proto.local 'sudo systemctl restart kn86'

The full USB-MSC update flow is for user-facing update UX testing, not for dev iteration. In practice this means we run a full .kn86fw build + flash maybe once a week during Wave 2 as a dry run, and use scp for the hundreds of rebuild cycles in between.

Expect the build pipeline to ship both shapes:

make nosh -> bench-rig binary for scp
make update-img -> tools/kn86fw/build/kn86.kn86fw (USB-MSC payload)

The update-img target stitches together a tarball of the current bootfs + rootfs, calls tools/kn86fw/ to wrap the header, and drops a .kn86fw file in build/.


Carried forward from research brief §Risks + surfaced during decision capture:

  1. PCB USB data-routing confirmation. Does the current KN-86 PCB route USB data lines (D+/D-) to the Pi’s OTG port, or is the external connector charge-only? If charge-only, the g_mass_storage flow is unreachable without a PCB revision. Owner: Hardware.

  2. Tryboot sentinel implementation details. Who writes /boot/kn86-boot-success, when in the nOSh boot sequence, and who is responsible for clearing tryboot_a_b=1 once the slot is committed? Three candidate flows:

    • (a) nOSh writes the sentinel at 60s mark; the flasher clears tryboot_a_b on the next connect (simple, but requires the user to reconnect at least once after a committed update).
    • (b) nOSh writes the sentinel and clears tryboot_a_b itself once it observes the sentinel on a second healthy boot (more self-contained but more nOSh state).
    • (c) A dedicated systemd service owns both (cleanest separation; adds a small persistent service). Owner: Embedded Systems + nOSh design. Pick before SD provisioning script lands.
  3. PID allocation path. Do we request a Product ID from pid.codes (community-allocated range) or from the Pi Foundation’s reserved range, or run unassigned during internal dev and swap for a proper allocation before any external hands touch the device? Owner: Embedded Systems + Josh. Decide before Phase 2 ships.

  4. Ship-signing plan for external distribution. This ADR ships SHA-256 integrity only on the Pi-side update path. If any KN-86 unit ever leaves internal hands (bench-rig demo, external beta, broader distribution), do we need Ed25519 signing on the Pi-side flow? Owner: Josh + security review. Decide before any external KN-86 ship.


  • Iteration safety. A bad update never bricks a device — tryboot auto-reverts. The confidence to ship rough-draft firmware builds increases.
  • Update UX is console-grade. Plug cable, click button, done. We can put a KN-86 on a desk in front of someone unfamiliar with Linux and they can update it without a terminal.
  • Saves are isolated. /home/shared is never touched by any update path, by design. Operator identity persists across every update cycle.
  • Many moving parts. We have initramfs, kexec, tryboot, a Tauri app, a helper binary, and cross-platform elevation. Each is well-trodden on its own; the sum adds surface area.
  • Kernel pinning discipline. g_mass_storage regressions on Zero 2 W kernels mean we can’t naively track upstream. Phase 2 must document the pinned kernel version and the upgrade-gate process.
  • Cross-platform flasher testing matrix. macOS, Windows, Linux each have quirks around USB drive mounting, elevation, and VID/PID caching. The 50-cycle test matrix is the gating criterion, not a nice-to-have.
  • Kernel upgrades. Any bump to the pinned kernel version must re-run the 50-cycle USB-MSC regression test across all three host OSes.
  • Signing. If any KN-86 unit leaves internal hands (external beta or broader distribution), ADR-0011-rev-1 (or a new ADR) introduces Ed25519 signing on the Pi-side path.
  • Boot latency. The 2-second USB-enumeration wait plus updater-gate scan adds ~2s to cold boot. If this is painful in practice, the keyboard-firmware-assisted signalling spike (Risk #3) moves up the priority list.

Phase 2 implementation (Wave 2). Each item is a future Notion task with a rough day estimate.

  1. Wave 2: SD provisioning script (~1-2 days). parted + mkfs + dd bringup for a fresh microSD: six-partition A/B layout, initial slot-A rootfs install from a build tarball, autoboot.txt in committed state. Deliverable: tools/sd-provision/provision.sh that takes a device path and a .kn86fw image, produces a bootable SD card.

  2. Wave 2: Updater image (~3 days). Initramfs assembly, key-combo gate daemon, kexec into updater kernel, g_mass_storage orchestrator + display driver for the “UPDATER MODE — CONNECT TO HOST” screen. Deliverable: kn86-firmware/updater/ subtree producing the initramfs cpio.

  3. Wave 2: updater_config.h + early-boot scan daemon (~1 day). Header file at kn86-firmware/include/updater_config.h plus the kn86-updater-gate binary that consumes it. Wire into systemd-early. Deliverable: daemon binary + unit file.

  4. Wave 2: Tauri flasher UI + Rust backend (~4-5 days). Tauri 2.x scaffold, Kinoshita amber/black theme, five-state UI state machine (IDLE -> DEVICE_DETECTED -> CONFIRM -> FLASHING -> VERIFY -> COMPLETE/ERROR), device detection via VID/PID + volume label, kn86-fw-format workspace crate consumption. Deliverable: tools/flasher/ monorepo entry, working build on macOS + Linux + Windows.

  5. Wave 2: kn86-flasher-helper elevated CLI (~2-3 days). Rust binary, cross-platform elevation via elevated-command, raw block-device write + fsync + read-back verify. Hard-coded allowed-device list. Deliverable: helper binary shipped alongside the Tauri app.

  6. Wave 2: A/B tryboot + autoboot.txt logic + sentinel hook in nOSh boot (~2 days). autoboot.txt rewriter in the flasher (atomic write semantics), 60-second sentinel writer in nOSh boot path, open-question #2 resolved. Deliverable: nOSh boot-path diff + flasher autoboot.txt logic + integration test.

  7. Wave 2: Bench-test matrix (~3 days). 50-cycle update loop on at least one Zero 2 W + Elecrow bench rig, USB stability tests across macOS + Windows + Linux hosts, Windows re-plug across every port, battery behaviour under power-via-USB during update. Deliverable: bench-test report + any regression fixes gated by this test.


Per CLAUDE.md Spec Hygiene Rule 3: this ADR does not change any Canonical Hardware Specification values. No updates to the CLAUDE.md table are required. No grep-and-fix pass needed.

The only new filesystem locations this ADR introduces (and that future ADRs may need to reference) are:

  • tools/kn86fw/format/kn86fw.h — canonical .kn86fw header (owned by GWP-144, already spec’d there).
  • kn86-firmware/include/updater_config.h — attention-gesture configuration (created in Phase 2 action item #3).
  • kn86-firmware/updater/ — updater image subtree (created in Phase 2 action item #2).
  • tools/flasher/ — desktop Tauri flasher monorepo entry (created in Phase 2 action item #4).
  • tools/sd-provision/ — SD provisioning script (created in Phase 2 action item #1).

These are future directories, created in Wave 2. No existing file edits are triggered by the acceptance of this ADR.