Switch the firmware from the bring-up diagnostic to pm_ui::draw_metronome with
static borrowed lane data (no allocator yet). Shows the actual metronome UI on the
device; live track + moving playhead come when pm-core is linked.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
draw_metronome() renders the screen for any parsed track: track name + big BPM,
play/stop transport, and the polymeter lane grid — per-lane beat cells coloured by
level (accent amber / normal cyan / ghost purple / rest dark), playhead highlight,
beat gridlines, poly (~) marker. Pure no_std view over borrowed data (LaneView/
Screen) so the firmware build stays allocator-free.
uisim now parses a real track (track-format) and renders draw_metronome to PNG —
iterate the UI on the host, no bench. Firmware still draws the bring-up diagnostic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reading mipidsi's interface/spi.rs: send_command writes the command byte and its
parameters as TWO separate SpiDevice transactions, so a normal SpiDevice de-asserts
CS between them. The ST7796 needs CS continuous across command+parameters, so
MADCTL/COLMOD/B6 args never loaded → default scan/orientation → 1/4 + rotated
(parameter-less commands and the pixel stream still worked, which is why it lit up).
CircuitPython's FourWire holds CS low for the whole command; replicate that: drive
the real CS (GP5) low for the session and give ExclusiveDevice a no-op CS. DC alone
selects command vs data.
Diagnosed entirely on the host: panelsim (new) decodes mipidsi's actual command/
pixel stream into a PNG and rendered perfectly, proving the protocol was right and
the bug was in the physical SPI/CS layer — then the driver source confirmed it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Host initdump proved mipidsi's MADCTL (0x48), COLMOD, and address window
(CASET 0..319 / RASET 0..479) already match CircuitPython exactly — so the 1/4
+ rotation wasn't an addressing bug. The missing piece was the ST7796 extension
init (B6/power/gamma) running as the PRIMARY bring-up right after reset (grafting
it onto mipidsi's already-DISPON'd panel blanked or under-configured it).
Now: manual hw reset + full CircuitPython st7796_init via the raw interface, THEN
Builder without reset_pin (re-asserts only the basics, extension setup persists).
initdump extended to also dump CASET/RASET draw windows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 0xF0 extension unlock (gates gamma/power) was the likely blanker; 0xB6 is a
basic command and needs no unlock. Strip to just DISPOFF -> 0xB6(480 lines) ->
DISPON, the one change vs mipidsi's working-but-1/4 baseline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Host initdump (rust/uisim --bin initdump) showed mipidsi emits only SLPOUT,
MADCTL=0x48, INVON, COLMOD, NORON, DISPON — MADCTL already matches CircuitPython,
but the ST7796 extension setup (unlock, 0xB6 480-lines, power, gamma) is missing,
and sending it AFTER mipidsi's DISPON blanked the live panel. Replay the full
known-good st7796_init via Display::dcs() ending in its own DISPON. Adds the
initdump tool (capture init byte sequence on the host, no bench).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Answer to 'can you simulate it?': the UI now renders on the host.
- pm-ui: shared no_std embedded-graphics drawing (draw_ui), used by BOTH the
firmware and the simulator — single source, no divergence.
- uisim: host crate that draws pm-ui onto a framebuffer and exports a PNG (pure
Rust, no SDL). Confirmed the bring-up pattern renders correctly off-device, so
the black screen is a panel/controller issue, not a draw bug.
- pm-kit: use pm_ui::draw_ui; trim the ST7796 extension init to just unlock + 0xB6
(the gamma/VCOM sent after DISPON likely blanked it); LED solid during init then
slow 1 Hz blink so hung-init / running / reset-loop are distinguishable.
Note: the simulator covers WHAT we draw (layout/colour/logic). It does NOT model
the ST7796 controller's hardware quirks (0xB6 line count, MADCTL scan, SPI init) —
those still need the bench, but that's a one-time bring-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mipidsi's ST7796 model uses the ST7789 init, which skips the ST7796-specific
extension-command unlock (0xF0) and Display Function Control (0xB6 = 480 driving
lines) — so the panel only scanned part of the screen (image confined to a corner
region + snow). After mipidsi's init, send the missing commands via Display::dcs()
using the known-good values from the CircuitPython st7796_init.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace clear() with same-path full-screen fill, add a 4-edge red border, four
distinct corner markers (TL green / TR yellow / BL cyan / BR magenta) and a TL
label, to pin down rotation/mirror/size from one flash. Apply flip_horizontal to
match the panel's MADCTL MX bit (CircuitPython uses 0x48).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Init the Kit's ST7796 320x480 over SPI0 (SCK=GP2, MOSI=GP3, CS=GP5, DC=GP6,
RST=GP7; BGR, colours inverted, 16 MHz) via mipidsi 0.9 + embedded-graphics, and
draw a panel + "PM-KIT / RUST OK" so SPI + the graphics stack are verifiable on
screen. GP25 LED keeps blinking as a heartbeat.
Compiles for thumbv8m; runtime (does it draw? colours/orientation right?) is the
on-device check. Next: tune orientation/colour if needed, then inputs + audio + pm-core.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First per-board binary. rust/pm-kit/ is a minimal rp235x-hal firmware that blinks
GP25 on the Pico 2 — proves the toolchain, RP2350 boot block (ImageDef), memory
layout, and flash before we add any drivers.
- src/main.rs + memory.x + build.rs + .cargo/config.toml: rp235x-hal blink, builds
for thumbv8m.main-none-eabihf.
- build.sh + uf2.py: one command builds the ELF in the container, objcopies to a raw
image, and packs pm-kit.uf2 (rp2350-arm-s family). Drag onto the Pico 2 in BOOTSEL.
Verified: builds clean; produces a valid 6-block UF2. Runtime (does it blink?) is the
on-device check. Next: drivers (display first) + link pm-core.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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>
A third implementation of the track DSL alongside engine.js and app.py, validated
against the same tests/fixtures/track-format.json:
- rust/track-format/: pure parse()/serialize() codec (std + alloc for now; no_std is
a later refinement). Ports the app.py/engine.js semantics exactly — grouping,
subdivisions, swing, ghost, polymeter, euclid, GM note-number aliases, unknown->beep,
default groove (group-start accents), tempo clamp, empty->beep, and the playback-flow
tokens (rep/end/relative-goto). Carries vol/cd too, so it's the most spec-complete
of the three.
- tests/conformance.rs: the Rust adapter — reads the shared fixtures, asserts each
case's normalized form (number-tolerant deep-equal) + serialize idempotency.
- rust/Containerfile + run.sh: Rust toolchain in a container (mirrors hardware/eda/),
with the thumbv8m.main-none-eabihf target for the eventual RP2350 firmware. Never
on the host.
Verified: ./rust/run.sh -> cargo test -> conformance + idempotent both pass.
docs/rust-port.md Stage 1 marked done.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>