From 3696dd6b346b6ae78be79092f9dfd8b65e218f1d Mon Sep 17 00:00:00 2001 From: mineracks <134782215+mineracks@users.noreply.github.com> Date: Thu, 28 May 2026 18:25:03 +1000 Subject: [PATCH] =?UTF-8?q?Initial=20skeleton=20=E2=80=94=20Phase=201=20sc?= =?UTF-8?q?affolding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A browser-based companion for SeedHammer v1 hardware. Three coordinated PWAs (composer, emulator, combined-sim) plus an optional Android wrapper, inspired by Gangleri42's SeedHammer II fork but retargeted to v1's Pi-Zero / WaveShare / MarkingWay hardware. What's in this commit: - LICENSE: Unlicense (matching upstream) - README.md: project overview + status + roadmap - CREDITS.md: upstream provenance + pinned baseline SHAs - docs/architecture/: five authoritative design docs (lifted from the prep work in mineracks-infrastructure): * BASELINES.md — pinned SHAs, license audit, path-mapping table * v1-engrave-spec.md — MarkingWay USB-serial wire protocol audit * v1-buttons-and-ui.md — GPIO map, UI screen flow, keyboard map * sh1e-spec.md — composer-to-Pi envelope format spec * seedsigner-reuse.md — Pyodide strategy + jumbo (SeedSigner+) support - Go package skeleton with doc.go contracts: backup, bezier, bspline, engrave, engrave/wire, engrave/wire/sh1e, font (+ comfortaa, poppins, constant), gui, input, internal/golden, platform/v1 - cmd/ entrypoints with stub main(): composer, emulator, combined-sim, seedsigner-sim - web/ static-shell skeleton - go.mod (module github.com/mineracks/seedhammer-v1-companion, Go 1.22) - go build ./... + go vet ./... both clean Next: lift universal packages (backup, font, bezier, bspline) from upstream seedhammer/seedhammer at v1.3.0 verbatim. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 41 +++ CREDITS.md | 63 +++++ LICENSE | 24 ++ README.md | 90 +++++++ backup/doc.go | 19 ++ bezier/doc.go | 10 + bspline/doc.go | 7 + cmd/combined-sim/doc.go | 19 ++ cmd/combined-sim/main.go | 11 + cmd/composer/doc.go | 24 ++ cmd/composer/main.go | 11 + cmd/emulator/doc.go | 24 ++ cmd/emulator/main.go | 11 + cmd/seedsigner-sim/doc.go | 16 ++ cmd/seedsigner-sim/main.go | 11 + docs/README.md | 19 ++ docs/architecture/BASELINES.md | 181 +++++++++++++ docs/architecture/seedsigner-reuse.md | 202 ++++++++++++++ docs/architecture/sh1e-spec.md | 355 +++++++++++++++++++++++++ docs/architecture/v1-buttons-and-ui.md | 296 +++++++++++++++++++++ docs/architecture/v1-engrave-spec.md | 323 ++++++++++++++++++++++ engrave/doc.go | 20 ++ engrave/wire/doc.go | 19 ++ engrave/wire/sh1e/doc.go | 27 ++ font/comfortaa/doc.go | 6 + font/constant/doc.go | 5 + font/doc.go | 21 ++ font/poppins/doc.go | 5 + go.mod | 3 + gui/doc.go | 22 ++ input/doc.go | 25 ++ internal/golden/doc.go | 11 + platform/v1/doc.go | 21 ++ web/README.md | 19 ++ 34 files changed, 1961 insertions(+) create mode 100644 .gitignore create mode 100644 CREDITS.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 backup/doc.go create mode 100644 bezier/doc.go create mode 100644 bspline/doc.go create mode 100644 cmd/combined-sim/doc.go create mode 100644 cmd/combined-sim/main.go create mode 100644 cmd/composer/doc.go create mode 100644 cmd/composer/main.go create mode 100644 cmd/emulator/doc.go create mode 100644 cmd/emulator/main.go create mode 100644 cmd/seedsigner-sim/doc.go create mode 100644 cmd/seedsigner-sim/main.go create mode 100644 docs/README.md create mode 100644 docs/architecture/BASELINES.md create mode 100644 docs/architecture/seedsigner-reuse.md create mode 100644 docs/architecture/sh1e-spec.md create mode 100644 docs/architecture/v1-buttons-and-ui.md create mode 100644 docs/architecture/v1-engrave-spec.md create mode 100644 engrave/doc.go create mode 100644 engrave/wire/doc.go create mode 100644 engrave/wire/sh1e/doc.go create mode 100644 font/comfortaa/doc.go create mode 100644 font/constant/doc.go create mode 100644 font/doc.go create mode 100644 font/poppins/doc.go create mode 100644 go.mod create mode 100644 gui/doc.go create mode 100644 input/doc.go create mode 100644 internal/golden/doc.go create mode 100644 platform/v1/doc.go create mode 100644 web/README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa6e33e --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Go build artifacts +/bin/ +*.exe +*.dll +*.so +*.dylib + +# WASM build artifacts +*.wasm +*.wasm.map +wasm_exec.js + +# Node / web build +node_modules/ +.pnpm-store/ +dist/ +.parcel-cache/ + +# Pyodide bundle (large; downloaded at build time) +web/seedsigner-sim/pyodide/dist/ + +# Editor / OS +.DS_Store +.vscode/ +.idea/ +*.swp +*~ + +# Test artifacts +coverage.out +coverage.html +*.test +seedsigner-screenshots/ + +# Local dev +.env +.env.local + +# go workspace overrides +go.work +go.work.sum diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000..d961a71 --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,63 @@ +# Credits + +This project stands on the shoulders of three communities. None of these +upstreams legally require attribution (the SeedHammer projects are released +under the Unlicense; SeedSigner is MIT). Attribution is kept here as +courtesy and to make code provenance traceable. + +## Upstream SeedHammer (Pi-Zero / v1 firmware) + +- **Repo:** https://github.com/seedhammer/seedhammer +- **Baseline:** v1.3.0 (commit `2f071c1d8f23eb7fd39b15fc0acb8874113f801e`) +- **License:** Unlicense (public domain dedication) +- **Lifted from this codebase:** plate-area constants (`backup/`), engrave-stroke + geometry (`engrave/`), MarkingWay USB-serial driver (`driver/mjolnir/`), + WaveShare HAT button mapping (`driver/wshat/`), pre-rasterised OpenType + fonts (`font/comfortaa`, `font/poppins`, `font/constant`), GUI screen flows + (`gui/`), camera + LCD drivers (`driver/libcamera/`, `driver/drm/`), + curve math (`bezier/`, `bspline/`), image helpers (`image/`). + +## Gangleri42's SeedHammer fork (SH-II features that inspired this port) + +- **Repo:** https://github.com/Gangleri42/seedhammer +- **Baseline:** branch `seedhammer-features` (commit `0a3c63efb125d17d8ec86ce739ecd058c8747cfe`) +- **License:** Unlicense (public domain dedication) +- **Lifted from this codebase:** composer PWA shell (`cmd/webnfc/`), firmware-in-browser + emulator pattern (`cmd/wasmemu/`), combined composer+emulator harness + (`cmd/webnfc-sim/`), Android wrapper structure (`cmd/seedhammer-android/`). + The engrave-payload encoder (`engrave/wire/`) is NOT lifted as-is — it is + SH-II-specific (the SH2E NFC envelope); v1 has a completely different live + USB-serial protocol. We replace it with a v1-shaped SH1E envelope (see + `docs/architecture/sh1e-spec.md`). + +## SeedSigner (companion emulator) + +- **Repo:** https://github.com/SeedSigner/seedsigner +- **Baseline:** TBD — bundled via Pyodide at a pinned commit; see + `web/seedsigner-sim/UPSTREAM.md` once that is wired up. +- **License:** MIT +- **Used in this project:** runtime UI assets (`src/seedsigner/resources/`), + custom icon font (`seedsigner-icons.otf`), screen-flow definitions + (`src/seedsigner/views/`, `src/seedsigner/gui/screens/`), screenshot + generator (`tests/screenshot_generator/`) used as a CI pixel-diff oracle, + 3D-printable enclosure CAD source (`enclosures/`) for the device chassis + render. +- **License compliance:** SeedSigner-derived files segregated under + `web/seedsigner-sim/upstream/` and `cmd/seedsigner-sim/upstream/` with + the MIT notice preserved in `LICENSE.seedsigner`. + +## Other dependencies + +- **`github.com/fxamacker/cbor/v2`** — MIT — used for SH1E payload + encoding (canonical CBOR). +- **`github.com/tarm/serial`** — MIT — USB-serial driver, brought in via + upstream SeedHammer's `driver/mjolnir/`. +- **`periph.io/x/conn/v3`** + **`periph.io/x/host/v3`** — Apache 2.0 — GPIO + library, brought in via upstream SeedHammer's `driver/wshat/`. +- **Pyodide** — MPL-2.0 — Python-in-WASM runtime used to host the + SeedSigner emulator in-browser. See `web/seedsigner-sim/pyodide/`. + +## Contributing + +If you contribute and want your name listed here, just say so in your PR. +We will not list you without consent. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0818a8 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# seedhammer-v1-companion + +**Status:** early development. Phase 1 (composer port) in progress. See +[docs/architecture/](docs/architecture/) for the project plan and design +docs. + +A browser-based companion for **SeedHammer v1** hardware (the original +Raspberry-Pi-Zero-based engraver), inspired by Gangleri42's SeedHammer II +fork. Ships three coordinated tools and one optional desktop wrapper. + +## What this is + +### 1. Plate composer (browser PWA) + +Design a stainless steel seed-backup plate from your phone or desktop — +seed words, custom title text, optional logos — then transfer the design +to a real SeedHammer v1 controller via QR code. + +The composer renders a pixel-faithful preview using the *same Go code the +Pi controller runs*. What you see in the browser is what the engraver +will physically punch. + +The on-the-wire envelope is **SH1E** (a CBOR + CRC32 format documented at +[docs/architecture/sh1e-spec.md](docs/architecture/sh1e-spec.md)). Sized +to fit a 24-word multisig plate in a single QR frame, with BBQr fallback +for larger multi-plate manifests. + +### 2. SeedHammer v1 emulator (browser PWA) + +Run the real v1 controller firmware in your browser. The same `gui/`, +`input/`, and `engrave/` Go packages that drive the physical device, +compiled to WASM. Use it to: + +- Test workflows end-to-end without a physical device +- Take screenshots / record screencasts of v1 flows +- Demo SeedHammer v1 to people without shipping hardware + +Keyboard mapping: arrows = joystick, Enter = center/confirm, 1/2/3 = Button1/2/3. + +### 3. Bundled SeedSigner emulator (browser PWA) + +A faithful in-browser SeedSigner — both the classic 1.3" 240×240 model and +the newer 2.8" SeedSigner+ "jumbo" model. Generates seed-phrase QR codes +that you can hand off to the SeedHammer v1 emulator via a single button +press, end-to-end without leaving the page. + +Built by hosting the upstream SeedSigner Python code via Pyodide so that +*the emulator IS the firmware* — when SeedSigner releases new versions +we bump the pinned commit and the sim updates. + +### 4. Optional Android wrapper + +Kotlin/Gradle shell hosting the composer WASM, for users who want a +plate-design app instead of a PWA. Mirrors the structure of Gangleri42's +SH2 Android companion. + +## Hardware targeted + +This codebase targets the **original SeedHammer v1** specifically — the +[Pi Zero v1.3 / WaveShare 1.3" 240×240 LCD HAT / MarkingWay engraver] +hardware. **Not** the newer SeedHammer II (RP2040 / TinyGo / SH2E NFC). + +For an SH-II companion, use [Gangleri42's fork](https://github.com/Gangleri42/seedhammer) +directly. Most of the inspiration for this project comes from there. + +## Status & roadmap + +- ☐ **Phase 1** — composer port (Go-to-WASM, SH1E reference encoder, web UI) +- ☐ Phase 2 — v1 emulator (firmware-in-browser, Gangleri42-faithful UI shell) +- ☐ Phase 2.5 — SeedSigner emulator + QR handoff +- ☐ Phase 3 — combined three-pane sim +- ☐ Phase 4 — real-device validation on real v1 hardware +- ☐ Phase 5 (optional) — ColdCard emulator (port from Gangleri42's fork) +- ☐ Phase 6 (optional) — Android wrapper + +## Building + +(Will be documented as Phase 1 lands — `go build ./...` + a Vite build for +the web shells.) + +## License + +Released under the [Unlicense](LICENSE) (public domain dedication), matching +upstream SeedHammer's choice. SeedSigner-derived files segregated and +retain their MIT notice. + +## Credits + provenance + +Heavy lifting by three upstreams. See [CREDITS.md](CREDITS.md) for what +came from where and the pinned baseline commits. diff --git a/backup/doc.go b/backup/doc.go new file mode 100644 index 0000000..0d0d861 --- /dev/null +++ b/backup/doc.go @@ -0,0 +1,19 @@ +// Package backup defines SeedHammer v1 plate dimensions and layout +// constants. +// +// Three plate types are supported, matching upstream's stainless plate SKUs: +// +// SmallPlate 85 × 55 mm — single seed (12 or 24 words) +// SquarePlate 85 × 85 mm — single seed + title +// LargePlate 85 × 134 mm — seed + descriptor for multisig +// +// The engraver's origin sits 97mm in the X axis from the plate edge (a +// fixed offset of the physical machine). outerMargin = 3 mm and +// innerMargin = 10 mm define the engrave-safe area. +// +// All constants are mm; the engrave/ package converts to machine steps +// (1 step ≈ 0.00796 mm) at command-generation time. +// +// Status: STUB — to be lifted verbatim from upstream's backup/backup.go +// at v1.3.0. The constants are stable across all v1.x releases. +package backup diff --git a/bezier/doc.go b/bezier/doc.go new file mode 100644 index 0000000..51eabdf --- /dev/null +++ b/bezier/doc.go @@ -0,0 +1,10 @@ +// Package bezier provides quadratic and cubic Bezier curve tessellation +// for the engrave pipeline. +// +// Used to convert OpenType glyph QuadTo/CubeTo segments and SH1E SVG path +// Q/C commands into linear MoveTo/LineTo sequences at the engraver's +// resolution. +// +// Status: STUB — to be lifted from upstream at v1.3.0 + cross-checked +// against Gangleri42's fork (the math is hardware-agnostic and unchanged). +package bezier diff --git a/bspline/doc.go b/bspline/doc.go new file mode 100644 index 0000000..9d3f488 --- /dev/null +++ b/bspline/doc.go @@ -0,0 +1,7 @@ +// Package bspline provides B-spline tessellation for the engrave pipeline. +// +// Used by the OpenType font rasteriser for the few glyphs that use +// B-spline rather than Bezier segments. +// +// Status: STUB — to be lifted from upstream at v1.3.0. +package bspline diff --git a/cmd/combined-sim/doc.go b/cmd/combined-sim/doc.go new file mode 100644 index 0000000..783fcdb --- /dev/null +++ b/cmd/combined-sim/doc.go @@ -0,0 +1,19 @@ +// Command combined-sim wires composer + emulator + SeedSigner-sim into a +// single browser page with a QR-handoff bus. +// +// The handoff bus has two modes: +// +// - Display mode (faithful): a sim renders a QR on its canvas; the +// SeedHammer emulator's mock camera reads pixels from that canvas +// and runs them through the upstream QR decoder. Same code path as +// a real device. +// +// - Direct mode (fast): a shared JS bus copies the payload between sims +// directly, skipping QR encode/decode. Useful for debugging. +// +// Status: STUB — depends on cmd/composer, cmd/emulator, cmd/seedsigner-sim +// landing first. +// +// Modelled on Gangleri42's cmd/webnfc-sim: +// https://github.com/Gangleri42/seedhammer/tree/seedhammer-features/cmd/webnfc-sim +package main diff --git a/cmd/combined-sim/main.go b/cmd/combined-sim/main.go new file mode 100644 index 0000000..a9b4d96 --- /dev/null +++ b/cmd/combined-sim/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Fprintln(os.Stderr, "combined-sim: not yet implemented — depends on composer + emulator + seedsigner-sim landing first") + os.Exit(2) +} diff --git a/cmd/composer/doc.go b/cmd/composer/doc.go new file mode 100644 index 0000000..29bf22b --- /dev/null +++ b/cmd/composer/doc.go @@ -0,0 +1,24 @@ +// Command composer is the browser-side plate composer for SeedHammer v1. +// +// Compiles to WebAssembly via GOOS=js GOARCH=wasm. The HTML/CSS shell that +// hosts this WASM lives under web/composer/. +// +// Responsibilities: +// - Render a real-time preview of the plate using the same engrave + font +// code the Pi controller runs (code-identity guarantees preview = reality). +// - Emit an SH1E envelope (see docs/architecture/sh1e-spec.md) that the +// Pi controller can scan via QR code. +// +// Exports four JS functions, modelled on Gangleri42's cmd/webnfc: +// +// seedhammerEncodeText(plateType uint8, blocks []TextBlock) []byte // SH1E +// seedhammerPreviewText(plateType uint8, blocks []TextBlock) string // SVG preview +// seedhammerEncodeSVG(plateType uint8, paths []SvgPath) []byte // SH1E +// seedhammerPreviewSVG(plateType uint8, paths []SvgPath) string // SVG preview +// +// Status: STUB — implementation lifted progressively from +// https://github.com/Gangleri42/seedhammer/tree/seedhammer-features/cmd/webnfc +// at pinned baseline 0a3c63efb125d17d8ec86ce739ecd058c8747cfe, with the +// SH-II geometry block replaced by v1 constants from upstream +// seedhammer/seedhammer v1.3.0 (backup/backup.go). +package main diff --git a/cmd/composer/main.go b/cmd/composer/main.go new file mode 100644 index 0000000..10b0240 --- /dev/null +++ b/cmd/composer/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Fprintln(os.Stderr, "composer: not yet implemented — see docs/architecture/sh1e-spec.md and cmd/composer/doc.go") + os.Exit(2) +} diff --git a/cmd/emulator/doc.go b/cmd/emulator/doc.go new file mode 100644 index 0000000..f14c564 --- /dev/null +++ b/cmd/emulator/doc.go @@ -0,0 +1,24 @@ +// Command emulator runs the SeedHammer v1 controller firmware in a browser. +// +// Compiles to WebAssembly via GOOS=js GOARCH=wasm. The HTML/CSS shell that +// hosts this WASM lives under web/emulator/. +// +// The WASM hosts the real v1 gui/, input/, and engrave/ packages, with +// hardware-side drivers (driver/wshat for buttons, driver/drm for the LCD, +// driver/libcamera for the camera, driver/mjolnir for the engraver) +// replaced by browser-side mocks: +// +// - Buttons → keyboard events (arrows / Enter / 1, 2, 3) +// - LCD → HTML5 canvas +// - Camera → mock that reads QRs from sibling panes on the same page +// - Engraver → null sink (preview mode) or visual playback animation +// +// Status: STUB — implementation will lift upstream v1.3.0 gui/ and input/ +// packages, with browser-side platform adapters in platform/v1/. +// +// Modelled architecturally on Gangleri42's cmd/wasmemu at: +// https://github.com/Gangleri42/seedhammer/tree/seedhammer-features/cmd/wasmemu +// (pinned at 0a3c63efb125d17d8ec86ce739ecd058c8747cfe), but his wasmemu +// emulates SH-II's touch UI — v1 is button-driven, so we rebuild from scratch +// while keeping his PWA shell pattern. +package main diff --git a/cmd/emulator/main.go b/cmd/emulator/main.go new file mode 100644 index 0000000..2a947b3 --- /dev/null +++ b/cmd/emulator/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Fprintln(os.Stderr, "emulator: not yet implemented — see docs/architecture/v1-buttons-and-ui.md and cmd/emulator/doc.go") + os.Exit(2) +} diff --git a/cmd/seedsigner-sim/doc.go b/cmd/seedsigner-sim/doc.go new file mode 100644 index 0000000..ed1bf6d --- /dev/null +++ b/cmd/seedsigner-sim/doc.go @@ -0,0 +1,16 @@ +// Command seedsigner-sim is a tiny Go helper that builds the SeedSigner +// emulator's static web assets. +// +// The actual emulation is NOT done in Go — it's done by hosting the +// upstream SeedSigner Python codebase in the browser via Pyodide. See +// docs/architecture/seedsigner-reuse.md for the architecture. +// +// This Go binary's job: +// - Download a pinned Pyodide release tarball +// - Pull the upstream SeedSigner Python source at a pinned commit +// - Verify checksums +// - Lay them out under web/seedsigner-sim/dist/ for the static-site build +// +// Status: STUB — implementation pending. Pinned commit choices documented +// in web/seedsigner-sim/UPSTREAM.md once that file exists. +package main diff --git a/cmd/seedsigner-sim/main.go b/cmd/seedsigner-sim/main.go new file mode 100644 index 0000000..1989fdc --- /dev/null +++ b/cmd/seedsigner-sim/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Fprintln(os.Stderr, "seedsigner-sim: not yet implemented — Pyodide build helper, see docs/architecture/seedsigner-reuse.md") + os.Exit(2) +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..5d35278 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,19 @@ +# Documentation + +## architecture/ + +Authoritative design docs for the project. These are the contract — every +implementation decision should be traceable back to one of these. + +| File | What it answers | +|---|---| +| [BASELINES.md](architecture/BASELINES.md) | Which exact upstream SHAs this project is built on top of, license findings, path-mapping table for the v1.0.0 → v1.3.0 layout refactor. | +| [v1-engrave-spec.md](architecture/v1-engrave-spec.md) | The MarkingWay USB-serial wire protocol used by upstream v1 — opcodes, framing, plate geometry, font format. | +| [v1-buttons-and-ui.md](architecture/v1-buttons-and-ui.md) | v1's physical button layout, GPIO mapping, screen-flow conventions, proposed emulator keyboard map. | +| [sh1e-spec.md](architecture/sh1e-spec.md) | The new on-the-wire envelope format for shipping plate designs from composer to controller (CBOR + CRC32, deterministic encoding, security analysis). | +| [seedsigner-reuse.md](architecture/seedsigner-reuse.md) | Strategy for bundling a faithful in-browser SeedSigner emulator via Pyodide + supporting both Classic and SeedSigner+ "jumbo" device profiles. | + +All five docs were drafted as prep before this repo existed — they originate +in `mineracks-infrastructure/roadmap/seedhammer-v1-companion/prep/`. The +copies here are the live authoritative versions; the prep docs are now +historical. diff --git a/docs/architecture/BASELINES.md b/docs/architecture/BASELINES.md new file mode 100644 index 0000000..ffdf3ee --- /dev/null +++ b/docs/architecture/BASELINES.md @@ -0,0 +1,181 @@ +# Project baselines (frozen 2026-05-28) + +These are the immutable starting points for the SeedHammer v1 companion port. +Always rebase against these exact SHAs unless deliberately bumping the +baseline — and if bumping, append a new dated section below, don't +overwrite this one. + +## Upstream v1 (seedhammer/seedhammer @ v1.3.0) + +| | | +|---|---| +| Tag | `v1.3.0` | +| Tag object SHA | `70ed718c797663beeb65d323340e3853fca7920f` | +| **Commit SHA** | **`2f071c1d8f23eb7fd39b15fc0acb8874113f801e`** | +| Repo | https://github.com/seedhammer/seedhammer | +| Why this tag | **Verified 2026-05-28**: v1.3.0 is the latest tag still targeting v1 hardware (Pi Zero / WaveShare 1.3" LCD HAT / mjolnir USB-serial engraver). All five v1.x tags (v1.0.0 through v1.3.0) are v1-hardware; v1.4.x+ are SeedHammer II (RP2040 / TinyGo / SH2E wire protocol). See "Baseline bump 2026-05-28" section below for the verification evidence. | + +### Other available v1.x tags + +All verified v1-hardware on 2026-05-28; later v1.x tags supersede earlier +ones with bug fixes and a directory-layout refactor (see bump note below). + +``` +v1.0.0 → 6f9aa7ac017db3978703e596c77e7919c7fdaa8d (original baseline; superseded) +v1.1.1 → 5b0265deae09117e9949ec8a1b76cc0ccc7bded0 (v1-hardware, pre-refactor layout) +v1.2.0 → 595a8c04574a6694e791be269710c3e81a7bab0a (v1-hardware, post-refactor layout) +v1.2.1 → f5c41e3a1b4ba815dfdee726d11b702d7cc4bc22 (v1-hardware, post-refactor layout) +v1.3.0 → 2f071c1d8f23eb7fd39b15fc0acb8874113f801e ← chosen baseline +``` + +### Baseline bump 2026-05-28: v1.0.0 → v1.3.0 + +Verified by direct GitHub-tree + raw-content inspection of all five v1.x +tags. **All are v1 hardware.** Evidence: + +- `go.mod` at v1.3.0 still imports `github.com/tarm/serial` (v1 USB-serial + engraver lib) and `periph.io/x/conn/v3` + `periph.io/x/host/v3` + (BCM283x GPIO lib, used by `bcm283x.GPIO6/19/5/26/13/21/20/16` for the + WaveShare HAT joystick + 3 keys — same pin map as v1.0.0). +- `flake.nix` at v1.3.0 references only `raspberrypi` (kernel, firmware, + camera modules 1/2/3, Pi Zero 2). **No TinyGo, no RP2040, no pico**. +- README at v1.3.0 explicitly: *"It runs on the same hardware as the + SeedSigner: Raspberry Pi Zero or Zero W, a WaveShare 1.3 inch 240x240 + LCD hat and a Pi Zero compatible camera with a OV5647 sensor."* +- `driver/mjolnir/driver.go` at v1.3.0: package comment *"driver for the + MarkingWay engraving machine"*, still uses `tarm/serial`. +- No `picobin/`, `uf2/`, `cmd/picosign`, `cmd/biptool`, `engrave/wire/`, + or `nfc/` at any v1.x tag — all are SeedHammer II markers. + +**Layout-refactor caveat (lift-path updates needed):** v1.2.0 reorganised +the tree without changing hardware. The "lift from upstream" paths below +have been updated to the v1.3.0 layout. v1.0.0 → v1.3.0 path mapping: + +| v1.0.0 path | v1.3.0 path | +|---|---| +| `input/input.go` | `driver/wshat/wshat.go` | +| `mjolnir/driver.go`, `mjolnir/sim.go` | `driver/mjolnir/driver.go`, `driver/mjolnir/sim.go` | +| `lcd/lcd_linux.go` | `driver/drm/drm_linux.go` | +| `camera/camera_linux.{cpp,go,h}` | `driver/libcamera/camera_linux.{cpp,go,h}` | +| `rgb16/`, `ninepatch/` | `image/rgb565/`, `image/ninepatch/` | +| `cmd/controller/platform_linux.go` | `cmd/controller/platform_rpi.go` | + +Top-level diff: v1.0.0 has 229 tree entries, v1.3.0 has 285. Net adds: +`driver/{drm,libcamera,mjolnir,wshat}`, `image/{alpha4,image.go,ninepatch,paletted,rgb565}`, +`font/{bitmap,vector}`, `bip39/{gen.go,wordlist.go}`. Removed: `affine/`, +`camera/`, `input/`, `lcd/`, `mjolnir/` (top-level), `ninepatch/`, `rgb16/` +(all subsumed by the refactor). go.mod adds `github.com/kortschak/qr` +(replaces `skip2/go-qrcode`) and `decred/dcrec/secp256k1/v4` as direct dep. + +The button-pin map (`GPIO 6/19/5/26/13/21/20/16` for Up/Down/Left/Right/ +Center/Button1/Button2/Button3) is **identical byte-for-byte** between +v1.0.0 and v1.3.0 — just moved file. Likewise the mjolnir wire protocol +constants (`StrokeWidth: 38, Millimeter: 126`) are unchanged. + +**Recommendation taken**: bump to v1.3.0. Rationale: same hardware, three +extra release-cycles of upstream bug-fixes, no risk of v2 transition +code (none exists at any v1.x tag), and the post-refactor layout is what +any future upstream cherry-picks will use. + +## Gangleri42 fork (Gangleri42/seedhammer @ seedhammer-features) + +| | | +|---|---| +| Branch | `seedhammer-features` | +| HEAD SHA | `0a3c63efb125d17d8ec86ce739ecd058c8747cfe` | +| HEAD date | 2026-05-22 | +| Repo | https://github.com/Gangleri42/seedhammer | + +### Fork-point note + +The fork's history is a **rewrite**, not a fork-from-tip. GitHub's `compare` +API returns 404 / null between upstream `main` and this branch because they +share no common ancestor near tip. Treating it as a separate codebase to +cherry-pick from, not a branch to merge. + +Lifting strategy: +1. Identify the files / packages we want from `seedhammer-features` at the + pinned HEAD (see [v1-engrave-spec.md](v1-engrave-spec.md) and + [v1-buttons-and-ui.md](v1-buttons-and-ui.md)) +2. Copy them as-is into the new repo with attribution + license preserved +3. Rebind the v1-incompatible bits (geometry constants, wire format, + button mapping) per the prep docs + +## Key files we plan to lift from Gangleri42 fork + +(All paths relative to fork root at the pinned HEAD) + +- `cmd/webnfc/` — composer PWA shell + Go-to-WASM entry +- `cmd/wasmemu/` — firmware-in-browser emulator (UI shell will be reused; + the WASM contents must be rebuilt against v1's `gui/` package) +- `cmd/webnfc-sim/` — combined composer + emulator +- `cmd/coldcard-sim/`, `cmd/coldcard-wasm/` — optional ColdCard emu +- `engrave/wire/` — **NOT** lifted directly; this is SH-II's SH2E format. + v1 has a completely different live-USB-serial protocol (see + [v1-engrave-spec.md](v1-engrave-spec.md)) +- `bezier/`, `bspline/`, `font/sh/`, `backup/`, `internal/golden/` — lift + as-is; these are geometry/font math, hardware-agnostic +- `gui/gridedit.go`, `gui/precompiled.go` — composer GUI helpers, lift + as-is if they remain decoupled from the v2 `gui/` package state + +## Key files we plan to lift from upstream v1.3.0 + +(All paths relative to upstream root at the pinned commit +`2f071c1d8f23eb7fd39b15fc0acb8874113f801e`. If you cross-reference older +prep docs that still cite v1.0.0 paths, see the path-mapping table in the +"Baseline bump 2026-05-28" section above.) + +- `driver/wshat/wshat.go` — v1 button + GPIO mapping (was `input/input.go` + at v1.0.0; see [v1-buttons-and-ui.md](v1-buttons-and-ui.md)) +- `gui/` — the v1 controller UI (Linux/Pi-side, full Go, not TinyGo) +- `driver/mjolnir/driver.go`, `driver/mjolnir/sim.go` — v1 engraver driver + + sim (was `mjolnir/...` at v1.0.0; see [v1-engrave-spec.md](v1-engrave-spec.md)) +- `driver/drm/drm_linux.go` — v1 LCD output (was `lcd/lcd_linux.go` at v1.0.0) +- `driver/libcamera/` — v1 camera (was `camera/` at v1.0.0) +- `font/comfortaa`, `font/poppins`, `font/constant` — v1 fonts + (pre-rasterised OpenType); v1.3.0 also adds `font/bitmap` + `font/vector` +- `image/rgb565/`, `image/ninepatch/` — moved from top-level `rgb16/` + + `ninepatch/` in the v1.2.0 refactor +- `backup/backup.go` — v1 plate-size constants (SmallPlate / SquarePlate / LargePlate) + +## License audit (verified 2026-05-28) + +Both repos use the **Unlicense** (public-domain dedication), not MIT as +originally assumed. Confirmed by fetching `LICENSE` from each pinned ref: + +- Upstream `seedhammer/seedhammer` @ `v1.3.0` — Unlicense + - https://raw.githubusercontent.com/seedhammer/seedhammer/v1.3.0/LICENSE + - File header: *"This is free and unencumbered software released into + the public domain."* with reference to https://unlicense.org +- `Gangleri42/seedhammer` @ `seedhammer-features` — Unlicense + - https://raw.githubusercontent.com/Gangleri42/seedhammer/seedhammer-features/LICENSE + - Byte-identical to upstream's LICENSE file (Gangleri42 inherited it). + +Implications for the new repo: + +- **No attribution legally required.** Both upstreams have explicitly + dedicated their work to the public domain. +- We will **still** preserve `CREDITS.md` and inline file-level attribution + blocks anyway, as a matter of good practice and community courtesy. The + goal of this port is to *amplify* the original projects, not to obscure + their origin. +- The new repo will adopt **Unlicense** as well, for full pipeline + compatibility and zero friction for downstream forks. + +For the SeedSigner assets we plan to bundle (see +[seedsigner-reuse.md](./seedsigner-reuse.md)), the upstream +`SeedSigner/seedsigner` repo is MIT-licensed. That requires us to keep the +MIT attribution alongside any SeedSigner assets we ship. Mixed-license +shipping is fine — we'll segregate SeedSigner-derived files under a +clearly-labelled subdirectory with the MIT notice retained. + +## How to use this file + +- Pin every PR against these SHAs (call them out in the PR body). +- If we deliberately bump: + - Don't edit the section above. Add a new section below dated and + cross-link old → new. + - Re-run the prep checks (engrave spec + button layout) against the + new SHAs in case anything changed. +- These SHAs are also the input to a deterministic `flake.nix` or + `vendor.txt` pin if we adopt one. diff --git a/docs/architecture/seedsigner-reuse.md b/docs/architecture/seedsigner-reuse.md new file mode 100644 index 0000000..cbc8516 --- /dev/null +++ b/docs/architecture/seedsigner-reuse.md @@ -0,0 +1,202 @@ +# SeedSigner emulator — reuse strategy + jumbo-variant support + +This doc covers two scope additions to the SeedSigner sim portion of the +project (Phase 2.5 in the project plan): + +1. Reuse of existing SeedSigner artwork and screen flow definitions +2. Support for the "jumbo" SeedSigner+ variant in addition to classic + +## TL;DR + +- Build the SeedSigner sim by **rendering upstream firmware code directly in + the browser via Pyodide** — bundle the official `SeedSigner/seedsigner@dev` + Python with the official screenshot generator's display-mock pattern, + switch the ST7789 driver for a canvas writer. ~3-4 days for a functional + build, +1-2 days for chassis art. +- This gets us **pixel-faithful screens for free** because we're literally + running the upstream Python UI code. +- One firmware, two device profiles — Classic 240×240 portrait and + SeedSigner+ 320×240 landscape. Same buttons, same flows, just a different + `(driver, w, h)` tuple. No code-fork needed. +- All reusable artwork (icons, fonts, logo, chassis CAD) is MIT-licensed + upstream — no permission needed. + +## Existing SeedSigner emulators (community survey) + +| Project | Lang/Runtime | Browser? | License | Verdict | +|---|---|---|---|---| +| `SeedSigner/seedsigner` `tests/screenshot_generator/` | Python+Pillow | No (headless) | MIT | **Reuse pattern + use as CI test oracle** | +| `enteropositivo/seedsigner-emulator` | Python+tkinter | Desktop, no | **No license** | Reference architecture only, don't copy code | +| `v4ires/seedsigner-emulator` | Fork of above | Desktop, no | No license | Same | +| `SeedSigner/seedsigner-settings-generator` | HTML+JS static | Yes | MIT | Useful as **packaging template** | +| Monero forks (`DiosDelRayo`, `Monero-HackerIndustrial`) | Python+tkinter | No | Various | Pattern reference only | + +**Key finding:** No browser-based SeedSigner emulator exists. We're in +greenfield. But the upstream screenshot generator at +`SeedSigner/seedsigner@dev:tests/screenshot_generator/generator.py` is +exactly the architecture we want — it runs the full Python UI with a +mocked ST7789 driver and renders to PNG. Reuse its `MockedDisplay` pattern, +swap PNG output for a `` writer via Pyodide. + +## Architecture decision: Pyodide vs Go-WASM port + +For the SeedHammer composer/emulator the language is Go (compiles to WASM +natively). For SeedSigner the language is Python. + +Three options for SeedSigner sim: + +| Option | Effort | Faithfulness | Bundle size | Maintenance | +|---|---:|---|---:|---| +| (a) Behavioural-only Go port — just emit the right QR formats | 2d | None — no UI | Tiny | Drift risk on every upstream UI change | +| (b) Hand-rebuild SeedSigner UI in JS/WASM from scratch | 7-10d | Best-effort | Medium | High — every upstream change needs porting | +| (c) **Bundle Pyodide + upstream Python verbatim** | 3-4d | **Identical to real device** | ~5MB gzipped | **Zero drift — bump upstream commit, done** | + +**Recommendation: (c)** for the SeedSigner sim, given the user's stated +preference for "great result that matches reality". The 5MB Pyodide bundle +is a one-time download (cached forever in PWA), and we get bit-perfect +fidelity to whatever upstream ships. When upstream releases v0.9.0, we +bump the commit, the sim updates automatically. + +This is *the same architecture choice* we made for the SeedHammer composer +(reuse upstream `engrave/`+`font/` packages directly), just expressed in +Python land via Pyodide instead of Go via WASM. + +## Reusable assets (all MIT — direct use OK) + +### Runtime UI assets +Paths relative to `SeedSigner/seedsigner@dev`: +- `src/seedsigner/resources/icons/` — `arrow-up`, `arrow-down`, `back`, + `btc_logo`, `btc_logo_30x30`, `btc_logo_bw`, `dire_warning`, `warning` + (plus `_selected` variants), PNG +- `src/seedsigner/resources/img/` — `btc_logo_60x60.png`, `logo_black_240.png` +- `src/seedsigner/resources/fonts/` — OpenSans, Inconsolata, NotoSans, + FontAwesome Free Solid, **`seedsigner-icons.otf`** (custom icon font) + +### Branding +- `docs/img/logo.svg` — official SeedSigner logo +- `docs/img/Mini_Pill_Main_Photo.jpg`, `Orange_Pill.JPG`, + `Open_Pill_*.JPG`, `Open_Pill_w_Comfort_Joystick.png` — hardware photos + +### Chassis CAD (best for rendering emulator frame) +- `enclosures/open_pill/` — `.f3d` Fusion 360 source + `.stl` +- `enclosures/orange_pill/` — `.f3d` + `.stl` for Upper, Lower, button, joystick +- `enclosures/orange_pill_mini/`, `open_pill_mini[_w_coverplate]/`, + `look_screws_pill/`, `pushcase/` + +**For SeedSigner+ chassis:** no upstream CAD exists. Sold by Go Brrr at +gobrrr.me. Either: +- render a generic landscape-form-factor chassis ourselves +- contact Go Brrr for permission to use their product render +- ship without chassis art initially (just the screen frame), add chassis + later if Go Brrr provides one or someone in the community models it + +## Behavioural spec — also upstream + +The most valuable thing we get from Pyodide-bundle-upstream is that +**`src/seedsigner/views/` + `src/seedsigner/gui/screens/` define every +screen flow in the device**. There's no need to re-document or +reverse-engineer flows — they're MIT-licensed code we run directly. + +## The screenshot generator as CI oracle + +We can wire `pytest tests/screenshot_generator/generator.py` into our +emulator's CI: render every canonical screen with the mocked display, +hash the PNGs, then have our browser emulator render the same screens +via Pyodide and compare. Any pixel divergence = regression. + +This is a **stronger** correctness story than any community emulator has. + +## Jumbo / SeedSigner+ variant + +### Hardware (verified 2026-05-28) + +| Field | Classic | SeedSigner+ (jumbo) | +|---|---|---| +| Screen | 1.3" 240×240 | **2.8" 240×320 (rendered as 320×240 landscape)** | +| Driver chip | `st7789` | `st7789` *or* `ili9341` (Go Brrr doesn't publish which; either is supported by firmware) | +| Pi board | Pi Zero v1.3 | **Same — Pi Zero v1.3** | +| Camera | OV5647 (Pi Zero camera) | **Same — OV5647-class** | +| Buttons | 5-way joystick + 3 keys | **Same — identical input pins + semantics** | +| Touchscreen | No | **No** (not in firmware, not in product) | + +Released in upstream **v0.8.6 "The Bigger Picture"** (2025-06-30). + +Sold by Go Brrr as **"SeedSigner+"** — ~€19 enclosure / ~€130 assembled. +Also available as **"Battery Powered SeedSigner+"** with AAA battery pack. + +A 3.5" ILI9486 480×320 variant is **declared but not implemented** in +firmware (`raise Exception("ILI9486 display not implemented yet")`). So a +true jumbo-jumbo exists on the roadmap — we'll plumb for it but won't +expose the profile until upstream lands the driver. + +### Firmware reality + +**Single codebase, multiple device profiles** — selection happens in +`src/seedsigner/hardware/displays/display_driver.py` via +`DisplayDriverFactory.instantiate_display_driver(display_type, width, height)`. + +For the emulator that means **one Python bundle, three (eventually four) +device profiles**: + +```yaml +profiles: + - id: classic + label: "SeedSigner Classic (1.3\" 240×240)" + driver: st7789 + width: 240 + height: 240 + orientation: portrait + - id: plus + label: "SeedSigner+ (2.8\" 320×240)" + driver: st7789 + width: 320 + height: 240 + orientation: landscape + - id: plus-ili9341 + label: "SeedSigner+ ILI9341 panel variant" + driver: ili9341 + width: 320 + height: 240 + orientation: landscape + # - id: jumbo-3-5 + # label: "SeedSigner 3.5\" (planned, not yet supported by firmware)" + # driver: ili9486 + # width: 480 + # height: 320 + # orientation: landscape + # disabled: true + # disabled_reason: "Upstream firmware ili9486 driver not yet implemented" +``` + +The browser emulator's UI gets a profile selector dropdown. Switching +profiles re-initialises the Pyodide-side `DisplayDriverFactory` with the +new tuple and re-renders. Buttons remain identical across all profiles. + +## Items to verify on real hardware + +1. Whether the SeedSigner+ panel's actual driver IC is ST7789 or ILI9341. + Go Brrr doesn't publish it; firmware accepts either. Our emulator + supports both as separate profiles. +2. Panel orientation handling — firmware reports 240×320 native and renders + 320×240 landscape. Visually verify on a real device that left-edge of + the rendered image matches the physical "top" of the screen as the user + holds it. +3. STL licensing for the SeedSigner+ enclosure if we want a chassis render. + Currently appears Go-Brrr-proprietary. + +## Open questions for project plan + +1. **License compatibility for Pyodide bundling.** Pyodide itself is MPL-2.0 + licensed. The Python wheels we include depend on each package's license. + Need to audit: SeedSigner deps + transitive deps for any GPL. +2. **Bundle size budget.** A baseline Pyodide bundle is ~10MB raw / ~3-5MB + gzipped. Plus SeedSigner code + assets. Need to measure and ensure PWA + caches sensibly. First load slower; subsequent loads instant. +3. **Camera/QR emulation in Pyodide.** SeedSigner reads QR codes via + PiCamera2/OpenCV. We need to wire the browser's ``-based QR + handoff to the SeedSigner Python's camera API stub. This is doable but + isn't trivial. Likely a Python shim that monkey-patches + `seedsigner.hardware.camera`. + +These three are not blockers for Phase 2.5 starting but should be flagged +upfront so we don't discover them mid-build. diff --git a/docs/architecture/sh1e-spec.md b/docs/architecture/sh1e-spec.md new file mode 100644 index 0000000..ffe970a --- /dev/null +++ b/docs/architecture/sh1e-spec.md @@ -0,0 +1,355 @@ +# SH1E — SeedHammer v1 Engrave envelope format + +**Status:** draft v0.1, 2026-05-28 +**Stability:** unstable — the format will change as we validate it against +real hardware. Pin the version byte; any breaking change bumps it. + +SH1E is the on-the-wire format that ships a plate design from the +browser-side composer to a SeedHammer v1 Pi controller, intended primarily +for transport via QR code(s) scanned by the Pi's camera. Secondary +transport channels (USB stick, HTTP) work without modification. + +The Pi side parses SH1E, runs the parsed design through its **existing +trusted rasteriser** (`engrave/` + `font/` + `backup/` packages), shows +the user a preview on the LCD, and waits for hold-to-confirm before +streaming the resulting 10-byte commands to the engraver via `mjolnir/`. + +The composer side emits SH1E for transport AND uses the same Go code in +WASM to render an in-browser preview that matches what the Pi will produce +**by code-identity**, not by re-implementation. See the [project plan +doc](./BASELINES.md) for the broader architecture. + +## Design goals + +1. **Intent, not commands.** SH1E describes what to engrave, not how. The + trusted Pi controller rasterises locally; the composer cannot directly + drive the engraver bytes. This keeps the security boundary in the right + place. +2. **Single-frame QR friendly.** A 24-word plate fits in one QR (≤ ~1100 + bytes for QR version 24 with low EC). Larger designs degrade to + multi-frame BBQr or animated QR. +3. **Self-describing.** Magic + version + length + CRC32 — any reader can + detect "this is SH1E", reject the wrong version cleanly, and detect + corruption from bit-flips in the QR. +4. **Canonical encoding.** Same design → same bytes → same QR. Users who + want to verify "the QR I'm scanning is the design I intended" can + reproduce it. +5. **Easy to parse safely.** Deterministic CBOR with strict ranges. No + recursive structures, no length-prefix tricks, no implicit defaults + that could be spoofed. + +## Envelope + +``` ++----------+----------+--------------+--------+-----------+ +| magic | version | payload_len | crc32 | payload | +| 4 bytes | 1 byte | 2 bytes (LE) | 4 bytes| N bytes | +| "SH1E" | 0x01 | 0x0000- | | | +| | | 0xFFFF | | | ++----------+----------+--------------+--------+-----------+ +``` + +- **`magic`** = ASCII `SH1E` (0x53 0x48 0x31 0x45). Constant. +- **`version`** = current `0x01`. Any other value rejected by readers. +- **`payload_len`** = exact byte count of `payload`, little-endian uint16. + Max 65535 bytes. (Reality budget: a single-frame QR fits ~1500 bytes + of binary data; we'd never approach the uint16 ceiling.) +- **`crc32`** = CRC-32/ISO-HDLC (poly `0xEDB88320`, init `0xFFFFFFFF`, + refin/refout true, xorout `0xFFFFFFFF`) computed over the `payload` + bytes only (NOT magic/version/length). Little-endian uint32. +- **`payload`** = CBOR-encoded `Design` (see below). RFC 8949 deterministic + encoding only. + +Total minimum overhead: **11 bytes** of envelope. + +## Payload structure (Design) + +The `Design` is a CBOR map. All fields are required unless marked +optional. Top-level keys are short integers (deterministic encoding rule: +integer keys sort before string keys; integer keys are encoded in their +smallest form). + +```cbor +{ + 1: # plate_type — see "Plate types" below + 2: [], # text_blocks (array, len 0..32) + 3: [], # svg_paths (array, len 0..16, OPTIONAL — omit if empty) + 4: (32) # design_fingerprint — SHA-256 of canonical-encoded fields 1-3, used as a stable design ID +} +``` + +### Plate types (integer enum, matches `backup/backup.go` order) + +| Value | Name | Dimensions (mm) | Source | +|---:|---|---|---| +| 0 | `SmallPlate` | 85 × 55 | `backup.SmallPlate` | +| 1 | `SquarePlate` | 85 × 85 | `backup.SquarePlate` | +| 2 | `LargePlate` | 85 × 134 | `backup.LargePlate` | + +### TextBlock + +```cbor +{ + 1: # font_id — see "Font IDs" + 2: # size — point size, 1..200 (clamped Pi-side) + 3: # x_mm — millimetres from plate origin, signed int (origin offset handled Pi-side) + 4: # y_mm — millimetres from plate origin + 5: # alignment — 0 Left | 1 Center | 2 Right + 6: # text — UTF-8, but Pi-side limited to ASCII (rejects non-ASCII at parse) + 7: # rotation — 0, 90, 180, 270 degrees only (OPTIONAL, default 0) +} +``` + +**Text length cap:** 256 chars per block (UTF-8 bytes, before ASCII check). +**Combined design cap:** 32 text blocks max. + +### Font IDs (matches upstream v1 fonts at the pinned baseline) + +| Value | Name | Source path | +|---:|---|---| +| 0 | `Comfortaa` | `font/comfortaa` | +| 1 | `Poppins` | `font/poppins` | +| 2 | `Constant` | `font/constant` | + +If we lift additional fonts in a later baseline, the enum extends but +**never renumbers**. + +### SvgPath + +For logos, custom marks, embedded designs. The Pi parses these to its +internal segment representation (`MoveTo` / `LineTo` / `QuadTo` / +`CubeTo`) — same shape as the font glyph code already handles. + +```cbor +{ + 1: # x_mm — anchor X + 2: # y_mm — anchor Y + 3: # scale_pct — percentage 1..1000 (clamped Pi-side) + 4: # path_d — SVG `d` attribute string, validated against a strict subset + 5: # rotation — 0/90/180/270 (OPTIONAL, default 0) +} +``` + +**SVG path subset (strict):** +- Commands allowed: `M`, `L`, `H`, `V`, `Q`, `C`, `Z` (and lowercase relatives `m`, `l`, `h`, `v`, `q`, `c`, `z`) +- No arcs (`A`/`a`) — they require curve approximation that's easier to do composer-side +- No `S`/`T` shortcuts — composer must emit explicit `Q`/`C` instead +- Coordinates: decimal numbers with optional leading sign, no scientific + notation, no comma separators, single-space-separated. (Stricter than + full SVG but trivially parseable in Go.) +- Max path length: 4096 chars +- Max number of points: 1024 + +## Canonical encoding rules + +Per RFC 8949 § 4.2.1, with these additional clarifications: + +1. Map keys sorted by integer value ascending. +2. Integers in their shortest CBOR form (tiny ints inline, then uint8, + uint16, uint32, uint64 as needed). +3. Text strings encoded as UTF-8 with no BOM. +4. Byte strings have explicit length prefix. +5. No indefinite-length items. +6. No tags (no CBOR semantic tags used in this spec). +7. Floating point not used anywhere (all numerics are integers). + +**Two encodings of the same Design MUST produce byte-identical payload.** +This is enforceable by the composer using a known-canonical CBOR library +(e.g. `fxamacker/cbor` with `EncOptions{Sort: SortBytewiseLexical}`). + +## QR transport + +A single SH1E envelope: + +| Design complexity | Approx envelope size | Recommended QR | +|---|---:|---| +| 12-word seed, SmallPlate, single font block | ~150 B | QR version 6, M ECC, alphanumeric mode | +| 24-word seed, LargePlate, 2 font blocks | ~280 B | QR version 10, M ECC, byte mode | +| 24-word seed + multisig descriptor, LargePlate, 4 font blocks | ~500 B | QR version 17, M ECC, byte mode | +| Above + small SVG logo | ~700 B | QR version 22, M ECC | +| Multi-plate manifest (3 plates) | 1.5-2 KB | **BBQr or animated QR** | + +For multi-frame transport, use **BBQr** (https://github.com/coinkite/BBQr) +as it's already supported by ColdCard / Sparrow / Specter and we'd get +ecosystem compatibility for free. Don't invent a new chunking format. + +The Pi controller scans BBQr or single QR transparently — the camera +flow on v1 already understands both via its existing QR reader. + +## Examples + +### Minimal: 12-word seed on SmallPlate, single font block + +```json5 +{ + 1: 0, // plate_type = SmallPlate + 2: [ + { + 1: 0, // font_id = Comfortaa + 2: 12, // size = 12pt + 3: 5, // x_mm + 4: 5, // y_mm + 5: 0, // alignment = Left + 6: "ABANDON ABILITY ABLE ABOUT ABOVE ABSENT ABSORB ABSTRACT ABSURD ABUSE ACCESS ACCIDENT" + } + ], + 4: <32-byte SHA-256> +} +``` + +CBOR encoding of this: approximately 130 bytes (depends on the exact +seed words used). + +### Two-block layout with logo + +```json5 +{ + 1: 1, // plate_type = SquarePlate + 2: [ + { // title + 1: 1, // font_id = Poppins + 2: 18, + 3: 10, 4: 8, + 5: 1, // Center + 6: "MY MULTISIG KEY 1 OF 3" + }, + { // seed words + 1: 0, // font_id = Comfortaa + 2: 12, + 3: 5, 4: 25, + 5: 0, + 6: "WORD1 WORD2 WORD3 ... WORD24" + } + ], + 3: [ + { // BTC logo top-right + 1: 70, 2: 5, + 3: 100, // 100% scale + 4: "M 0 0 L 10 0 L 10 10 L 0 10 Z M 2 2 L 8 8 M 8 2 L 2 8" + } + ], + 4: <32-byte SHA-256> +} +``` + +## Parser validation rules (Pi side) + +The Pi-side parser **must** reject a SH1E envelope if any of these hold, +without further processing: + +1. `magic` ≠ `SH1E` +2. `version` ≠ `0x01` +3. `payload_len` doesn't match actual payload byte length +4. `crc32` doesn't match recomputed CRC +5. Payload is not canonical CBOR (sort order wrong, indefinite-length used, + floats present, unknown tag present) +6. Required key missing (1, 2, or 4 absent) +7. Any unknown integer key in any map (forwards-compat: unknown string + keys allowed and ignored, unknown integer keys MUST be rejected — this + reserves int keys for future-version expansion) +8. `plate_type` not in `{0, 1, 2}` +9. `font_id` references a font not bundled in the running firmware +10. `text` contains a non-ASCII codepoint (because v1 fonts are ASCII-only) +11. Any numeric field outside its documented range +12. Total text block count > 32 or any text length > 256 bytes +13. Total SVG path count > 16 or any path string > 4096 chars +14. `design_fingerprint` doesn't match a recomputed SHA-256 of fields 1-3 +15. Resulting plate layout exceeds plate dimensions (with the + `outerMargin = 3 mm` from `backup.go` applied) +16. Total command stream would exceed the engraver's safe operating envelope + +After validation passes, the Pi: +- Renders a preview on the LCD using the existing rasterisation pipeline +- Shows total command count + estimated engrave time +- Waits for hold-to-confirm on Button3 (the existing engrave-confirm pattern) +- Streams the resulting commands via `mjolnir/` + +## Forward compatibility + +Two extension dimensions: + +1. **Bumping version byte** (`0x01 → 0x02`) — used for breaking changes + (e.g. coordinate system change, new envelope structure). Old firmware + rejects, prompting user to update. +2. **Adding new integer keys** to the maps — used for non-breaking + additions (e.g. new optional `kerning` field on `TextBlock`). Old + firmware **rejects** unknown integer keys (per rule 7 above) so a + composer using v0.2 keys can target v0.1 readers only by omitting them. +3. **String keys** are reserved for **forward-compat hints** — old readers + ignore them. Use sparingly and never for security-relevant data (since + they're ignored). + +## Security analysis + +### Threat model + +The Pi controller is the trusted compute base. The composer is untrusted — +could be malicious, could have bugs, could be served from a tampered CDN. + +### What SH1E protects against + +- **Wrong content engraved silently:** the Pi rasterises locally and shows + a preview the user must confirm. A malicious composer cannot bypass. +- **Out-of-range coordinates damaging the machine:** parser rule 15 + + range checks on every numeric. +- **Buffer-overflow style attacks:** strict size caps on all variable-length + fields, CBOR parser used must be vetted for buffer safety. +- **Confused-deputy via SVG complexity:** strict subset, hard caps on + path string length and point count. +- **Bit-flip corruption in QR:** CRC32 catches single-byte flips with + ~99.9999998% probability. + +### What SH1E does NOT protect against + +- **Social engineering** to engrave the wrong seed. The Pi preview is + the user's last line of defence. They must read it carefully. +- **Pre-image or collision attacks on `design_fingerprint`.** It's + SHA-256 over canonical bytes — strong enough that any practical + collision is computationally infeasible. +- **Malicious upstream firmware.** Out of scope — that's a wider problem. + +### Fuzzing requirement + +Before any release ships with SH1E support, the Pi-side parser MUST be +fuzz-tested. Recommend `go-fuzz` or Go 1.18+ native fuzzing for at least +1 CPU-week against the parser entry point. + +## Open questions + +1. **Should we use `engrave/wire/`-style envelope for binary symmetry with + SH2E?** Currently no — SH2E is a *post-rasterised command stream*, SH1E + is a *design intent*. They serve different purposes. But the magic + prefix `SH1E` parallels `SH2E` deliberately. +2. **Should the Pi sign acknowledgement of a successful engrave back via + QR?** Useful for multi-plate flows (composer can verify which plate + was actually written). Out of scope for v0.1 — add in v0.2 if there's + user demand. +3. **What about hardware-bound designs?** A user could optionally embed + the iDRAC/serial number of their specific SeedHammer v1 in the design + to prevent "wrong physical device played the wrong design". Probably + overkill. Out of scope for v0.1. +4. **Should we support custom fonts uploaded as part of the design?** No. + That dramatically expands the attack surface (font parsers are + notoriously bug-prone) and breaks the "rasterise with trusted Pi + code" property. If users want a new font, propose upstream and we + ship it in firmware. + +## Implementation references + +- CBOR library (Pi side, Go): `github.com/fxamacker/cbor/v2` — already + used elsewhere in the seedhammer codebase +- CBOR library (composer side, Go-WASM): same package, compiled to WASM +- CRC32 (Go): `hash/crc32.MakeTable(crc32.IEEE)` — standard library +- BBQr (Go): port from `github.com/coinkite/BBQr` Python reference, or + use an existing Go BBQr lib if one exists at implementation time + +## Sources + +- Upstream `backup/backup.go` for plate dimensions (pinned in + [BASELINES.md](./BASELINES.md)) +- Upstream `mjolnir/driver.go` for engraver wire details (see + [v1-engrave-spec.md](./v1-engrave-spec.md)) +- RFC 8949 (CBOR) +- RFC 1952 (CRC-32 variant matches; see also CRC catalogue at + reveng.sourceforge.io for `CRC-32/ISO-HDLC`) +- BBQr spec at https://github.com/coinkite/BBQr diff --git a/docs/architecture/v1-buttons-and-ui.md b/docs/architecture/v1-buttons-and-ui.md new file mode 100644 index 0000000..287e66c --- /dev/null +++ b/docs/architecture/v1-buttons-and-ui.md @@ -0,0 +1,296 @@ +# SeedHammer v1 — buttons + UI flow + +Source pinned to upstream tag `v1.0.0` of `github.com/seedhammer/seedhammer` +(commit `6f9aa7a`, dated 2023-06-29). All file references are to that tag. + +## Hardware + +- **Display:** Waveshare 1.3" 240×240 LCD HAT (ST7789-based). Confirmed in the + package comment of `input/input.go:1-2`: + > "package input implements an input driver for the joystick and buttons on + > the Waveshare 1.3" 240x240 HAT." +- **Camera:** OV5647 (per existing project knowledge — `cmd/controller/main.go` + comment notes "in the same configuration as SeedSigner", which uses OV5647). +- **Pi board:** Raspberry Pi Zero (per `cmd/controller/main.go:1-2` — + "It runs on a Raspberry Pi Zero, in the same configuration as SeedSigner."). + SeedSigner v1 used Pi Zero v1.3 (camera-cable variant); SeedHammer ships the + same SKU. +- **Buttons:** 8 physical inputs total = 5-way joystick + 3 keys. GPIO mapping + confirmed from both the SeedHammer source and the Waveshare wiki — they match + exactly. See table below. + +## Physical layout (ASCII sketch) + +Looking at the HAT mounted on top of the Pi, with the 240×240 LCD facing the +operator: the joystick sits at the lower-left corner and the three keys form a +vertical column on the lower-right. + +```text ++-----------------------------------+ +| | +| | +| 240x240 LCD | +| | +| | +| | +| | +| ^ | +| | | +| <-- (o) --> [KEY1] | +| | [KEY2] | +| v [KEY3] | +| | ++-----------------------------------+ +``` + +The joystick is a 5-way: up, down, left, right, press-in (Center). KEY1/KEY2/KEY3 +are momentary tactile switches. All eight inputs are active-low with pull-ups +enabled in firmware (`input/input.go:53` — `btn.Pin.In(gpio.PullUp, gpio.BothEdges)`). + +## Code-level button names + +All eight inputs are exposed as constants of type `input.Button` +(`input/input.go:19-31`). The order of the iota matters because the controller's +debug `input ` command (`cmd/controller/debug.go:62-94`) and the GUI +code branch on these constants. + +| Physical input | Go identifier | BCM GPIO | Source | +|-----------------------|-------------------|----------|------------------------------| +| Joystick Up | `input.Up` | 6 | `input/input.go:42` | +| Joystick Down | `input.Down` | 19 | `input/input.go:43` | +| Joystick Left | `input.Left` | 5 | `input/input.go:44` | +| Joystick Right | `input.Right` | 26 | `input/input.go:45` | +| Joystick Press | `input.Center` | 13 | `input/input.go:46` | +| Key 1 (top) | `input.Button1` | 21 | `input/input.go:47` | +| Key 2 (middle) | `input.Button2` | 20 | `input/input.go:48` | +| Key 3 (bottom) | `input.Button3` | 16 | `input/input.go:49` | +| (debug-only) Rune | `input.Rune` | — | `input/input.go:29` | +| (debug-only) Screenshot| `input.Screenshot`| — | `input/input.go:30` | + +`Rune` and `Screenshot` are synthetic events emitted only from the debug build +(`cmd/controller/debug.go`); on real hardware only the eight physical inputs fire. + +The Waveshare wiki's Pinout table at + matches this list 1:1 (KEY1=P21, +KEY2=P20, KEY3=P16, Up=P6, Down=P19, Left=P5, Right=P26, Press=P13), which means +the v1 firmware is using the HAT's stock wiring — no custom board, no jumpers. + +Event delivery model (`input/input.go:33-77`): one goroutine per pin, 10 ms +debounce, sends `Event{Button, Pressed bool}` on a channel. The GUI layer adds +a derived `Click` flag (`Pressed=false` transition after a `Pressed=true`) — +that's what most screens key off via `e.Click`. + +## Button-role conventions across the GUI + +Reading every `switch e.Button` in `gui/gui.go` (and `input.Button*` references — +~50 of them), the v1 firmware uses a strikingly consistent role assignment: + +- **Joystick Up/Down** — scroll a list, move a selection up/down a column, + move keyboard cursor up/down a row. +- **Joystick Left/Right** — page navigation (Receive↔Change addresses, + Singlesig↔Multisig on the main screen), and left/right cursor inside the + on-screen keyboard. +- **Joystick Center** — synonym for `Button3` ("primary confirm") in most + screens. Explicit examples: + - `gui/gui.go:2096` — `case input.Center, input.Button3:` (keyboard rune select) + - `gui/gui.go:1666` — `case input.Button2, input.Center:` (Confirm-Seed edit) + - `gui/gui.go:2231` — `case input.Button3, input.Center:` (engrave next-step) + - `gui/gui.go:2462` — `case input.Button3, input.Center:` (main screen select) +- **Button1 (top)** — Back / cancel. Renders with `assets.IconBack`. +- **Button2 (middle)** — Secondary action (Edit, Info, Flip-camera). On the + Engrave screen it's the press-and-hold "dry run" arming key + (`gui/gui.go:1241-1247`). +- **Button3 (bottom)** — Primary action / confirm / next. Renders with + `assets.IconCheckmark` or `assets.IconRight`. Press-and-hold to engrave + (`gui/gui.go:1248-1255`, `confirmDelay`). + +These conventions are not declared in one place — they emerge from how +`layoutNavigation` is called with `Style: StyleSecondary` (B1, B2) vs +`Style: StylePrimary` (B3). The pattern is consistent enough that the emulator +can mirror it without per-screen overrides. + +## Main UI screen flow (high-level) + +Entry point is `cmd/controller/main.go`, which constructs `gui.NewApp(...)` and +loops on `a.Frame()` forever. The app owns a single `MainScreen` +(`gui/gui.go:2311-2335, 2674-2702`); every other screen is a transient child +mounted on the MainScreen's fields (`scanner`, `desc`, `seed`, `engrave`, +`warning`, etc.) and unmounted when its `Layout` returns done. + +```text + boot + | + v + +--------------+ + | MainScreen | page = singleKey | multiKey + | (carousel) | Left/Right: change page + +--------------+ Center/B3: Select() + | + +--------+---------+ + | | + page == singleKey page == multiKey + | | + v v + +-------------+ +---------------+ + | SeedScreen | | ScanScreen | (camera + QR) + | (enter 12/ | | Scan wallet | + | 24 words) | | output desc | + +-------------+ +---------------+ + | | + | v + | +-------------------+ + | | DescriptorScreen | + | | (shows xpubs; | + | | loops over each | + | | signer's seed) | + | +-------------------+ + | | + | v + | +-------------------+ + | | SeedScreen | per-signer + | +-------------------+ + | | + +---------+--------+ + v + +-----------------+ + | EngraveScreen | step-by-step + | (Connect Mjolnir| instructions; B3 + | → align → cut) | hold-to-engrave; + +-----------------+ B2 hold = dry run; + | B1 = back/cancel. + v + complete --> back to MainScreen +``` + +Screen-by-screen button table (only the screens an emulator user will see in +the first 5 minutes; QR-scan and shamir flows omitted): + +| Screen | Up/Down | Left/Right | Center / B3 | B1 (Back) | B2 | Source | +|-----------------------|--------------------|-------------------|------------------------------|-------------------|---------------------|-----------------------| +| `MainScreen` | — | switch page | confirm `Select()` | — | — | `gui/gui.go:2456-2494`| +| `ChoiceScreen` (12/24)| change choice | — | confirm choice | back | — | `gui/gui.go:~1585` | +| `WordKeyboardScreen` | cursor row | cursor column | type letter | back | (delete word?) | `gui/gui.go:2043-2099`| +| `SeedScreen` (confirm)| select word | — | (B3) confirm seed | back (or discard) | (B2/Center) edit | `gui/gui.go:1653-1703`| +| `DescriptorScreen` | — | — | (B3) proceed | (B1) back | (B2) addresses | `gui/gui.go:475-495` | +| `AddressesScreen` | scroll page | Receive↔Change | — | (B1) close | — | `gui/gui.go:246-269` | +| `ScanScreen` | — | — | (B3) accept | (B1) back | (B2) flip-camera | `gui/gui.go:610-625` | +| `EngraveScreen` | — | — | (B3) hold-to-engrave | (B1) prev/cancel | (B2) hold for dry-run| `gui/gui.go:1227-1263`| +| `ConfirmWarningScreen`| — | — | (B3) hold to confirm | (B1) decline | — | `gui/gui.go:864-870` | +| `ErrorScreen` | — | — | (B3) dismiss | — | — | `gui/gui.go:794-805` | + +Two interaction nuances the emulator must replicate: + +1. **Hold-to-confirm.** `EngraveScreen` and `ConfirmWarningScreen` distinguish + `e.Pressed` (key down) from `e.Click` (full down-up cycle). They start a + `confirmDelay` countdown on press and complete the action only if the key is + still held when the timeout fires (`gui/gui.go:1248-1255`). A browser + emulator must therefore expose press-down and press-up as separate events, + not just keypress. +2. **Idle screensaver.** `App.Frame` (`gui/gui.go:2706-2717`) shows a screensaver + after 3 min of no input and "eats" the first button press to wake. The + emulator should mirror this (or at least not break it) to keep behaviour + true. + +## Proposed emulator keyboard mapping + +The v1 hardware has exactly 8 buttons + 2 debug synthetics, all available on a +standard desktop keyboard: + +| Browser key | Maps to | Notes | +|-----------------------|--------------------|---------------------------------------------| +| `ArrowUp` | `input.Up` | Joystick up | +| `ArrowDown` | `input.Down` | Joystick down | +| `ArrowLeft` | `input.Left` | Joystick left | +| `ArrowRight` | `input.Right` | Joystick right | +| `Enter` | `input.Center` | Joystick press-in. Most "confirm" is here. | +| `1` | `input.Button1` | Top key (Back / cancel) | +| `2` | `input.Button2` | Middle key (Secondary / dry-run) | +| `3` | `input.Button3` | Bottom key (Primary confirm / hold-to-act) | +| `s` (shift+S) | `input.Screenshot` | Debug-only on hardware; useful in emulator | +| typing a letter A–Z | `input.Rune` | Debug `runes` shortcut equivalent | + +Recommended secondary aliases (no conflicts): + +- `Escape` → `input.Button1` (universal "back" muscle memory). +- `Space` → `input.Button3` (universal "confirm" muscle memory; works with + hold-to-confirm naturally because keydown/keyup map cleanly). +- `w/a/s/d` → Up/Left/Down/Right (gamer convention; optional, not default). + +The emulator must emit **down and up** events separately. Browser model: +`keydown` → `Pressed: true`, `keyup` → `Pressed: false`. The 10 ms hardware +debounce can be skipped in the emulator since the OS already debounces. +Auto-repeat must be suppressed for keys that drive hold-to-confirm +(`event.repeat` filter on `keydown`), otherwise Button3 will fire +`Pressed: true` repeatedly and the GUI's `confirm.Start(...)` will never settle. + +## v2 / Gangleri42 reference mapping (for comparison) + +Important finding: Gangleri42's `cmd/wasmemu/` is **not a v1 emulator**. From +`cmd/wasmemu/keyboard.go:11-21` and the visible `cmd/wasmemu/index.html` header +("SeedHammer II — firmware emulator", 480×320 canvas): + +> "SeedHammer II is a touch device — this is the primary input path on real +> hardware (see processTouch in cmd/controller/platform_sh2.go). [...] Touch is +> the only navigation input; keyboard exists solely for the NFC-tap shortcut." + +The wasmemu binds `seedhammerTouch(x, y, pressed)` (mouse on canvas) for +navigation, and binds digit keys `1`–`9`, `0`, `e`, `q`, `w` to **NFC-tap +payload shortcuts** — not to UI buttons. The full payload-key table from +`cmd/wasmemu/index.html`: + +| Key | NFC payload | +|-----|----------------------------------------------------------| +| 1 | BIP-39 12-word | +| 2 | BIP-39 24-word | +| 3 | P2WSH 2-of-3 multisig | +| 4 | P2SH 2-of-3 multisig | +| 5 | singlesig (bare xpub) | +| 6 | BlueWallet JSON multisig | +| 7 | NIP-19 nsec | +| 8 | NIP-19 npub | +| 9 | codex32 share A (2-of-3) | +| 0 | codex32 unshared | +| e | CUSTOM block text | +| q | unknown format (rejection) | +| w | compound Nostr (rejection) | + +**Implication for our v1 emulator design:** there is no prior-art keyboard map +to copy. We get to pick the v1 scheme cleanly. The proposed mapping above +(arrow keys + Enter + 1/2/3) reuses the digit keys for button-press in a way +that **does not collide** with the v2 emu's NFC shortcuts — because v1 has no +NFC. If we later build a combined v1+v2 emulator, the keys 1/2/3 will need to +context-switch based on whether the focused device is v1 (UI button) or v2 +(NFC payload). Easier solution: reserve a different family of keys (e.g., +F1/F2/F3) for v1 buttons if the combined emu happens. For a v1-only emu, the +proposed mapping above is correct. + +We could optionally borrow Gangleri42's `seedhammerSynthTapText` / +`seedhammerSynthTapNDEF` pattern (untyped JS bridge globals) to bolt on a +"paste a mnemonic" debug helper in the v1 emulator that auto-types into the +`WordKeyboardScreen` via `input.Rune` events — same mechanism v1 already uses in +`cmd/controller/debug.go` `runes` command. + +## Open questions + +- **Long-press semantics.** v1 firmware reads `confirmDelay` from the GUI + package; we should pin the exact value (likely 1.5 s — see + `gui/gui.go` reference to `confirmDelay`). The emulator needs to mirror it + exactly or hold-to-confirm "feels off". Worth grepping for the const in a + follow-up pass. +- **Screensaver.** Should the emulator implement the 3-min idle screensaver, + or is it a distraction? Probably skip in the browser — most demos last <3 min + and the "eat first wake-press" behaviour confuses screencasting. +- **Camera substitute.** v1 `ScanScreen` expects a live OV5647 frame. The + emulator will need a stub camera (likely a canned QR-bytes injector, mirroring + Gangleri42's `seedhammerSynthTap` but for QR not NFC). Out of scope for this + doc; track separately. +- **Engraver substitute.** v1 `EngraveScreen` writes to a serial port spoken to + the Mjolnir engraver. The debug build already wires `mjolnir.NewSimulator()` + (`cmd/controller/debug.go:18-21`) — reuse this in the wasm build by tagging + appropriately. +- **Letter input.** `WordKeyboardScreen` uses Up/Down/Left/Right to drive a + 4-row on-screen keyboard. Should the emulator also accept direct A-Z typing + via `input.Rune` (already supported in debug builds), or should it force the + user through the joystick to faithfully reproduce hardware UX? Suggest both: + default to faithful joystick, expose a "type words" debug helper for + productivity. diff --git a/docs/architecture/v1-engrave-spec.md b/docs/architecture/v1-engrave-spec.md new file mode 100644 index 0000000..0d965db --- /dev/null +++ b/docs/architecture/v1-engrave-spec.md @@ -0,0 +1,323 @@ +# SeedHammer v1 engrave wire format + +Reverse-engineered by reading the upstream Go controller source at the v1.0.0 +tag. This is the protocol the Raspberry Pi Zero speaks over USB-serial to the +MarkingWay engraving machine. + +## Source + +- Upstream repo: `github.com/seedhammer/seedhammer` +- Tag: **v1.0.0** (commit `6f9aa7a`, released 29 June 2024) +- Why v1.0.0 and not v1.4.x: from v1.4.0 onward the upstream repo became the + SeedHammer II firmware. v1.0.0 is the last tag that is unambiguously the + original v1 hardware (Pi Zero + MarkingWay engraver + the same + configuration as SeedSigner; see `cmd/controller/main.go:1-3`). + +Key files: + +| Path | LOC | Role | +|---|---|---| +| `mjolnir/driver.go` | 1–418 | The complete engraver wire driver. Opens the serial port, runs the init/program/finish state machine, defines `Program.Move` / `Program.Line`. | +| `mjolnir/sim.go` | 1–204 | Reference simulator. Independently confirms every opcode and the response sequence — treat it as the canonical decoder. | +| `engrave/engrave.go` | 1–1217 | `Program` interface (`Move(image.Point)` / `Line(image.Point)`); rasterises text, QR (constant-time and standard), shapes into Move/Line. | +| `font/font.go` | 1–78 | Glyph format: pre-decoded OpenType segments (MoveTo/LineTo/QuadTo/CubeTo) with float32 coords, ASCII-only index. | +| `backup/backup.go` | 1–80 | Plate size table and outer/inner margins (mm). | +| `gui/gui.go` | 1086–1130 | Where `mjolnir.Engrave(...)` is actually called: per-side `Program{DryRun}` constructed, glyph commands streamed through `Engrave(...)`. | + +The driver package is named **mjolnir** (Thor's hammer) — that's the v1 +codename for the engraver subsystem; do not look for a package called +`engraver` or `driver`. + +## Transport + +USB-serial via [`github.com/tarm/serial`](https://github.com/tarm/serial). +Connection params hard-coded in `mjolnir/driver.go:44-83`: + +| Setting | Value | +|---|---| +| Baud | **115200** | +| Word length | 8 | +| Stop bits | 1 | +| Parity | none | +| Flow control | none (handshake=0, replace=0) | +| Xon limit | 2048 | +| Xoff limit | 512 | + +Device path: + +- Linux (Pi Zero): `/dev/ttyUSB0`, falls back to `/dev/ttyUSB1`. +- Windows (dev/test): `COM3`. + +The MarkingWay engraver therefore presents to the Pi as a USB-serial CDC +device. There is no GPIO/SPI involvement at the controller-to-engraver layer +(the Pi's GPIO is used only for the LCD/buttons inside the SeedSigner-style +case, not for engraver I/O). + +Handshaking is **command/response at the application layer**, not at the +serial layer. Every controller-initiated command produces a 1-or-more-byte +echo or status from the engraver (see opcode table). There is no XON/XOFF or +RTS/CTS; the controller-side buffer is `bufio.NewWriterSize(dev, +progBatchSize*cmdSize) = 80*10 = 800 bytes`. + +## Wire format + +**Binary, fixed-width, command-tagged.** No framing, no checksum, no +length prefix, no ACK byte. The receiver is a simple byte-driven state +machine that knows how many bytes follow each opcode. + +### Two distinct phases + +1. **Control phase.** Variable-length commands with immediate echo. Used + for init, set speed, set delays, move-to-origin, query position, and to + enter program mode. +2. **Program phase.** Fixed 10-byte commands ("draw commands"), streamed + in **batches of 80 commands (800 bytes)** without per-command ACK. The + engraver sends a 1-byte status byte after each batch (`bufferProgramStatus + 0x60`) requesting the next batch, plus one `programStepStatus 0x6f` per + completed step and a final `programCompleteStatus 0x6a`. From + `mjolnir/driver.go:108-109` and `:240-305`. + +### Draw command layout (program phase, 10 bytes) + +From `mjolnir/driver.go:370-380` (`mkcoords`) and `:394-418` +(`Move` / `Line`): + +``` +byte 0 : opcode (0x80 = MoveTo / pen up, 0x00 = LineTo / pen down) +bytes 1..3 : X coordinate, 24-bit little-endian unsigned +bytes 4..6 : Y coordinate, 24-bit little-endian unsigned +bytes 7..9 : Z coordinate, 24-bit little-endian unsigned (always 0 in v1) +``` + +Concrete example: move pen to (10 mm, 5 mm). With `Millimeter = 1/0.00796 ≈ +125.628`, 10 mm = 1256 ≈ `0x0004E8`, 5 mm = 628 ≈ `0x000274`. On the wire: + +``` +80 E8 04 00 74 02 00 00 00 00 +``` + +LineTo to the same point: `00 E8 04 00 74 02 00 00 00 00`. + +Coordinate range: `0` to `0xFFFFFF` (24-bit unsigned). Negative coords +panic (`driver.go:372-374`). Z is reserved — the controller always sends +zero and the simulator never reads it (`sim.go:80-84` only parses X and Y). + +Padding: if a batch isn't full, the controller pads with **`0xFF`** bytes +(NOP, `nopCmd`). The engraver treats `0xFF` as a no-op in program mode +(`sim.go:179-180`). The padding is critical: omit it and the engraver +won't emit the completion status (`driver.go:243-245` — "Otherwise, the +engraver won't send a completed status"). + +### Opcode table + +All values from `mjolnir/driver.go:85-106`. Echo/response columns from +`sim.go:86-119` and the `expect(...)` calls in `driver.go:185-220`. + +| Opcode | Name | Args (bytes after opcode) | Reply | Phase | Notes | +|---|---|---|---|---|---| +| `0x00` | `initCmd` | none in control phase; in program phase the leading `0x00` is `lineCmd` and is followed by 9 coord bytes | After init: a status byte loop (see status table) | Control / Program | Same byte serves two purposes depending on phase. `sim.go:149-157` makes this explicit. | +| `0x16` | (un-named "query position") | none | Echoes `0x16`, then 9 coord bytes (X/Y/Z each 24-bit LE) | Control | Defined in `driver.go:215-220` but the function is bound to `_` — present in the protocol, not used by v1.0.0 firmware. | +| `0x21` | `moveToOriginCmd` | 1 byte: `0x50` (`moveToOriginCmdExtra`) | Echoes `0x21 0x00` (`moveToOriginCmdResponse`) | Control | "Reset origin to current physical position" (`driver.go:322-327`). Called twice in `Engrave`: before and after the needle-warmup pass. | +| `0x30` | `setSpeedCmd` | 6 bytes: print(LE16), move(LE16), `xxx`(LE16). Speed range `[1000, 30]` where **lower = faster** | Echoes `0x30` | Control | `driver.go:226-229`. v1 always passes `xxx = 0xE6` (230) — purpose undocumented, possibly a Z-axis or acceleration parameter. Called three times in `Engrave`: 300/300 for warmup, then user move/print speeds, then 300/300 for the post-engrave move-to-end. | +| `0x31` | `setDelaysCmd` | 2 bytes: penDown delay, penUp delay (0–255) | Echoes `0x31` | Control | `driver.go:232-235`. v1 hard-codes `setDelays(0x14, 0x14)` = (20, 20). | +| `0x60` | `initProgramCmd` | 2 bytes: nbatches (LE16). Each batch = 80 × 10-byte commands = 800 bytes | None directly — engraver then drives the program-phase loop via status bytes | Control → Program | `driver.go:250` and `:171-174` in the sim. nbatches must be ≤ 0xFFFF (program-too-large guard at `driver.go:246-249`). | +| `0x80` | `moveCmd` | 9 coord bytes (X/Y/Z LE24) | None (batched) | Program | Pen-up move. | +| `0x00` | `lineCmd` | 9 coord bytes (X/Y/Z LE24) | None (batched) | Program | Pen-down draw / "hammer along path". Note this is the same byte as `initCmd`; phase-disambiguated. | +| `0xAF` | `cancelCmd` | none | Engraver transitions through `cancellingStatus` to `cancelledStatus` | Any | Sent on the quit channel (`driver.go:147`). | +| `0xFF` | `nopCmd` | 9 ignored bytes (treated as filler) | None (batched) | Program | Pad byte for incomplete batches. | + +### Status byte table (engraver → controller) + +From `driver.go:99-106` and `sim.go:108-119`. These are always **single +bytes** read by the controller; there's no length prefix. + +| Status | Name | When sent | Controller reaction | +|---|---|---|---| +| `0x00` | `initializedStatus` | After `initCmd` succeeds | Exit init loop. | +| `0x60` | `bufferProgramStatus` | When the engraver's command buffer is ready for the next 80-command batch | Send 80 × 10-byte commands (with 0xFF padding if needed). | +| `0x62` | `cancellingStatus` | After receiving `cancelCmd`, before stopping | Wait. | +| `0x65` | `cancelledStatus` | Cancel complete | Set `ErrCancelled`. During init the controller responds by re-sending `initCmd` (`driver.go:200-206`). | +| `0x6A` | `programCompleteStatus` | Final batch consumed | Break out of program loop. | +| `0x6F` | `programStepStatus` | After each draw command executes | Used to drive the progress bar (`driver.go:282-295`); the controller throttles updates to every 10th step or the last step. | + +Note `0x00` is **both** the init-success status and the `lineCmd` opcode. +The driver disambiguates by remembering which phase it's in +(`stateExecuting` vs everything else, `sim.go:148-157`). + +### Connection lifecycle (one full engrave job) + +The order is documented by `mjolnir.Engrave(...)` in `driver.go:111-366`: + +1. `cancel()` → `wr(initCmd)`, loop on status until `initializedStatus`. +2. `setSpeeds(300, 300, 0xE6)` — warmup speeds. +3. `setDelays(0x14, 0x14)`. +4. `origin()` — reset the origin (engraver assumes current physical + position is (0,0)). Required because the engraver does not retain + absolute position across power cycles (`driver.go:322-327`). +5. **Needle-warmup pass**: a tiny 3-step program that walks + `(0,0) → line → move → line → move → line → move` out to (10 mm, 10 mm) + in 3 segments, to "exercise the needle" — "some machine needles are + stuck for the first few engravings" (`driver.go:325-345`). +6. `origin()` again (resets origin to back to (0,0) physically). +7. `setSpeeds(printSpeed, moveSpeed, 0xE6)` — user speeds. +8. `runProgram(prog, progress)` — the actual engrave: stream nbatches × 80 + draw commands, padded with 0xFF. +9. `setSpeeds(300, 300, 0xE6)` again. +10. `moveTo(prog.End)` — park the head at the user-supplied end point. + +The `MoveSpeed` / `PrintSpeed` fields on `Program` are normalised in +`[0,1]` where 0 = lowest, 1 = highest, and mapped to engraver units by +`speed = printSpeed*30 + (1-printSpeed)*1000` (`driver.go:347-358`). So +on the wire the engraver wants **smaller numbers = faster** (30 fast, 1000 +slow). Defaults: `defaultMoveSpeed = 0.5`, `defaultPrintSpeed = 0.1` +(`driver.go:40-42`). + +## Plate geometry + +### Coordinate system + +- **Units on the wire: machine steps.** One machine step = `0.00796 mm` + (`mjolnir.Step` in `driver.go:30-35`). The inverse `Millimeter = 1/Step + ≈ 125.628 steps/mm` is the scale passed into `backup.Engrave` + (`gui/gui.go:1012`). +- **Origin: physical needle position at the moment `moveToOriginCmd` + fires.** The engraver has no absolute encoder; the user is expected to + jog the head to the plate's bottom-left fiducial before engraving (this + is the "EngraveSideA" GUI step in `gui.go:1029-1036`). +- **Axes orientation**: X right, Y up in the controller's image-space. + Coordinates are 24-bit unsigned on the wire → effectively only the + positive quadrant is addressable, which matches a plate fixed at the + origin. +- **StrokeWidth**: `0.3 mm` (`mjolnir.StrokeWidth`, + `driver.go:30-31`). This is the punch-impression width assumed for + hatching and font stroking — not a wire parameter, but it propagates + into the rasterisation done in `engrave.go` and so determines how many + Move/Line commands a glyph produces. + +### Plate sizes (`backup/backup.go:23-57`) + +Three SKUs, all expressed in **millimetres of usable engrave area** (the +metal plate is larger; these are the rectangles the engraver paints in). + +| `PlateSize` | mm (W × H) | Offset on bed (mm) | Use | +|---|---|---|---| +| `SmallPlate` (0) | 85 × 55 | (97, 0) | 12-word seed only | +| `SquarePlate` (1) | 85 × 85 | (97, 49) | Seed + small descriptors | +| `LargePlate` (2) | 85 × 134 | (97, 0) | Full multisig descriptor backup | + +Note the constant X-offset of **97 mm** in `offset()` +(`backup.go:49-56`). The engrave area starts 97 mm in from the origin on +every plate — that's the gap from the engraver's physical home to the +plate clamp. Y offset is 49 mm only for the square plate (the square +plate sits higher in the clamp). + +### Safety margins + +- `outerMargin = 3 mm` (`backup.go:79`) — minimum distance from any + drawn pixel to the plate edge. +- `innerMargin = 10 mm` (`backup.go:80`) — clear region around the + plate's mounting holes. + +### Font / glyph format + +The Pi-side controller **rasterises everything to Move/Line itself**. +The engraver knows nothing about glyphs, characters, or curves — it only +sees pen-up/pen-down moves to 24-bit coordinates. + +From `font/font.go:11-78`: + +- `font.Face` carries `Metrics{Ascent, Height float32}` plus an ASCII-only + glyph index: `Index [unicode.MaxASCII]Glyph` — **only ASCII < 0x80 is + supported**; non-ASCII runes return `false` from `Decode`. +- Each `Glyph` references a slice of `Segments []uint32` containing one + of four opcodes followed by float32-bit-encoded coord pairs: + - `SegmentOpMoveTo` (0) — 1 point + - `SegmentOpLineTo` (1) — 1 point + - `SegmentOpQuadTo` (2) — 2 points (control, endpoint) + - `SegmentOpCubeTo` (3) — 3 points (two controls, endpoint) +- Three font faces are baked in at build time, all OpenType converted + ahead of time by `font/convert.go`: + - `font/comfortaa/` + - `font/poppins/` + - `font/constant/` (used by `engrave.ConstantStringer` for the + constant-time seed/passphrase paths — see `engrave.go:610-790`). +- Quad and cubic Béziers are flattened to line segments inside + `engrave.go` (search `SegmentOpQuadTo` / `SegmentOpCubeTo` ≈ + `engrave.go:1049-1057`) before being emitted as `p.Line(...)`. + +So the wire never carries a glyph index. A composer that wants to emit +the same plates must either (a) ship its own font rasteriser producing +identical Move/Line streams, or (b) reuse the upstream `font` + `engrave` +packages directly. + +## How v1 differs from v2 / SH2E (high-level) + +v2 ("SH2E") is the wire format in `Gangleri42/seedhammer` on the +`seedhammer-features` branch under `engrave/wire/wire.go`. It is a +**different layer entirely**: a payload format, not a live machine +protocol. + +| Aspect | v1 (this doc) | SH2E | +|---|---|---| +| What it describes | Live USB-serial protocol between Pi and engraver | A self-describing payload (text grid or curve set) carried out-of-band | +| Transport | USB-serial @ 115200 baud, command/response | NFC tag / NDEF record, MIME `application/vnd.seedhammer.engrave` | +| Frame | None — raw opcodes; phase implicit | 16-byte envelope: `'SH2E'` magic + version + mode + ptype + reserved + body length + CRC-32 | +| Integrity | None | CRC-32/IEEE over body | +| Versioning | None on the wire; firmware version baked into Pi controller | Version byte (currently `0x01`) | +| Coordinates | 24-bit LE per axis, in machine steps (1 step = 7.96 μm) | Knot streams (curves) or text-grid rows; abstract — engraver decides geometry | +| Content addressed | Move/Line raster only; controller has already chosen layout | Either `PtypeTextGrid` (16 lines × 26 chars max) or `PtypeCurves` (knots) | +| State | Stateful: init → setSpeed → setDelays → origin → warmup → program → end | Stateless: payload is fully self-describing | +| Cancellation | `0xAF` byte on serial | n/a — payload is delivered atomically | +| Constant-time modes | Done at the rasteriser layer (`engrave.ConstantStringer`, `engrave.ConstantQR`) — the engraver sees identical command counts for any seed | Explicit `ModeCT = 1` flag in envelope (and v1 firmware rejects it — see "v1 firmware rejected by SH2E" note in the wire summary) | +| Suitability for the composer | Composer must rasterise text/QR into Move/Line streams itself, then either drive the engraver directly OR emit a `.shp` file that mimics what `cmd/controller` would send | Composer can produce a high-level payload and let the engraver handle layout | + +Practical implication for the v1 composer web app: we cannot just emit +"the v1 wire format" the way SH2E lets you emit an envelope. We need a +**rasteriser + plate layout engine** that produces a Move/Line stream +identical to what `backup.Engrave(...)` would produce in the Pi +controller. The Move/Line stream itself is then trivially serialisable +(it's just `(opcode, x, y)` triples). Easiest paths: + +1. Port `engrave/engrave.go` + `font/font.go` + `backup/backup.go` to + TS/Rust/whatever — they're pure functions, no hardware dependencies. +2. Or run the upstream Go packages headless from the composer backend + (`engrave.Program` is an interface; supply a no-op `Move`/`Line` impl + that just records calls). + +## Open questions / things to verify on real hardware + +1. **What is the third `setSpeedCmd` argument** (`xxx = 0xE6`)? The + driver always passes 230, never anything else. Could be Z-axis speed, + acceleration ramp, or a vestigial dwell time. Not documented anywhere + in the code; needs a logic-analyser capture. +2. **Is Z (bytes 7-9 of a draw command) really always zero?** The + simulator never reads it. If the MarkingWay firmware actually does + anything with Z, we'd be silently ignoring it. Sniff the wire on a + genuine v1 unit to confirm. +3. **`0x16` query-position**: the driver has the code but the result is + thrown away (`_, _ = atleast, queryPos` at `driver.go:221`). Does the + engraver actually respond to it? If yes, a composer could surface + live position feedback. +4. **What happens if we send draw commands with `Z != 0`?** Could be + useful for a deeper punch, or could panic the firmware. Don't try on + anything that holds value until verified. +5. **Buffer depth.** `progBatchSize = 80` is fixed in the driver. Is 80 + a firmware-dictated maximum, or just a conservative number the + controller chose? If we can raise it, big plates engrave with fewer + round-trips. +6. **Plate origin assumption.** The driver assumes the user has jogged + the needle to the plate's bottom-left before pressing engrave (the + first `origin()` zeros the position there). The composer has no way + to enforce this. Consider whether we add an explicit "home / jog" + step in the composer-driven flow, or whether we keep the manual jog + step from the SeedSigner-style UI. +7. **Cancellation race.** The driver sends `cancelCmd` then waits for + `cancelledStatus`, but if a batch is mid-flight, can the engraver + drop part of a 10-byte command and resync? Worth checking what the + real firmware does — the sim assumes clean cancellation + (`sim.go:147-148`). +8. **ASCII-only glyphs.** `font.Face.Index` is sized to + `unicode.MaxASCII`. If the composer needs to emit BIP-39 in any + language other than English, the font subsystem needs widening (or + we restrict to English wordlists). Confirm whether v1 firmware has + any opinion here — probably not, since it only sees Move/Line. diff --git a/engrave/doc.go b/engrave/doc.go new file mode 100644 index 0000000..f9bf6e5 --- /dev/null +++ b/engrave/doc.go @@ -0,0 +1,20 @@ +// Package engrave converts plate designs into MarkingWay engraver commands. +// +// Wraps three concerns: +// +// - Geometry: take a layout (text + SVG paths + plate type) and produce +// a stream of MoveTo/LineTo commands in the engraver's coordinate +// system (machine steps, 1 step ≈ 0.00796 mm). +// +// - Tessellation: convert higher-order curves (Quad/Cube/Spline) into +// line segments at the engraver's resolution. Uses bezier/ + bspline/. +// +// - Wire: serialise the command stream into the 10-byte binary frames +// the MarkingWay protocol expects. See engrave/wire/ for the on-the-wire +// formats (engraver USB-serial as well as SH1E for QR transport). +// +// Status: STUB — to be lifted from upstream seedhammer/seedhammer at v1.3.0 +// (commit 2f071c1d8f23eb7fd39b15fc0acb8874113f801e), specifically the +// engrave package. See docs/architecture/v1-engrave-spec.md for the +// audit of the v1 wire protocol that this package targets. +package engrave diff --git a/engrave/wire/doc.go b/engrave/wire/doc.go new file mode 100644 index 0000000..31a9eb8 --- /dev/null +++ b/engrave/wire/doc.go @@ -0,0 +1,19 @@ +// Package wire is the parent for v1 wire formats. +// +// Two sub-protocols share this directory: +// +// - wire/ (this dir, future): the live MarkingWay USB-serial protocol +// spoken by the Pi controller to the physical engraver. 10-byte binary +// command frames in 80-command batches, with status-byte handshake. See +// docs/architecture/v1-engrave-spec.md for the full audit. +// +// - wire/sh1e/: the QR-transport envelope (magic + version + length + +// CRC32 + CBOR-encoded design). See docs/architecture/sh1e-spec.md. +// +// SH1E is a transport from composer to Pi; the live wire is the protocol +// the Pi speaks to the engraver. They are NOT the same and don't share an +// encoder. +// +// Status: STUB — live wire encoder to be lifted from upstream's mjolnir/ +// driver at v1.3.0. +package wire diff --git a/engrave/wire/sh1e/doc.go b/engrave/wire/sh1e/doc.go new file mode 100644 index 0000000..e467352 --- /dev/null +++ b/engrave/wire/sh1e/doc.go @@ -0,0 +1,27 @@ +// Package sh1e implements the v1 SH1E plate-design envelope. +// +// SH1E ships a plate design (plate type + text blocks + optional SVG paths) +// from the browser-side composer to a SeedHammer v1 Pi controller, intended +// primarily for transport via QR code(s) scanned by the Pi's camera. +// +// The envelope structure: +// +// +----------+----------+--------------+--------+-----------+ +// | magic | version | payload_len | crc32 | payload | +// | 4 bytes | 1 byte | 2 bytes (LE) | 4 bytes| N bytes | +// | "SH1E" | 0x01 | uint16 | | CBOR | +// +----------+----------+--------------+--------+-----------+ +// +// Payload is CBOR-encoded per RFC 8949 deterministic encoding rules so a +// given Design produces byte-identical bytes every time. +// +// The composer side encodes; the Pi controller side decodes + validates + +// rasterises locally using the trusted engrave/ pipeline. The composer +// never ships pre-rendered engraver commands — keeping the security +// boundary on the Pi. +// +// Full spec: docs/architecture/sh1e-spec.md +// +// Status: STUB — reference encoder + decoder + canonical-encoding test +// fixtures pending. Will land in Phase 1 of the project. +package sh1e diff --git a/font/comfortaa/doc.go b/font/comfortaa/doc.go new file mode 100644 index 0000000..59e85ce --- /dev/null +++ b/font/comfortaa/doc.go @@ -0,0 +1,6 @@ +// Package comfortaa holds the Comfortaa pre-rasterised font data for the +// v1 engrave pipeline. +// +// ASCII-only. To be lifted verbatim from upstream seedhammer/seedhammer at +// v1.3.0. Status: STUB. +package comfortaa diff --git a/font/constant/doc.go b/font/constant/doc.go new file mode 100644 index 0000000..e64767b --- /dev/null +++ b/font/constant/doc.go @@ -0,0 +1,5 @@ +// Package constant holds the constant-width fallback font for the v1 +// engrave pipeline. +// +// ASCII-only. To be lifted verbatim from upstream at v1.3.0. Status: STUB. +package constant diff --git a/font/doc.go b/font/doc.go new file mode 100644 index 0000000..a1d0db7 --- /dev/null +++ b/font/doc.go @@ -0,0 +1,21 @@ +// Package font provides the v1 controller's pre-rasterised engraver fonts. +// +// The engraver itself knows nothing about glyphs — the Pi controller +// rasterises every character to MoveTo/LineTo segments before sending bytes +// down the USB-serial pipe. These fonts (Comfortaa, Poppins, and a +// constant-width fallback) are pre-baked OpenType segments compiled into +// the Go binary. +// +// Subdirectories carry the actual glyph data: +// +// - font/comfortaa/ — Comfortaa font, pre-rasterised +// - font/poppins/ — Poppins font, pre-rasterised +// - font/constant/ — constant-width fallback font +// +// Character set is ASCII-only. Lifting non-ASCII glyphs requires either +// new fonts upstream or a deliberate decision to add Unicode support to +// the v1 engrave pipeline (out of scope for this companion port). +// +// Status: STUB — directory tree mirrors upstream layout; glyph data to be +// lifted verbatim from upstream seedhammer/seedhammer at v1.3.0. +package font diff --git a/font/poppins/doc.go b/font/poppins/doc.go new file mode 100644 index 0000000..e652f7c --- /dev/null +++ b/font/poppins/doc.go @@ -0,0 +1,5 @@ +// Package poppins holds the Poppins pre-rasterised font data for the v1 +// engrave pipeline. +// +// ASCII-only. To be lifted verbatim from upstream at v1.3.0. Status: STUB. +package poppins diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c959b09 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/mineracks/seedhammer-v1-companion + +go 1.22 diff --git a/gui/doc.go b/gui/doc.go new file mode 100644 index 0000000..e5e3ec9 --- /dev/null +++ b/gui/doc.go @@ -0,0 +1,22 @@ +// Package gui is the v1 controller's on-device UI — screen drawing, menu +// navigation, page state machine. +// +// Self-contained, hardware-abstracted: it talks to platform/ for input +// events and frame output. That abstraction lets the same gui run on a +// real Pi (via driver/wshat + driver/drm) and in the browser emulator +// (via platform/v1 + canvas). +// +// Screen flow follows the v1 controller's existing conventions: +// +// - Joystick (Up/Down/Left/Right/Center) = navigation +// - Button1 = back +// - Button2 = secondary action (hold-to-arm dry-run) +// - Button3 = primary confirm (hold-to-engrave) +// +// Hold-to-confirm distinguishes Pressed from Click with a confirmDelay +// timer (see gui.go in upstream). +// +// Status: STUB — to be lifted from upstream seedhammer/seedhammer at v1.3.0. +// Browser-emulator-specific tweaks (mock camera, keyboard event source) +// live in platform/v1, NOT in this package. +package gui diff --git a/input/doc.go b/input/doc.go new file mode 100644 index 0000000..8f8d8ca --- /dev/null +++ b/input/doc.go @@ -0,0 +1,25 @@ +// Package input is the v1 controller's hardware input abstraction. +// +// On a real Pi: reads GPIO pins from the WaveShare 1.3" LCD HAT via +// periph.io (driver/wshat in upstream layout). +// +// In the browser emulator: keyboard event source from the host page, +// translated to the same Event{Button, Pressed} envelope. +// +// Eight inputs total: 5-way joystick (Up/Down/Left/Right/Center) + 3 keys +// (Button1/Button2/Button3). GPIO pin map (BCM283x): +// +// Up → GPIO 6 +// Down → GPIO 19 +// Left → GPIO 5 +// Right → GPIO 26 +// Center → GPIO 13 +// Button1 → GPIO 21 +// Button2 → GPIO 20 +// Button3 → GPIO 16 +// +// Active-low with internal pull-ups, 10 ms debounce. +// +// Status: STUB — to be lifted from upstream at v1.3.0 (driver/wshat path, +// formerly input/input.go pre the v1.2.0 refactor). +package input diff --git a/internal/golden/doc.go b/internal/golden/doc.go new file mode 100644 index 0000000..8edbf3f --- /dev/null +++ b/internal/golden/doc.go @@ -0,0 +1,11 @@ +// Package golden is a test-fixture provider used by both the composer-side +// preview WASM and the emulator to assert that rendered output is identical +// to reference goldens. +// +// Golden images, golden SH1E byte sequences, and golden engrave command +// streams all live here. CI compares live rendering against these. +// +// Status: STUB — fixtures generated progressively as features land. The +// SH1E reference encoder will be tested against the documented examples +// in docs/architecture/sh1e-spec.md. +package golden diff --git a/platform/v1/doc.go b/platform/v1/doc.go new file mode 100644 index 0000000..12f29c0 --- /dev/null +++ b/platform/v1/doc.go @@ -0,0 +1,21 @@ +// Package v1 holds platform adapters that bind gui/, input/, and engrave/ +// to either a real SeedHammer v1 device or the browser emulator. +// +// Two build-time targets: +// +// - real: GOOS=linux GOARCH=arm — for the Pi Zero v1.3 itself. +// Binds gui/ frame output to driver/drm/, input/ to +// driver/wshat/ (GPIO via periph.io), engrave/ to +// driver/mjolnir/ (USB-serial to MarkingWay). +// +// - browser: GOOS=js GOARCH=wasm — for the in-browser emulator. +// Binds gui/ frame output to a JS-exposed canvas writer, +// input/ to keyboard events forwarded from the page, +// engrave/ to a null sink (preview) or animation harness +// (visual playback). +// +// Build constraints select the right adapter; both expose the same +// platform.Driver interface so gui/ doesn't know which it's running on. +// +// Status: STUB — interface design and both adapters land in Phase 1 + Phase 2. +package v1 diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..9740d9e --- /dev/null +++ b/web/README.md @@ -0,0 +1,19 @@ +# web/ + +Static-site assets for each browser target. + +| Path | What it ships | +|---|---| +| `composer/` | The plate composer PWA (uses `cmd/composer/` WASM) | +| `emulator/` | The v1 emulator PWA (uses `cmd/emulator/` WASM) | +| `combined/` | The three-pane combined sim (uses all three) | +| `seedsigner-sim/` | The Pyodide-hosted SeedSigner emulator | +| `shared/` | Common CSS/JS/assets used by multiple shells | + +Each subdirectory has its own `index.html`, `app.js`, `app.css`, +`manifest.webmanifest`, `sw.js`, modelled on Gangleri42's PWA shells. + +Build pipeline (TBD — likely Vite or a small `make` rule that wires +`go build -o app.wasm ./cmd/X` and copies static files to `dist/`). + +Status: skeleton only; shells lifted in Phase 1.