Sprint 5 Design Pack — GWP-176
Story narrative
Section titled “Story narrative”GWP-176 is the desktop GUI sibling to the device-side update path defined in ADR-0011 (Pi Zero 2 W system image update mechanism) and the multi-surface umbrella ADR-0020 (firmware update model). Both ADRs already nominate Tauri 2.x as the framework, sketch the UI state machine, and specify the helper-binary elevation pattern. This pack does not re-decide those points — it consolidates them into a single design surface, fills in screen-level wireframes, names the scaffold layout, and surfaces the open questions that need Josh-level decisions before any code lands.
Stale-ticket disambiguation. The original brief on this task referenced “kn86flash (the underlying CLI flasher tool, GWP-111).” That framing is obsolete in two ways: (a) GWP-111 was retired by ADR-0020 along with the rest of the pre-Pi firmware-cluster cohort (GWP-107, 108, 110, 111, 112, 113, 117 — see ADR-0020 L23 and L163), and (b) the Pi-OS-on-SD topology never had a standalone kn86flash CLI; the CLI surface is kn86-flasher-helper, the elevated raw-write helper invoked by the Tauri UI per ADR-0011 §“Desktop flasher architecture” L171–172. There is no second-class CLI flasher to “wrap” — the Tauri UI and its helper binary are the flasher. The header-format crate tools/kn86fw/ (GWP-144) is a separate concern (image builder and header parser, not a writer).
What’s already decided (do not re-litigate in this PR):
- Framework: Tauri 2.x. Per ADR-0011 L165 and the update-system research brief §“Framework recommendation with rationale” (
docs/device/os/update-system.mdL42–58). Rationale: Rust backend owns disk I/O directly without IPC hops, ~10 MB bundle vs Electron’s ~100 MB, native webview matches the Kinoshita boutique aesthetic, and the Rust workspace lets us share thekn86-fw-formatparser withtools/kn86fw/and any future headlesskn86-cli. - Two-binary architecture.
kn86-flasher(Tauri UI, unprivileged) +kn86-flasher-helper(elevated CLI doing raw block-device writes), invoked viaelevated-commandcrate cross-platform. Per ADR-0011 L169–177. - State machine.
IDLE → DEVICE_DETECTED → CONFIRM → FLASHING → VERIFY → COMPLETE/ERROR. Per ADR-0011 L429. - Device detection. USB VID/PID match + MSC volume label
KN86_UPDATEfor positive confirmation. Per ADR-0011 L181–185. - Image format consumed.
.kn86fw(Phase 0 today;.kn86releasecomposite per ADR-0020 §“One composite release artifact” L52 once that lands). Header parsing reusestools/kn86fw/lib.rsper ADR-0011 L210. - Crate location. ADR-0011 L210, L448 says the flasher lives at
tools/flasher/(“sibling totools/kn86fw/”). Director’s note: this design pack proposestools/kn86flash-gui/instead, both because the user brief named it that way and becausetools/flasher/is generic enough to confuse with the GUI’s underlying helper. Open question #1 below — confirm with Josh which name wins. Default if unconfirmed: follow ADR-0011 (tools/flasher/).
What this pack adds on top of the ADRs:
- Per-screen ASCII wireframes for the five UI states + error variants.
- Explicit user-story decomposition (initial bring-up, dev iteration, end-user re-flash, recovery).
- Backend-interface contract between the Tauri UI and
kn86-flasher-helper(process spawn, stdout protocol, stderr error tokens) — closes ADR-0011’s open question #1 (“flasher invokes helper how?”) at the design-pack level. - SD device enumeration strategy per OS, with the privilege escalation per OS spelled out concretely.
- Image-source UX (local file picker vs. GitHub releases pull).
- Verification UX boundary — what’s in scope for v1 vs. deferred.
- Cross-platform packaging strategy and how it relates to GWP-125 (the macOS DMG task) and the existing release CI.
- Scaffold directory proposal with file-by-file inventory.
User stories
Section titled “User stories”Four distinct user personas / scenarios drive the GUI’s UX requirements. Each gets one row of the matrix below; each row is referenced by acceptance criterion in the §Acceptance section.
| Story | User | When | Trigger | Expected outcome | Acceptance criterion |
|---|---|---|---|---|---|
| S1: Initial bring-up | Josh / hardware bring-up engineer | First time a freshly assembled prototype boots | Just-printed PCB, blank SD card from manufacturing | Insert SD, run flasher, write a known-good .kn86fw, eject. Device boots from p2/p4 (slot A) on first power-up. | AC1, AC2, AC8 |
| S2: Dev iteration | C engineer working on nOSh / cart engineer | Several times per day during nOSh-runtime feature development | Just built a new .kn86fw locally via kn86fw build. Want to push it to the dev rig without unplugging anything physical. | Hold SYS+LINK on dev rig → boots into updater mode → MSC volume mounts on dev laptop → flasher auto-detects, one click, done. <60 s round-trip. | AC1, AC3, AC4, AC9 |
| S3: End-user re-flash | Eventual customer (post-Q4 2027 ship) | When KN-86 issues a firmware update | Receives a .kn86release from the website (or auto-update notification — Phase 3 deferred) | Download .kn86release, open flasher (or click a kn86flash:// URL), follow the on-screen instructions (“hold SYS+LINK, plug in cable”), one button. Visual feedback confirms completion. Tryboot rollback handles any failed boot. | AC1, AC4, AC5, AC10 |
| S4: Recovery / re-image | End user OR engineer when both A/B slots are corrupt | Rare — only after a catastrophic failure or factory-reset request | Either (a) device boots into updater mode but neither slot is committable, or (b) user uses a separate SD card writer to re-image from scratch using a “full SD image” variant | The Tauri flasher offers a “Recovery” mode (advanced) that writes both slots A+B sequentially from a full .kn86recovery image, NOT the differential .kn86fw. Or the user pulls the SD and uses the dd-equivalent path via the tools/sd-provision/ pipeline directly. | AC6 (deferred path call-out) |
The four stories are intentionally ranked by frequency: dev iteration (S2) is the highest-volume use case and dominates the UX budget; end-user re-flash (S3) is the highest-stakes use case and dominates the polish budget; recovery (S4) is the lowest-frequency but highest-blast-radius and is explicitly deferred to a v2 scope.
UI flow + ASCII wireframes
Section titled “UI flow + ASCII wireframes”The flow follows the state machine from ADR-0011 L429: IDLE → DEVICE_DETECTED → CONFIRM → FLASHING → VERIFY → COMPLETE / ERROR. Below is the per-screen wireframe — each is a single viewport (~720×480 logical, the Tauri default) rendered as ASCII to keep design diff-able and to set expectations for the visual designer (Kinoshita AMBER #E6A020 on black #000000 per CLAUDE.md hardware spec, as amended 2026-06-13 per ADR-0036; same default palette as the device, intentionally on-brand — WHITE / GREEN alternates not surfaced in the flasher UI).
Screen 0 — IDLE (no device detected)
Section titled “Screen 0 — IDLE (no device detected)”┌──────────────────────────────────────────────────────────────────────────┐│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │├──────────────────────────────────────────────────────────────────────────┤│ ││ [ KINOSHITA WORDMARK / LOGO ] ││ ││ Connect a KN-86 in updater mode to get started. ││ ││ ┌─ HOW TO ENTER UPDATER MODE ─────────────────┐ ││ │ 1. Power off the deck. │ ││ │ 2. Hold SYS + LINK on the keypad. │ ││ │ 3. Connect USB-C cable while still held. │ ││ │ 4. Release after the amber bezel pulses. │ ││ └─────────────────────────────────────────────┘ ││ ││ ◆ WAITING FOR DEVICE ◆ (animated) ││ ││ [ Choose firmware file… ] [ Pull latest from kinoshita.dev ] ││ │├──────────────────────────────────────────────────────────────────────────┤│ STATUS: idle · no device · no firmware loaded [ ? Help ] │└──────────────────────────────────────────────────────────────────────────┘Notes: image source can be picked before the device is connected (so the user can preload the file while the device boots into updater mode); preload is reflected in the status bar.
Screen 1 — DEVICE_DETECTED (firmware not yet loaded)
Section titled “Screen 1 — DEVICE_DETECTED (firmware not yet loaded)”┌──────────────────────────────────────────────────────────────────────────┐│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │├──────────────────────────────────────────────────────────────────────────┤│ ││ ✓ KN-86 DETECTED ││ Serial: KN86-PROTO-0007 ││ Mount: /Volumes/KN86_UPDATE (macOS) ││ Slot: B (active is A — writing to inactive slot) ││ Battery: 74% ⚡ charging via USB ││ ││ FIRMWARE TO WRITE ││ ┌────────────────────────────────────────────────────────────────────┐ ││ │ No firmware selected. │ ││ │ │ ││ │ [ Choose .kn86fw or .kn86release file… ] │ ││ │ [ Pull latest stable from kinoshita.dev ] │ ││ │ [ Pull latest nightly (advanced) ] │ ││ └────────────────────────────────────────────────────────────────────┘ ││ │├──────────────────────────────────────────────────────────────────────────┤│ STATUS: device ready · firmware required [ ? Help ] │└──────────────────────────────────────────────────────────────────────────┘Screen 2 — CONFIRM (device + firmware both loaded; gate before write)
Section titled “Screen 2 — CONFIRM (device + firmware both loaded; gate before write)”┌──────────────────────────────────────────────────────────────────────────┐│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │├──────────────────────────────────────────────────────────────────────────┤│ ││ READY TO WRITE ││ ││ ┌─ DEVICE ─────────────────────┐ ┌─ FIRMWARE ────────────────────┐ ││ │ KN86-PROTO-0007 │ │ kn86-v0.2.1-stable.kn86fw │ ││ │ Slot B (inactive) │ │ Built: 2026-05-01 │ ││ │ Battery: 74% ⚡ │ │ nOSh: 0.2.1 │ ││ │ │ │ SHA-256: 6cca8e…d749 ✓ │ ││ └──────────────────────────────┘ │ Size: 187 MB │ ││ └───────────────────────────────┘ ││ ││ ⚠ This will overwrite slot B. Slot A (current) remains intact. ││ If the new firmware fails to boot, the deck reverts to slot A ││ automatically (tryboot rollback). ││ ││ ⚠ Integrity-checked, not signed. Verify firmware source. ││ ││ [ Cancel ] [ Begin flash → ] (primary) ││ │├──────────────────────────────────────────────────────────────────────────┤│ STATUS: ready · awaiting confirmation [ ? Help ] │└──────────────────────────────────────────────────────────────────────────┘Screen 3 — FLASHING (write in progress)
Section titled “Screen 3 — FLASHING (write in progress)”┌──────────────────────────────────────────────────────────────────────────┐│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │├──────────────────────────────────────────────────────────────────────────┤│ ││ WRITING FIRMWARE TO SLOT B ││ ││ kn86-v0.2.1-stable.kn86fw → /dev/disk5 (slot B: bootfs+rootfs) ││ ││ ┌──────────────────────────────────────────────────────────────────┐ ││ │ Writing bootfs (p3) ████████████████████░░░░░░░░░░░ 68% │ ││ │ Writing rootfs (p5) ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0% │ ││ │ fsync ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ ││ │ Verify ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ ││ └──────────────────────────────────────────────────────────────────┘ ││ ││ Throughput: 4.2 MB/s Elapsed: 00:14 ETA: 00:32 ││ ││ ⛔ DO NOT UNPLUG OR POWER OFF ││ │├──────────────────────────────────────────────────────────────────────────┤│ STATUS: flashing 68% · do not disconnect [ ? Help ] │└──────────────────────────────────────────────────────────────────────────┘Screen 4 — VERIFY (read-back integrity check)
Section titled “Screen 4 — VERIFY (read-back integrity check)”┌──────────────────────────────────────────────────────────────────────────┐│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │├──────────────────────────────────────────────────────────────────────────┤│ ││ VERIFYING SLOT B ││ ││ Recomputing SHA-256 across written payload… ││ ││ ┌──────────────────────────────────────────────────────────────────┐ ││ │ Verify ████████████████████░░░░░░░░░░░ 72% │ ││ └──────────────────────────────────────────────────────────────────┘ ││ ││ Expected: 6cca8ed3b70a3298a74910fdfbb700a8120ed07dac959962fec0c66c… ││ Computed: (in progress) ││ ││ ⛔ DO NOT UNPLUG OR POWER OFF ││ │├──────────────────────────────────────────────────────────────────────────┤│ STATUS: verifying 72% · do not disconnect [ ? Help ] │└──────────────────────────────────────────────────────────────────────────┘Screen 5 — COMPLETE (success)
Section titled “Screen 5 — COMPLETE (success)”┌──────────────────────────────────────────────────────────────────────────┐│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │├──────────────────────────────────────────────────────────────────────────┤│ ││ ✓ ││ ││ FLASH COMPLETE ││ ││ Slot B written and verified. Tryboot armed for next boot. ││ ││ ┌─ NEXT STEPS ──────────────────────────────────────────────────────┐ ││ │ 1. Disconnect the USB cable. │ ││ │ 2. Power-cycle the deck. │ ││ │ 3. The deck will boot from slot B. If it fails to reach the │ ││ │ desktop within 60 s, it auto-reverts to slot A on the next │ ││ │ power cycle. │ ││ └───────────────────────────────────────────────────────────────────┘ ││ ││ Wrote 187 MB in 00:42 · verified in 00:18 · total 01:00 ││ ││ [ Flash another device ] [ Quit ] ││ │├──────────────────────────────────────────────────────────────────────────┤│ STATUS: complete · safe to disconnect [ ? Help ] │└──────────────────────────────────────────────────────────────────────────┘Screen 6 — ERROR (any failure path)
Section titled “Screen 6 — ERROR (any failure path)”┌──────────────────────────────────────────────────────────────────────────┐│ KINOSHITA ◇ KN-86 DECKLINE FLASHER v0.1.0 │├──────────────────────────────────────────────────────────────────────────┤│ ││ ✗ ││ ││ FLASH FAILED ││ ││ ERROR: SHA256_MISMATCH ││ Verify pass found a checksum mismatch on slot B (p5/rootfs). ││ ││ What this means: ││ The written data does not match the source firmware. This is ││ usually caused by the cable being disconnected mid-write, a ││ failing SD card, or USB power instability. ││ ││ What to do: ││ • Slot A is unchanged — the deck still boots normally. ││ • Try a different USB-C cable. ││ • Try a different USB port (data-capable, not a hub). ││ • If the problem persists, the SD card may need replacement. ││ ││ [ Copy diagnostics ] [ Try again ] [ Quit ] ││ │├──────────────────────────────────────────────────────────────────────────┤│ STATUS: error · slot A unchanged · safe to disconnect [ ? Help ] │└──────────────────────────────────────────────────────────────────────────┘Each error variant follows this skeleton with a different ERROR token + body. Tokens map 1:1 to the helper-binary’s stderr emissions (see §Backend interface contract below): MAGIC_MISMATCH, TRUNCATED_HEADER, SHA256_MISMATCH, UNSUPPORTED_VERSION, DEVICE_DISAPPEARED, WRITE_IO_ERROR, ELEVATION_DENIED, WRONG_VOLUME_LABEL.
Tauri stack rationale (brief)
Section titled “Tauri stack rationale (brief)”This is restated only to record it in one place; the underlying decision is owned by ADR-0011 §“Framework recommendation with rationale” and docs/device/os/update-system.md L42–58. The short form:
| Option | Why it loses |
|---|---|
| Tauri 2.x | Winner. Rust backend owns disk I/O + elevated-command + native webview, ~10 MB bundle, shares a workspace crate with tools/kn86fw/. |
| Electron | ~100 MB bundle, requires Node IPC dance to call Rust, no advantage over Tauri for our use case. |
| Native Qt (rpi-imager fork) | Apache-2.0 base is permissive and proven, but locks us into Qt/QML and the generic “write any disk” framing — we want a narrow Kinoshita-branded experience. Useful as a reference for buffer sizes, sync semantics, and the drivelist/mountutils MIT-licensed standalone libs (which we can pull into Tauri). |
| Rust + egui | Native-rendered, single-binary, no webview tax. Real candidate — but the team’s frontend skillset leans web, and the visual design budget for a boutique product favors HTML/CSS over immediate-mode GUI. Reconsider only if Tauri’s webview adds friction we didn’t anticipate (e.g., macOS WKWebView quirks blocking USB device enumeration UI). |
Plain CLI (kn86-flasher-helper alone) | Already exists in the architecture. Insufficient for end-user S3 path — non-technical users can’t be expected to enumerate /dev/disk* and pick the right one. CLI is the foundation; the GUI is the polish layer on top. |
Backend interface contract — UI ↔ kn86-flasher-helper
Section titled “Backend interface contract — UI ↔ kn86-flasher-helper”ADR-0011 leaves “exactly how the UI invokes the helper” as an open implementation question. Resolving it here keeps the engineering brief tight when implementation gates open.
Decision: spawn-and-pipe, not linked crate.
The Tauri UI (running unprivileged) spawns kn86-flasher-helper as a subprocess via elevated-command::Command. They communicate over stdin/stdout/stderr with a simple line-oriented protocol (newline-delimited JSON for structured events; plain text for fatal-error tokens). No FFI, no shared library, no ABI lock-in.
Rationale:
- Process boundary = privilege boundary. The helper runs elevated (root on macOS/Linux, Administrator on Windows); the UI does not. A linked-crate model would pull privileged code into the UI’s address space, defeating the helper’s whole reason for existing.
- Crash isolation. A bug in the helper’s raw-write path can’t crash the UI (and vice versa). The UI can detect helper exit and surface a clean error.
- Trivially testable.
kn86-flasher-helperis testable in isolation via shell scripts; mock-helpers for UI integration tests are tiny. A linked-crate model would force in-process mocking and elevation simulation. - Aligns with
tools/kn86fw/. That crate is already a CLI with bothlib.rs(consumed in-process) andmain.rs(consumed via spawn). The flasher repeats the pattern: shared workspace cratekn86-fw-formatfor in-process header parsing, helper binary for the privileged write path.
Protocol sketch:
helper-stdin (UI → helper): command JSON, e.g.: {"op":"flash","fw_path":"/path/to/x.kn86fw","device":"/dev/disk5","slot":"B"} {"op":"verify","fw_path":"...","device":"/dev/disk5"} {"op":"abort"}
helper-stdout (helper → UI): newline-delimited JSON events: {"event":"phase","phase":"writing_bootfs","total":67108864} {"event":"progress","bytes_done":40000000,"bytes_total":67108864} {"event":"phase","phase":"verify"} {"event":"complete","slot":"B","sha256":"6cca8e..."}
helper-stderr (helper → UI): fatal error tokens, one per line: ERROR MAGIC_MISMATCH "header magic 'XXXXXXXX' does not match KN86FWv1" ERROR SHA256_MISMATCH "expected 6cca8e...d749 got deadbe...beef" ERROR ELEVATION_DENIED "user dismissed the elevation prompt"
helper exit code: 0 = success, non-zero = error (token already on stderr)The UI keeps the helper alive for the duration of a single flash transaction; the helper exits cleanly on op:abort or after a complete event. Long-lived helper would buy nothing and complicate elevation.
SD device enumeration + privilege escalation per OS
Section titled “SD device enumeration + privilege escalation per OS”The flasher needs to (a) find candidate USB block devices, (b) filter to ones that look like a KN-86 in updater mode, and (c) elevate when it’s time to write. Each OS has a distinct path.
| OS | Enumeration | Filter | Elevation |
|---|---|---|---|
| Linux | udev via the udev Rust crate (or block-utils + /sys/block walk as a fallback). Subscribe to add/remove events for live device-list updates. | Match USB VID/PID pid.codes-allocated or Pi-Foundation range (open question #2 below) AND mount label KN86_UPDATE. | pkexec (polkit) wrapping the helper. Ship a polkit policy file /usr/share/polkit-1/actions/dev.kinoshita.flasher.policy in the AppImage. The elevated-command crate handles the wrapping; we own the .policy authoring. |
| macOS | IOKit device-arrival notifications via the core-foundation + io-kit-sys crates, OR diskutil list -plist polled at ~1 Hz as a simpler fallback for v1. | Same VID/PID + volume label match. macOS auto-mounts MSC volumes under /Volumes/KN86_UPDATE. | Authorization Services via Apple’s SMJobBless (deprecated but still works) OR a one-shot osascript -e 'do shell script ... with administrator privileges' for v1 simplicity. The elevated-command crate currently uses osascript on macOS; that’s acceptable for v1. |
| Windows | SetupDiGetClassDevs from the Win32 API for USB enumeration, OR poll wmic logicaldisk (or the modern PowerShell Get-Volume) for v1 simplicity. | Same VID/PID + drive volume label match. Windows assigns a drive letter (e.g., E:); we read \\.\PhysicalDriveN for raw write, not the letter. | UAC via runas verb on ShellExecute. The elevated-command crate handles this; the manifest must declare requireAdministrator for the helper, not the UI. |
v1 enumeration scope: poll-based across all three OSes (1 Hz diskutil / lsblk / wmic poll) is sufficient for the “user explicitly enters updater mode and waits a few seconds” UX. Event-based hot-plug detection (udev / IOKit / SetupDi) is a v2 polish — moves “Waiting for device…” → “Detected” latency from ~1 s to instant. Not worth the platform-specific crate burden in v1.
Drive-name display: show the OS-native path (/dev/disk5 on macOS, /dev/sdc on Linux, \\.\PhysicalDrive3 on Windows) in the UI alongside the friendlier “Slot B (inactive)” label. Power users want the device path; non-technical users ignore it.
Image source UX
Section titled “Image source UX”Two image-source modes, both available from Screen 0 / Screen 1:
- Local file picker (default for S2 dev iteration). OS-native open dialog filtered to
.kn86fwand.kn86releaseextensions. Recently-used files surfaced as a “recents” list in a dropdown. - GitHub releases pull (default for S3 end-user). UI hits
https://api.github.com/repos/jschairb/kn86-deckline/releases/latest(or whatever the canonical release surface ends up being — open question #3), shows the most recent stable + most recent nightly, downloads the chosen.kn86releaseto~/.cache/kn86-flasher/(XDG basedir /Library/Caches//%LOCALAPPDATA%), verifies its SHA-256 against the GitHub-attached signature file (or just the asset’s own SHA-256 if we don’t sign in v1), then proceeds as if the user had picked it locally.
Both modes converge on the same Screen 2 (CONFIRM) flow — the difference is opaque to everything downstream of file selection. The “Pull from kinoshita.dev” button on Screen 0 is a third entry point that’s just a redirect to the GitHub release pull (with a friendlier label); we don’t need a separate kinoshita.dev artifact host in v1.
Caching: downloaded .kn86release files stay in the cache dir keyed by SHA-256 so re-flashing the same version is instant the second time.
Verification UX
Section titled “Verification UX”ADR-0011 §“Update flow” L114 already specifies: after write, the helper recomputes SHA-256 of the written payload and compares to the header’s stored digest. This pack scopes the UX:
v1 in-scope:
- Post-write SHA-256 verification pass (Screen 4), surfaced as a separate progress phase in the UI. Failure → ERROR screen with token
SHA256_MISMATCH. - Header validation before write (magic, version, reserved-region zeros, file truncation) using the
kn86-fw-formatworkspace crate. Failures shown on Screen 2 (CONFIRM) before the user can click “Begin flash.”
v1 explicitly deferred (separate task):
- Boot-once verification. ADR-0011 mentions a 60-second boot-success sentinel that nOSh writes after a healthy boot; the
tryboot_a_bflag is then cleared by either nOSh itself or by the flasher on next reconnect (ADR-0011 open question #2 L386). Wiring the flasher to cleartryboot_a_bon next reconnect is its own task — it requires a stable side-channel (probablyg_serial) for the device to report “I committed” to the flasher. Defer to a sibling GWP-176-B (file later), or roll into the nOSh-owned commit path entirely.
The v1 verification message (“Slot B written and verified. Tryboot armed for next boot.”) is honest about this boundary — we verify the write, the device verifies the boot, and the user’s next-power-cycle determines commit. Don’t promise more.
Cross-platform packaging
Section titled “Cross-platform packaging”Tauri 2.x produces native installers per platform via tauri-bundle. The packaging matrix:
| OS | Format | Tooling | Crosslink |
|---|---|---|---|
| Linux | AppImage (primary), .deb + .rpm (secondary, lower-priority) | tauri-bundle + appimage-builder for the AppImage; bundles polkit policy + helper binary | Existing release CI (see docs/device/os/release-setup.md) extends to a parallel flasher-release job — distinct from the .kn86fw system-image release pipeline. |
| macOS | .dmg containing notarized .app bundle | tauri-bundle + Apple notarization in CI. Universal binary (Intel + Apple Silicon). | Coordinate with GWP-125 — that task is the macOS DMG packaging path for the kn86-emulator. The flasher reuses the same notarization cert + CI patterns; they’re independent artifacts but share infrastructure. |
| Windows | .msi (primary), .exe (NSIS, secondary) | tauri-bundle + WiX for MSI. Code-signed with an EV cert (long-term — for v1 prototype, a self-signed dev cert is fine; ship the SmartScreen warning until we get a proper cert). | New CI job; nothing to crosslink yet. |
Code signing strategy is a longer conversation tied to the broader release infrastructure — for v1 prototype distribution to a small alpha cohort, unsigned (with documented “right-click → Open” workaround on macOS, “More info → Run anyway” on Windows) is acceptable. Signing graduates to gating when we widen distribution. Cross-link this concern to ADR-0011 open question §“Signing and rollback keys” and the eventual release-infra ADR.
Helper bundling: kn86-flasher-helper is co-bundled with the UI in every package. Both binaries live in the same install dir; the UI invokes the helper by relative path. Updating one updates the other (no version-skew risk).
Scaffold proposal — tools/kn86flash-gui/ (or tools/flasher/)
Section titled “Scaffold proposal — tools/kn86flash-gui/ (or tools/flasher/)”Directory layout when implementation gate opens. No files created in this PR — this is the proposal.
tools/kn86flash-gui/ # see open question #1 re: name├── README.md # what it is, build, test, package├── Cargo.toml # workspace member; depends on kn86-fw-format├── src-tauri/│ ├── Cargo.toml│ ├── tauri.conf.json # window config, allowlist, bundle metadata│ ├── build.rs│ ├── icons/│ │ ├── icon.png # 1024×1024 source│ │ ├── icon.icns # macOS│ │ ├── icon.ico # Windows│ │ └── 512x512.png, 256x256.png # Linux│ └── src/│ ├── main.rs # Tauri app entry, command registration│ ├── commands.rs # invoke handlers exposed to webview│ ├── device.rs # SD device enumeration (cfg-gated per OS)│ ├── helper.rs # spawn + protocol handling for kn86-flasher-helper│ ├── source.rs # local file / GitHub release fetch│ └── error.rs # error token taxonomy├── src/ # webview frontend (Vite + vanilla TS or Svelte — open Q #4)│ ├── index.html│ ├── main.ts # state machine + Tauri invoke calls│ ├── styles.css # Kinoshita amber/black palette│ └── screens/│ ├── idle.ts│ ├── device_detected.ts│ ├── confirm.ts│ ├── flashing.ts│ ├── verify.ts│ ├── complete.ts│ └── error.ts├── package.json # frontend tooling deps├── vite.config.ts└── tsconfig.json
tools/kn86-flasher-helper/ # new sibling crate├── README.md├── Cargo.toml # workspace member; depends on kn86-fw-format└── src/ ├── main.rs # CLI entry, JSON protocol parser ├── write.rs # raw block-device write loop with O_DIRECT ├── verify.rs # post-write SHA-256 pass └── allowlist.rs # hard-coded allowed device patterns
tools/kn86-fw-format/ # extracted from tools/kn86fw/src/header.rs├── Cargo.toml # new workspace member, leaf dep of kn86fw + flasher└── src/ └── lib.rs # Header parsing + SHA-256 verify, no IO
tools/Cargo.toml # workspace; add kn86flash-gui, kn86-flasher-helper, kn86-fw-formatWorkspace impact: the existing tools/Cargo.toml workspace gains three members. tools/kn86fw/ gets refactored to depend on kn86-fw-format instead of inlining the header (a small follow-on PR — out of scope for the design pack but flagged as a precondition).
Acceptance criteria expanded (≥4 testable items with file paths)
Section titled “Acceptance criteria expanded (≥4 testable items with file paths)”These criteria are for the design pack itself (this PR). Implementation acceptance criteria will be authored on the implementation task(s) when the gate opens.
docs/plans/sprints/2026-05-01-gwp-176-tauri-flasher-design.mdexists and matches the format of the other Sprint 4 design packs (story narrative, user stories, acceptance criteria, edge cases, engineering hand-off, open questions). Current PR delivers this.- Wireframes cover all six screens (IDLE, DEVICE_DETECTED, CONFIRM, FLASHING, VERIFY, COMPLETE, plus ERROR variant). Each is a single ASCII viewport. Verified against §UI flow above.
- Backend protocol contract is concrete enough for an engineer to implement against — JSON event shapes, error token list, exit-code semantics. Verified against §Backend interface contract above.
- Per-OS enumeration + elevation paths are named with specific crates / system commands per OS, not abstract “use the OS’s native API.” Verified against §SD device enumeration table.
- Stale-ticket disambiguation is recorded — design pack notes that
kn86flash/GWP-111 is retired and the actual CLI iskn86-flasher-helper. Verified against §Story narrative. - Scaffold proposal lists every directory + file the eventual implementation PR will land. No files created in this PR; only the proposal exists. Verified against §Scaffold proposal.
- Cross-references to ADR-0011 and ADR-0020 are explicit with section/line numbers where load-bearing. Verified throughout.
- Open questions list is decidable — each item has a default answer the implementer would use absent Josh’s call. Verified against §Open questions.
Edge cases (≥2)
Section titled “Edge cases (≥2)”- User picks a
.kn86fwfor a different KN-86 hardware revision. The header carriesmin_bootloader_version(pertools/kn86fw/format/kn86fw.h); the helper rejects withUNSUPPORTED_VERSION. The UI shows a clean error explaining “this firmware needs bootloader v3, your device has v2 — update the bootloader first or pick an older firmware.” Bootloader version comes from a side-channel (g_serialquery) or — for v1 simplicity — is just not enforced and the device’s own boot-time check fails into tryboot rollback. v1 default: rely on tryboot rollback; surface min-bootloader-version mismatch as a CONFIRM-screen warning, not a block. - User unplugs the cable mid-write. Helper detects via failed
write()→ exits non-zero withDEVICE_DISAPPEARED. UI surfaces ERROR screen with “slot B is now in an indeterminate state; slot A is unaffected; reconnect and re-flash.” This is the most common failure mode in S2 (dev iteration) and the UX needs to make it un-scary, not catastrophic — the A/B model means partial writes never brick. - MSC volume mounts but doesn’t have the
KN86_UPDATElabel (e.g., user has a generic USB drive plugged in with a coincidental VID/PID match — extremely unlikely with apid.codes-allocated PID, but defensive coding). Helper refuses to write withWRONG_VOLUME_LABEL. UI explains “this device looks like a KN-86 but the volume label doesn’t match — refusing to write to avoid corrupting your other USB drive.” - Two KN-86 devices connected simultaneously (Josh has multiple prototypes on the bench during S2). UI lists both, user picks one. Helper writes to the picked one only; the other is unaffected. Multi-device flash (write same firmware to both serially) is a v2 nice-to-have, not v1.
- Helper binary is missing from the install dir (corrupted install, or AV quarantined it on Windows). UI detects via spawn failure → “kn86-flasher-helper not found at expected path; reinstall the flasher.” Don’t try to be clever about helper relocation in v1.
- GitHub releases API rate-limited / network down. UI falls back to the local file picker with a “couldn’t reach kinoshita.dev — pick a file you’ve already downloaded?” prompt. Don’t hard-fail the IDLE screen on network errors.
- User cancels the elevation prompt (UAC dismissed, polkit denied, osascript admin sheet cancelled). Helper exits with
ELEVATION_DENIED; UI returns to CONFIRM screen with a polite “elevation required to write — try again?” inline message. Don’t crash, don’t moon-landing-explain.
Engineering hand-off notes (when gate opens)
Section titled “Engineering hand-off notes (when gate opens)”Owner when implementation lands: ideally a single Rust + frontend engineer working across both tools/kn86flash-gui/src-tauri/ and tools/kn86flash-gui/src/. Estimate from ADR-0011 L429–433: ~4–5 days for the Tauri UI + Rust backend + ~2–3 days for the helper = ~7 days total, dispatched as one feature with two PRs:
- PR-A:
kn86-fw-formatworkspace crate extraction (precondition). Refactortools/kn86fw/src/header.rsto live intools/kn86-fw-format/and re-export. ~1 day. No behavior change. - PR-B:
tools/kn86flash-gui/+tools/kn86-flasher-helper/(the work). Tauri scaffold + helper binary + cross-platform CI integration. ~6–7 days.
Files NOT touched in either PR:
kn86-emulator/— flasher is desktop tooling, not emulator code.tools/sd-provision/— that’s the upstream image producer; flasher is the downstream image writer. They share the.kn86fwformat and nothing else./home/shared/partition (p6) — flasher policy is “never open p6, never mount p6, never reference any device node other than the inactive slot’s bootfs + rootfs.” Hard-coded inkn86-flasher-helper/src/allowlist.rs. ADR-0011 L331.
Dispatch shape: single Rust engineer, one feature branch, two PRs (extraction first, then app). Frontend skill required (TypeScript or Svelte, Tauri webview integration). CI work to add MSI/DMG/AppImage build matrix is bounded — Tauri’s bundler does most of it, but the macOS notarization step will need a Josh-supplied Apple Developer cert.
Watch for: scope creep into Phase 3 features (auto-update server check, signing, OTA WiFi path) that ADR-0011 explicitly defers. Helper binary’s allow-list is the load-bearing safety mechanism; reviewer should confirm it can’t be bypassed by any UI invocation.
Open questions for Josh
Section titled “Open questions for Josh”- Crate name:
tools/kn86flash-gui/(per the user brief on this task) ortools/flasher/(per ADR-0011 L210, L448)? Recommendation:tools/flasher/— matches the ADR which is the durable spec, and the paralleltools/kn86fw/(image builder) +tools/flasher/(image writer) is a clean naming pair. The brief’skn86flash-guiwas likely written before ADR-0011’s crate-name decision was inked. Default if not answered:tools/flasher/. - VID/PID allocation. ADR-0011 L322 names this as an open question — request a free PID from
pid.codes(community-managed range, free for hobby use), use the Pi Foundation’s allocated range (requires their permission), or pick something that won’t collide with existing USB devices. Recommendation:pid.codes— fast, free, well-understood. Default if not answered: file apid.codesrequest as a Phase 2 prereq task. - Update server URL for “Pull from kinoshita.dev.” Recommendation: GitHub Releases API on
jschairb/kn86-decklinefor v1 (no separate hosting infrastructure needed), with the URL displayed in the UI askinoshita.dev/firmware(purely cosmetic — actual fetch hits GitHub). Default if not answered: GitHub Releases on this repo. - Frontend framework inside Tauri. Vanilla TypeScript + a tiny state machine library, vs. Svelte (lightweight, reactive, compiles to vanilla), vs. React (heavy for this use case). Recommendation: Svelte 4 — minimal runtime, reactive, the seven screens are static enough that React’s component-tree complexity is unjustified. Default if not answered: vanilla TypeScript (lower learning curve, no framework lock-in, easier review for the C-engineer-heavy team).
- Recovery mode (S4) — in v1 or deferred? Recommendation: defer to v2 (file as GWP-176-C). v1 covers S1–S3, which is 99% of expected usage; recovery is rare and well-served by pulling the SD and re-imaging via
tools/sd-provision/. Default if not answered: defer. .kn86releasecomposite (per ADR-0020) — does flasher v1 consume.kn86fwonly, or.kn86releasefrom day one? Recommendation: support both in v1 —.kn86releaseis just a tarball wrapping a.kn86fw, the extraction is trivial. Designing for.kn86releasefrom day one avoids a v1.1 rework. Default if not answered: support both.- Boot-once commit (clearing
tryboot_a_bfrom the flasher) — in v1 or deferred? Recommendation: defer to a sibling task (GWP-176-B). v1 ships verified-write; commit is its own concern that needs theg_serialside channel design (ADR-0011 open question #2). Default if not answered: defer. - Implementation gate. Per parent GWP-163, no sub-task goes In Progress while GWP-152 is open. This pack ships under that gate (design only). Confirm: the implementation tasks above (PR-A crate extraction, PR-B app scaffold) wait for GWP-152 close before being filed / dispatched, OR file them now in Planning so they’re queued for the v0.1-ship-day sprint kickoff. Recommendation: file in Planning now, dispatch when gate opens.