mirror of
https://github.com/mineracks/seedhammer-v1-companion.git
synced 2026-06-26 20:51:06 +10:00
Initial skeleton — Phase 1 scaffolding
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) <noreply@anthropic.com>
This commit is contained in:
commit
3696dd6b34
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@ -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
|
||||
63
CREDITS.md
Normal file
63
CREDITS.md
Normal file
@ -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.
|
||||
24
LICENSE
Normal file
24
LICENSE
Normal file
@ -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 <https://unlicense.org>
|
||||
90
README.md
Normal file
90
README.md
Normal file
@ -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.
|
||||
19
backup/doc.go
Normal file
19
backup/doc.go
Normal file
@ -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
|
||||
10
bezier/doc.go
Normal file
10
bezier/doc.go
Normal file
@ -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
|
||||
7
bspline/doc.go
Normal file
7
bspline/doc.go
Normal file
@ -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
|
||||
19
cmd/combined-sim/doc.go
Normal file
19
cmd/combined-sim/doc.go
Normal file
@ -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
|
||||
11
cmd/combined-sim/main.go
Normal file
11
cmd/combined-sim/main.go
Normal file
@ -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)
|
||||
}
|
||||
24
cmd/composer/doc.go
Normal file
24
cmd/composer/doc.go
Normal file
@ -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
|
||||
11
cmd/composer/main.go
Normal file
11
cmd/composer/main.go
Normal file
@ -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)
|
||||
}
|
||||
24
cmd/emulator/doc.go
Normal file
24
cmd/emulator/doc.go
Normal file
@ -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
|
||||
11
cmd/emulator/main.go
Normal file
11
cmd/emulator/main.go
Normal file
@ -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)
|
||||
}
|
||||
16
cmd/seedsigner-sim/doc.go
Normal file
16
cmd/seedsigner-sim/doc.go
Normal file
@ -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
|
||||
11
cmd/seedsigner-sim/main.go
Normal file
11
cmd/seedsigner-sim/main.go
Normal file
@ -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)
|
||||
}
|
||||
19
docs/README.md
Normal file
19
docs/README.md
Normal file
@ -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.
|
||||
181
docs/architecture/BASELINES.md
Normal file
181
docs/architecture/BASELINES.md
Normal file
@ -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.
|
||||
202
docs/architecture/seedsigner-reuse.md
Normal file
202
docs/architecture/seedsigner-reuse.md
Normal file
@ -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 `<canvas>` 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 `<canvas>`-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.
|
||||
355
docs/architecture/sh1e-spec.md
Normal file
355
docs/architecture/sh1e-spec.md
Normal file
@ -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: <uint> # plate_type — see "Plate types" below
|
||||
2: [<TextBlock>], # text_blocks (array, len 0..32)
|
||||
3: [<SvgPath>], # svg_paths (array, len 0..16, OPTIONAL — omit if empty)
|
||||
4: <bytes>(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: <uint> # font_id — see "Font IDs"
|
||||
2: <uint> # size — point size, 1..200 (clamped Pi-side)
|
||||
3: <int> # x_mm — millimetres from plate origin, signed int (origin offset handled Pi-side)
|
||||
4: <int> # y_mm — millimetres from plate origin
|
||||
5: <uint> # alignment — 0 Left | 1 Center | 2 Right
|
||||
6: <tstr> # text — UTF-8, but Pi-side limited to ASCII (rejects non-ASCII at parse)
|
||||
7: <uint> # 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: <int> # x_mm — anchor X
|
||||
2: <int> # y_mm — anchor Y
|
||||
3: <uint> # scale_pct — percentage 1..1000 (clamped Pi-side)
|
||||
4: <tstr> # path_d — SVG `d` attribute string, validated against a strict subset
|
||||
5: <uint> # 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
|
||||
296
docs/architecture/v1-buttons-and-ui.md
Normal file
296
docs/architecture/v1-buttons-and-ui.md
Normal file
@ -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 <name>` 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
|
||||
<https://www.waveshare.com/wiki/1.3inch_LCD_HAT> 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.
|
||||
323
docs/architecture/v1-engrave-spec.md
Normal file
323
docs/architecture/v1-engrave-spec.md
Normal file
@ -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.
|
||||
20
engrave/doc.go
Normal file
20
engrave/doc.go
Normal file
@ -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
|
||||
19
engrave/wire/doc.go
Normal file
19
engrave/wire/doc.go
Normal file
@ -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
|
||||
27
engrave/wire/sh1e/doc.go
Normal file
27
engrave/wire/sh1e/doc.go
Normal file
@ -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
|
||||
6
font/comfortaa/doc.go
Normal file
6
font/comfortaa/doc.go
Normal file
@ -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
|
||||
5
font/constant/doc.go
Normal file
5
font/constant/doc.go
Normal file
@ -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
|
||||
21
font/doc.go
Normal file
21
font/doc.go
Normal file
@ -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
|
||||
5
font/poppins/doc.go
Normal file
5
font/poppins/doc.go
Normal file
@ -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
|
||||
3
go.mod
Normal file
3
go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module github.com/mineracks/seedhammer-v1-companion
|
||||
|
||||
go 1.22
|
||||
22
gui/doc.go
Normal file
22
gui/doc.go
Normal file
@ -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
|
||||
25
input/doc.go
Normal file
25
input/doc.go
Normal file
@ -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
|
||||
11
internal/golden/doc.go
Normal file
11
internal/golden/doc.go
Normal file
@ -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
|
||||
21
platform/v1/doc.go
Normal file
21
platform/v1/doc.go
Normal file
@ -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
|
||||
19
web/README.md
Normal file
19
web/README.md
Normal file
@ -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.
|
||||
Loading…
Reference in New Issue
Block a user