From 56bff7e599911437134a6799013984a3ece71d0a Mon Sep 17 00:00:00 2001 From: Me Here Date: Wed, 3 Jun 2026 15:19:38 -0500 Subject: [PATCH] pm-grid: USB-MIDI audio + make Rust the shipping Grid firmware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audio (the Scroll Pack has no speaker, so MIDI is the only path): - usb-device 0.3 + usbd-midi 0.5 on the rp2040-hal UsbBus; enumerates as a class-compliant MIDI device 'PM_G-1 Grid'. - tick() emits a GM note-on per lane hit on channel 10 (note from the ported SOUND_GM map, velocity by level) via send_bytes([0x09,0x99,note,vel]) — raw 4-byte packets, so arbitrary GM drum notes work without the named Note enum. - USB polled every loop iteration AND during the boot splash (so the host can enumerate during the ~2.5s animation). Debug: defmt/defmt-rtt + panic-probe + flip-link; runner probe-rs run --chip RP2040 (Pi Debug Probe). build.sh emits pm-grid.uf2 + pm-grid.elf; deploy serves both; key info! log points + 1Hz heartbeat. Web: drop CircuitPython from the PM_G-1 product. info-grid.html features the Rust .uf2 download + accurate controls/views (X/Y swap, Ticker); build.sh + deploy.sh no longer bundle/serve pm_g1_circuitpy.zip or pico-scroll-app.{py,mpy}. pico-scroll/ stays as the reference port; editor FW_PATHS.G left for graceful degradation. Co-Authored-By: Claude Opus 4.8 (1M context) --- build.sh | 20 ++---- deploy.sh | 14 ++-- docs/rust-port.md | 28 +++++--- info-grid.html | 72 ++++++++++---------- rust/pm-grid/.cargo/config.toml | 10 ++- rust/pm-grid/Cargo.toml | 8 ++- rust/pm-grid/build.sh | 1 + rust/pm-grid/src/main.rs | 113 ++++++++++++++++++++++++++++---- 8 files changed, 185 insertions(+), 81 deletions(-) diff --git a/build.sh b/build.sh index 95dc66c..0c64958 100755 --- a/build.sh +++ b/build.sh @@ -23,9 +23,9 @@ echo "precompiled dist/app.mpy ($(stat -c%s dist/app.mpy) bytes <- $(stat -c%s p # bundles each ship their own precompiled binary; the served URLs follow the same one-target-per-file rule. ( cd pico-explorer && "$MPYC" app.py -o "$ROOT/dist/explorer-app.mpy" ) echo "precompiled dist/explorer-app.mpy ($(stat -c%s dist/explorer-app.mpy) bytes <- $(stat -c%s pico-explorer/app.py) source)" -# PM_G-1 "Grid" firmware (Pimoroni Pico Scroll Pack on a plain RP2040 Pico). Same mpy-cross (CircuitPython 10.2.1). -( cd pico-scroll && "$MPYC" app.py -o "$ROOT/dist/scroll-app.mpy" ) -echo "precompiled dist/scroll-app.mpy ($(stat -c%s dist/scroll-app.mpy) bytes <- $(stat -c%s pico-scroll/app.py) source)" +# PM_G-1 "Grid" ships the native Rust firmware now (rust/pm-grid → pm-grid.uf2, built by +# rust/pm-grid/build.sh and served by deploy.sh). The old CircuitPython build (pico-scroll/app.py) +# is no longer bundled or served; the source stays in-repo as the reference port. python3 - <<'PY' import os, pathlib, re @@ -69,12 +69,6 @@ assert not _xbad, "pico-explorer/app.py has non-ASCII at %r -- keep it ASCII (ve pathlib.Path("dist/pico-explorer-app.py").write_text(_xsrc) # editor reads APP_VERSION from here pathlib.Path("dist/pico-explorer-app.mpy").write_bytes(pathlib.Path("dist/explorer-app.mpy").read_bytes()) print("copied pico-explorer-app.py + pico-explorer-app.mpy") -_gsrc = pathlib.Path("pico-scroll/app.py").read_text() # PM_G-1 Grid firmware (Pico Scroll Pack) -_gbad = [(i, c) for i, c in enumerate(_gsrc) if ord(c) > 0x7F] -assert not _gbad, "pico-scroll/app.py has non-ASCII at %r -- keep it ASCII (version regex + clean source)" % (_gbad[:5],) -pathlib.Path("dist/pico-scroll-app.py").write_text(_gsrc) # editor reads APP_VERSION from here -pathlib.Path("dist/pico-scroll-app.mpy").write_bytes(pathlib.Path("dist/scroll-app.mpy").read_bytes()) -print("copied pico-scroll-app.py + pico-scroll-app.mpy") import zipfile # PM_K-1 CircuitPython drive bundle (download → unzip onto CIRCUITPY) with zipfile.ZipFile("dist/pm_k1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z: for f in ("code.py", "boot.py", "programs.json", "font_s.bin", "font_m.bin", "font_l.bin", @@ -92,11 +86,5 @@ with zipfile.ZipFile("dist/pm_x1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z z.write("dist/explorer-app.mpy", "app.mpy") z.write("dist/editor.html", "editor.html") print("zipped pm_x1_circuitpy.zip") -# PM_G-1 Grid drive bundle (Pico Scroll Pack on a plain Pico). No font/icon blobs - the LED firmware draws directly. -with zipfile.ZipFile("dist/pm_g1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z: - for f in ("code.py", "boot.py", "programs.json", "README.md"): - z.write("pico-scroll/" + f, f) - z.write("dist/scroll-app.mpy", "app.mpy") - z.write("dist/editor.html", "editor.html") -print("zipped pm_g1_circuitpy.zip") +# PM_G-1 Grid is the native Rust firmware now — no CircuitPython bundle (see rust/pm-grid). PY diff --git a/deploy.sh b/deploy.sh index 8eeca29..06515bc 100755 --- a/deploy.sh +++ b/deploy.sh @@ -63,7 +63,13 @@ fi # BOOTSEL-drag pm-grid.uf2 onto the RPI-RP2 drive to flash the PM_G-1 Grid LED metronome. if [[ -f "$SRC_DIR/rust/pm-grid/pm-grid.uf2" ]]; then cp "$SRC_DIR/rust/pm-grid/pm-grid.uf2" "$DEST_DIR/pm-grid.uf2" - echo " pm-grid.uf2 ($(stat -c '%s' "$DEST_DIR/pm-grid.uf2") bytes) # Rust RP2040 firmware (LED-first alpha)" + echo " pm-grid.uf2 ($(stat -c '%s' "$DEST_DIR/pm-grid.uf2") bytes) # Rust RP2040 firmware (the PM_G-1 Grid)" +fi +# ELF with defmt info — `probe-rs run --chip RP2040 pm-grid.elf` flashes over the Pi Debug Probe and +# streams defmt RTT logs/panics (decoded from the ELF). Served for local probe-based debugging. +if [[ -f "$SRC_DIR/rust/pm-grid/pm-grid.elf" ]]; then + cp "$SRC_DIR/rust/pm-grid/pm-grid.elf" "$DEST_DIR/pm-grid.elf" + echo " pm-grid.elf ($(stat -c '%s' "$DEST_DIR/pm-grid.elf") bytes) # probe-rs flash + defmt RTT logging" fi cp "$DIST_DIR/pm_k1_circuitpy.zip" "$DEST_DIR/pm_k1_circuitpy.zip"; echo " pm_k1_circuitpy.zip ($(stat -c '%s' "$DEST_DIR/pm_k1_circuitpy.zip") bytes)" # PM_K-1 CircuitPython bundle cp "$DIST_DIR/pico-cp-app.py" "$DEST_DIR/pico-cp-app.py"; echo " pico-cp-app.py ($(stat -c '%s' "$DEST_DIR/pico-cp-app.py") bytes)" # served for version reading + reference @@ -71,9 +77,9 @@ cp "$DIST_DIR/pico-cp-app.mpy" "$DEST_DIR/pico-cp-app.mpy"; echo " pico-cp-app. cp "$DIST_DIR/pm_x1_circuitpy.zip" "$DEST_DIR/pm_x1_circuitpy.zip"; echo " pm_x1_circuitpy.zip ($(stat -c '%s' "$DEST_DIR/pm_x1_circuitpy.zip") bytes)" # PM_X-1 Explorer CircuitPython bundle cp "$DIST_DIR/pico-explorer-app.py" "$DEST_DIR/pico-explorer-app.py"; echo " pico-explorer-app.py ($(stat -c '%s' "$DEST_DIR/pico-explorer-app.py") bytes)" # served for version reading cp "$DIST_DIR/pico-explorer-app.mpy" "$DEST_DIR/pico-explorer-app.mpy"; echo " pico-explorer-app.mpy ($(stat -c '%s' "$DEST_DIR/pico-explorer-app.mpy") bytes)" # PM_X-1 firmware (the editor pushes this when device id = X) -cp "$DIST_DIR/pm_g1_circuitpy.zip" "$DEST_DIR/pm_g1_circuitpy.zip"; echo " pm_g1_circuitpy.zip ($(stat -c '%s' "$DEST_DIR/pm_g1_circuitpy.zip") bytes)" # PM_G-1 Grid CircuitPython bundle -cp "$DIST_DIR/pico-scroll-app.py" "$DEST_DIR/pico-scroll-app.py"; echo " pico-scroll-app.py ($(stat -c '%s' "$DEST_DIR/pico-scroll-app.py") bytes)" # served for version reading -cp "$DIST_DIR/pico-scroll-app.mpy" "$DEST_DIR/pico-scroll-app.mpy"; echo " pico-scroll-app.mpy ($(stat -c '%s' "$DEST_DIR/pico-scroll-app.mpy") bytes)" # PM_G-1 firmware (the editor pushes this when device id = G) +# PM_G-1 Grid is the native Rust firmware now (pm-grid.uf2 above) — the CircuitPython grid +# artifacts (pm_g1_circuitpy.zip / pico-scroll-app.{py,mpy}) are no longer built or served. +rm -f "$DEST_DIR/pm_g1_circuitpy.zip" "$DEST_DIR/pico-scroll-app.py" "$DEST_DIR/pico-scroll-app.mpy" # remove stale Python grid downloads rm -f "$DEST_DIR/player-asbuilt.html" # renamed to teacher.html rm -f "$DEST_DIR/concepts.html" # Concepts is now the landing (/) # info-*.html are first-class pages again: each form factor has a lean widget page diff --git a/docs/rust-port.md b/docs/rust-port.md index 6865581..93f594d 100644 --- a/docs/rust-port.md +++ b/docs/rust-port.md @@ -147,12 +147,16 @@ On `embassy` / `rp-hal`: - WS2812 → `ws2812-pio`. USB-MIDI → `usbd-midi` / `embassy-usb`. - GT911 touch (Kit) over I²C. -#### `pm-grid` — Scroll Pack firmware 🟢 BUILT (LED-first milestone), pending on-device check -The Rust sibling of `pico-scroll/app.py` (`rust/pm-grid/`). Target is a **plain RP2040** (Cortex-M0+, +#### `pm-grid` — Scroll Pack firmware 🟢 BUILT (LED + USB-MIDI audio), pending on-device check +The Rust sibling of `pico-scroll/app.py` (`rust/pm-grid/`), and **the firmware the PM_G-1 ships now** — +the CircuitPython build is dropped from the product (info-grid.html / build.sh / deploy.sh no longer +bundle or serve it; source stays as the reference port). Target is a **plain RP2040** (Cortex-M0+, `thumbv6m-none-eabi`) — NOT the Pico 2 — so it has its own HAL (`rp2040-hal` 0.10 + `rp2040-boot2`), `.cargo/config.toml`, `memory.x` (BOOT2 + flash + 264 KB RAM) and `build.sh`+`uf2.py` (RP2040 family -id `0xe48bff56`). `thumbv6m-none-eabi` added to `rust/Containerfile`. Compiles clean → **48 KB -`pm-grid.uf2`** (BOOTSEL drag-flash; no probe needed). Kept out of the host workspace like `pm-kit`. +id `0xe48bff56`). `thumbv6m-none-eabi` added to `rust/Containerfile`. Compiles clean → **`pm-grid.uf2`** +(BOOTSEL drag-flash) + **`pm-grid.elf`** (probe-rs + defmt). Both served by deploy.sh; the info page +links the `.uf2`. Kept out of the host workspace like `pm-kit`. Debug build uses `defmt`/`defmt-rtt` + +`panic-probe` + `flip-link`, runner `probe-rs run --chip RP2040` (the user's Pi Debug Probe). What's implemented (faithful port of `pico-scroll`): the **IS31FL3731 driver** (vendored bulk 144-byte framebuffer, one I²C block write per frame — the right architecture, mirrors the @@ -170,10 +174,18 @@ list; X/Y=tempo ∓ with auto-repeat), the **built-in set lists**, and three LED `_render_grid` / `_render_pendulum`. Boot splash scrolls "PM-G1 GRID" (liveness + pixel-map check). -**Deferred to the next milestone** (matches pm-kit's order — none built yet): **USB-MIDI** note-out -(the Scroll Pack has NO speaker, so this is the real audio path) + MIDI clock, **live-sync SysEx** -(0x40-0x43 + version query), firmware push (0x10/0x21-0x23), on-device practice log, settings.json, -and playback-flow auto-advance (`rep`/`end`/continue). An optional piezo on a free GPIO is also TODO. +**Audio — USB-MIDI ✅ DONE** (the Scroll Pack has NO speaker, so this is the real audio path): +`usb-device` 0.3 + `usbd-midi` 0.5 (the `rp2040-hal` `UsbBus`). Enumerates as a class-compliant MIDI +device ("PM_G-1 Grid"); `tick` emits a **GM note-on per lane hit on channel 10** (note from the ported +`SOUND_GM` map, velocity by level 120/90/45) via `UsbMidiClass::send_bytes([0x09,0x99,note,vel])` — +raw 4-byte packets, sidestepping the named-`Note` enum so arbitrary GM drum notes work. USB is polled +every loop iteration **and during the boot splash** (1.5 ms cadence) so the host can enumerate. Play +through the editor's **Device audio**. + +**Still deferred**: MIDI clock in/out, **live-sync SysEx** (0x40-0x43 + version query), firmware push +(0x10/0x21-0x23), on-device practice log, settings.json, playback-flow auto-advance (`rep`/`end`/ +continue), optional piezo. Note: without the SysEx version query, the editor's firmware-push won't +target the Grid (intended — it's UF2-flashed now). ### 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 diff --git a/info-grid.html b/info-grid.html index d03ae68..c230135 100644 --- a/info-grid.html +++ b/info-grid.html @@ -4,7 +4,7 @@ VARASYS PM_G-1 Grid - wiring, parts & firmware (Pimoroni Pico Scroll Pack / RP2040) - +