# Rust port — staged plan This is the plan for the native-Rust direction first discussed alongside the A/B-bootloader idea. **What changed since then:** the track format is now formally specced (`docs/track-format.md`) with a golden-vector conformance suite (`tests/run.mjs`). That suite is the thing that makes a port *safe* — any Rust engine has a precise, executable definition of "correct" to validate against, the same one `engine.js` and `app.py` already pass. ## The core idea Port from the **inside out**, lowest-risk first. The pure logic (track codec, then the scheduler) is host-testable with zero hardware and is gated by the existing golden vectors. Only once that's proven do we touch drivers, A/B, and the actual firmware. We do **not** rip out CircuitPython until the Rust engine passes the vectors *and* the drivers are proven on hardware. ## Architecture: one firmware core, modular drivers per form factor Trying many form factors (Kit, Explorer, **Grid**/Scroll Pack, …) is how we *discover the line between core and driver*. In Rust that line is enforced by the type system instead of copied by hand — today each CircuitPython form factor is its own ~1,500-line `app.py` clone; the Rust build is one core crate plus a thin per-board binary. **`pm-core` — the core (`no_std`, zero hardware):** - the track-format codec (`rust/track-format`, Stage 1) and the scheduler/clock (Stage 2, already `no_std` and building for RP2350), - playback-flow (rep/end/continue, segment seams), app state, set-list model, - the **USB-MIDI / live-sync / firmware-update protocol** logic (the SysEx opcode handling, which is form-factor-independent). It is host-testable and gated by the golden vectors — the same suite `engine.js` and `app.py` pass. **This is "core."** **Driver traits — what the core is generic over (the swappable part):** define small project traits — `Display` (or render straight to an `embedded-graphics` `DrawTarget`), `Inputs` (yields button / touch events), `Clicker` (audio out), `Indicator` (RGB) — and write each concrete driver against **`embedded-hal`** bus traits (`I2c`, `SpiBus`, `OutputPin`, `DelayNs`). The core's UI code then doesn't care whether the target is a 17×7 mono matrix or a 320×480 colour TFT. **Per-board binary crates — `pm-kit`, `pm-explorer`, `pm-grid`:** a thin `main.rs` BSP that instantiates the right concrete drivers and hands them to the generic core: - **Grid** (Scroll Pack): IS31FL3731 over I²C (a `DrawTarget` for a 17×7 mono frame) + 4 GPIO buttons. - **Explorer / Kit:** ST7789 via `mipidsi` + `embedded-graphics`; GT911 touch (Kit) over I²C; WS2812 via `ws2812-pio`; I²S to the PCM5102A via PIO. **The honest caveat (what the Grid prototype is teaching us):** a 17×7 mono grid and a 320×480 touch TFT are too different for *one* pixel-identical UI. So the clean split is **core engine + protocol + state = fully shared; the *view* = per-display-class.** The Grid is the most extreme, minimal display in the lineup, which makes it the best forcing-function for finding exactly where that boundary falls before we commit drivers to Rust. The CircuitPython `pico-scroll/` build exists to nail that UI down on real hardware first. ## Stages ### Stage 0 — toolchain in a container Add a Rust toolchain image (mirroring `hardware/eda/`): a `Containerfile` with `rustup`, the `thumbv8m.main-none-eabihf` target (RP2350 is Cortex-M33), `flip-link`, `probe-rs`, `elf2uf2`. Driven by a `run.sh` like the EDA one. **Never on the host.** ### Stage 1 — `track-format` crate ✅ DONE (`rust/track-format/`) Implemented and **passing**: `./rust/run.sh` builds the container and runs `cargo test`, which validates the crate against `tests/fixtures/track-format.json` (conformance + idempotency). The Rust codec agrees with `engine.js` and `app.py` on every vector — and carries `vol`/`cd`, so it's the most spec-complete of the three. Original scope below. #### (original) Stage 1 — `track-format` crate ← the concrete first PR A pure, `no_std`-compatible crate: `parse(&str) -> Track` and `serialize(&Track) -> String`, plus a `normalize()` that emits the neutral structure from `docs/track-format.md` §5. Then a `cargo test` that reads `tests/fixtures/track-format.json` and asserts each case's `norm` and round-trip — i.e. a **third adapter alongside `js_adapter.mjs` / `py_adapter.py`**. When this is green, the Rust engine provably agrees with web + device on every groove, euclid, swing, ghost, polymeter, and the playback-flow tokens. No hardware, fully testable in the container. This is the highest-value slice: small, gated by work already done, and it proves the toolchain. ### Stage 2 — scheduler/engine ✅ DONE (`rust/track-format/src/schedule.rs`) Ported the look-ahead step scheduler (the `durs` math from `app.py` `tick`/`_prepare_next`). `render(track, bars)` produces the deterministic click timeline; `tests/schedule.rs` asserts the timings — quarter-note spacing, subdivisions, swing 2/3:1/3, polymeter 5:4, accents/ghosts, mute, multi-bar looping. All green on the host, no hardware. The real-time firmware loop will just play this timeline against the wall clock. **Also done:** the crate is now `#![no_std]` + `alloc` and **builds for the RP2350 target** (`cargo build --lib --target thumbv8m.main-none-eabihf`) — the codec + scheduler are firmware-ready. ### Stage 3 — drivers (hardware) 🔧 IN PROGRESS (`rust/pm-kit/`) **Milestone 1 (boot-proof) built:** `rust/pm-kit/` is a minimal RP2350 binary (rp235x-hal) that blinks GP25 — compiles for the target and packs to `pm-kit.uf2` via `./rust/pm-kit/build.sh`. Confirms toolchain + RP2350 boot block + flash before any drivers. Once it blinks on the Pico 2 we add drivers (display first) and link `pm-core`. HAL choice (rp235x-hal vs embassy) finalizes with the first real driver. On `embassy` / `rp-hal`: - ST7789 240×320 display → `mipidsi` + `embedded-graphics` (mature; the parts are well-supported). - I²S to the PCM5102A → RP2350 PIO. - WS2812 → `ws2812-pio`. USB-MIDI → `usbd-midi` / `embassy-usb`. - GT911 touch (Kit) over I²C. ### Stage 4 — native A/B + secure boot Replace the `.mpy`-level A/B hack (`code.py` loads `app.mpy`, rolls back to `app.bak`) with the **RP2350 bootrom's native** partition-table A/B + signed boot, configured via `picotool` (the chip already provides this — see the earlier hardware discussion). The Rust app is the image in the slot; rollback and version selection move into silicon. ## What you keep / lose - **Gain:** memory safety, native A/B + secure boot, performance headroom, one typed model instead of three hand-written parsers. - **Lose:** the live one-click `.mpy` push (Rust is compile→flash→reboot). The editor's *data* live-sync (tempo/pattern/setlist mirroring) still works — it's a data protocol. Only live *logic* edits go away, and an embedded `wasm3`/script module could buy those back if wanted. ## Acceptance gate Every codec/engine change must pass `tests/fixtures/track-format.json`. The Rust crate joins `js`/`py` as a runner adapter, so "same groove on web, device, and the Rust build" is enforced, not hoped for. ## Recommendation Do **Stage 1 in a container next** — it's small, testable today (given a toolchain), reuses the suite, and produces a real artifact to judge the Rust path on before committing to drivers or a firmware rewrite. Defer Stages 3–4 until Stage 1–2 are green and you've decided the live-push tradeoff is acceptable.