- schedule.rs: ports the firmware's durs/timeline math (app.py tick/_prepare_next). render(track, bars) yields the deterministic click timeline; tests/schedule.rs asserts quarter-note spacing, subdivisions, swing 2/3:1/3, polymeter 5:4, accents/ghosts, mute, and multi-bar looping. All green on the host. - The crate is now #![no_std] + alloc and builds for thumbv8m.main-none-eabihf, so the codec + scheduler are firmware-ready (verified: cargo build --lib --target thumbv8m.main-none-eabihf). ./rust/run.sh -> 9 tests pass (2 conformance + 7 schedule). docs/rust-port.md updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
81 lines
4.6 KiB
Markdown
81 lines
4.6 KiB
Markdown
# 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.
|
||
|
||
## 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)
|
||
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.
|