Move the 3x5 LED font (DIGITS, glyph, build_name_cols) into src/fonts.rs.
Pure code move, compiler-verified identical behavior; main.rs 1835 -> ~1770 lines.
First step of the recommended main.rs split; further extraction (FAT/MSC storage,
views) to continue incrementally as those areas are touched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit for panic/brick risks (a panic = black screen on this device):
- sx_send (live-sync broadcasts + 5s heartbeat) pushed to tx_q with no cap. If
the editor disconnects without a BYE while sync_armed and nothing drains
MIDI-IN, tx_q grows unbounded -> heap exhaustion -> brick. Now drops messages
when tx_q > 256 (the heartbeat re-syncs when the host returns). Notes/clock
were already capped.
- build_setlists now drops empty set lists, so load()/next_track() can never
hit a `% 0`. (parse guarantees >=1 lane; built-ins/parsed lists are non-empty,
this is belt-and-suspenders.)
Other unwrap()s are boot-time peripheral init; lanes[0]/items[0]/step[0] are all
safe (parse substitutes beep:4 for empty programs; built-ins lead the list).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the host writes the drive (SCSI Write sets a dirty flag) and the drive has
been idle ~1.5s AND playback is stopped, the loop re-reads programs.json and
rebuilds the set lists (reload_user) -> a dropped file applies without a reboot.
Read-only path (split read_programs_json out of read_user_setlists; the format
flash-write only happens at boot), so no FAT-corruption risk from dual access.
Note on the recommended write path: the device deliberately does NOT write the
shared FAT while the host has it mounted (that corrupts the host cache - same
reason CircuitPython is one-direction-at-a-time). The practice log should instead
go to the editor via LOGSYNC (0x45); settings.json *read* (device read-only) is a
safe follow-up. Documented in docs/rust-port.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The drive-read at boot bricked the display (barely blinked, then black).
Likely the new fatfs + owned set lists exhausted the 24KB heap (alloc panic ->
halt before the splash). Three fixes:
- Heap 24KB -> 96KB (Pico has 264KB).
- format_pmg1 writes one 4KB sector per call (the proven MSC write pattern)
instead of a single 7-sector erase+program.
- Run read_user_setlists AFTER the splash, so a FAT/flash failure can no longer
leave the screen black; added defmt logs around it to localize any remaining
failure over the probe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On boot (before USB setup, so the flash write cannot disrupt enumeration) the
device mounts the Mass Storage FAT volume and uses it:
- fatfs 0.4 (git rev c4b88477; 0.3.6 needs core_io for no_std) via a read-only
FlashIo over the .filesystem region (reads flash through a black_box ptr).
- If the root-dir volume label is not "PM_G-1" (e.g. a leftover CircuitPython
volume), write an embedded blank PM_G-1 FAT12 template (src/fat_template.bin =
first 7 sectors of mkfs.fat -F12 -S4096 -n PM_G-1; sets both BPB + root-dir
VOLUME_ID label) -> the drive now shows as PM_G-1.
- Read programs.json (LFN) and a tolerant scanner (parse_setlists) turns it into
user set lists appended to the built-ins. Drop programs.json on the drive,
reboot, your grooves appear (B-hold cycles set lists).
Set lists are now a runtime Vec<SetList>{title,items} (built-ins -> owned +
drive); refactored load/next_track/next_setlist/goto_target/prepare_next/sel.
Validated off-bench: a host probe ran fatfs against a real mkfs 4096-sector image
(label + programs.json read confirmed) before flashing.
WRITE-from-device (practice log / settings) is still deferred (the read path is
in; needs a write-back FlashIo).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adapt the usbd-storage rp2040 example into pm-grid as a composite MIDI+MSC
device:
- Host sees a 1MB removable drive backed by the upper 1MB of flash (a
.filesystem region, NOLOAD so it stays out of the UF2 and survives reflashes).
- scsi_command handles the SCSI set (Inquiry / ReadCapacity10/16 /
ReadFormatCapacities / Read / Write / ModeSense / RequestSense / TestUnitReady).
Reads come from flash via raw pointer; writes accumulate a 4KB block then
erase+program the sector with rp2040-flash (wrapped in interrupt::free).
- Host owns the FAT format (formats on first use). Unblocks on-device persistence.
- Composite poll: usb_dev.poll([&mut midi, &mut scsi]); scsi.poll services commands.
Build fixes required by adding rp2040-flash:
- rp2040-hal 0.10 -> 0.11 (0.10 + rp2040-flash 0.6 both export the __aeabi_*/
__addsf3 ROM intrinsics -> duplicate symbols). No HAL API breakage.
- lto = false + codegen-units = 1 (fat-LTO tripped the same duplicate intrinsic).
UF2 stays ~257KB thanks to NOLOAD. defmt logs on block writes + unknown commands.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- feed_midi (was feed_sx) now also handles realtime: 0xF8 tick -> slave_tick
(EMA of the inter-tick interval -> derived BPM, 5..300 clamp, jitter reject),
0xFA/0xFB -> start, 0xFC -> stop. RX loop feeds CIN 0xF single-byte packets too.
- While slaved: the tempo ramp and our own clock-OUT are suppressed (no feedback);
the lock drops after a >1s gap in incoming ticks.
- Default on; only engages when a host actually sends clock (the editor does not).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Playback flow (rep/end), ported from pico-scroll:
- At each master-bar boundary, after bars*rep cycles the end-action fires:
end=stop stops; end=next / end=+N advances through the set list.
- The next track is preloaded one bar early (parsed + per-lane durs) into a
pending slot, then swapped at the exact seam (master lane bar boundary; all
lanes restart there) for a gapless handoff. load()/manual nav clears pending.
MIDI clock out (default on, so a DAW can slave to the Grid):
- 24-PPQN 0xF8 against the wall clock + 0xFA/0xFC Start/Stop on play/stop (button
or live-sync). Queued on tx_q as CIN 0xF single-byte packets.
Deferred items needing persistent storage (no CIRCUITPY drive in the Rust build,
needs a flash KV layer - separate milestone): practice log, settings.json,
SLSYNC/LOGSYNC. Also deferred: MIDI clock in, optional piezo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port pico-scroll's live-sync to Rust (docs/livesync-protocol.md):
- Reassemble SysEx from incoming USB-MIDI 4-byte packets (by Code Index Number);
dispatch manufacturer 0x7D frames.
- Version query 0x02 -> 0x03 'G;0.1.0' (editor now identifies the device).
- HELLO 0x40 -> reply FULL; FULL 0x41 -> parse patch + running and adopt it;
DELTA 0x42 -> apply play/stop/bpm/sel/beat; BYE 0x43 -> disarm.
- Broadcast a DELTA from each on-device input (play/stop, sel, bpm) + a FULL
heartbeat ~5s (track-format::serialize). Echo-guarded by a boot-derived origin;
sync_applying flag suppresses re-broadcast while applying.
- Unify all USB-MIDI TX (notes + SysEx) onto one tx_q drained one-per-poll.
- defmt info! on every received op for probe debugging.
Structural lane= edits aren't applied incrementally (arrive as a fresh FULL).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bulk MIDI endpoint holds one 4-byte packet until the host reads it (~once
per USB frame), so calling send_bytes twice for simultaneous lane hits dropped
the 2nd note (WouldBlock, silently ignored). Queue note-ons in a VecDeque and
drain one-per-poll, keeping the rest for the next iteration — chords now play in
full (staggered ~1ms, imperceptible) instead of all-but-one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Free the top row for a beat indicator: faint ticks at each beat (every sub steps)
across cols 0-10 with a bright playhead at the master lane's current step. The
scrolling name moves down to rows 2-6 (row 1 = separator). BPM block unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Swap X/Y: X now tempo-up, Y tempo-down (match the physical Scroll Pack layout).
- On the master lane's step 0 (the '1'), flash the entire 17x7 matrix at full
brightness for 80ms — a visual downbeat strobe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rust sibling of pico-scroll/app.py — the PM_G-1 'Grid' 17x7 LED metronome on a
plain RP2040 Pico (thumbv6m, not the Pico 2). LED-first milestone:
- IS31FL3731 driver: vendored bulk 144-byte framebuffer, one I2C block write per
frame (port of the CircuitPython Matrix; the is31fl3731 crate isn't used).
- Polymeter scheduler driven by track-format::schedule::lane_durs (the cross-impl
contract) + per-lane step clocks + tempo ramp + gap-trainer.
- 4-button input (A play/stop·hold=view, B next-track·hold=next-setlist, X/Y tempo).
- Built-in set lists; 3 views: Ticker (default), Grid, Pendulum.
- Ticker (user-designed): name infinite-scrolls left; BPM pinned right rotated 90
CCW = hundreds dot-bar (1 dot/100) + last 2 digits rotated. 130 -> 1 dot + '30'.
- Build scaffolding: rp2040-hal 0.10 + boot2, memory.x, build.sh + uf2.py (RP2040
family id). thumbv6m-none-eabi added to rust/Containerfile. Excluded from the
host workspace like pm-kit. Compiles clean -> 48 KB pm-grid.uf2.
Audio (USB-MIDI; the board has no speaker), live-sync, firmware push, practice log
and playback-flow auto-advance are deferred to the next milestone (as on pm-kit).
Also: delete COORDINATION.md (solo now); docs/rust-port.md updated with pm-grid
status + corrected Grid driver-matrix row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>