Landing (index.html):
- Remove the PM_E-1 (editor.html) viewport; default the live viewport to PM_E-2.
- Repoint the vp-bar + the "design in the editor" link to pm_e-2.html. editor.html
still exists, just not featured on the landing.
PM_E-2 editor (pm_e-2.html) - the device-connection badge + Device-audio toggle:
- Group both in the header as matching .devctrl pills, side by side.
- Clear tooltips spelling out exactly what each does: the badge only REPORTS the
USB-MIDI link (green + name when a PM device is plugged in); Device audio is an
on/off switch that routes a connected device through the computer speakers and
does not require a device to toggle.
- Device-audio button now shows on/off state via colour (green when on), matching
the badge, instead of the .primary class (which clashed with the pill style).
- Fix _isDevicePort: it only matched pico/circuitpython/pimoroni/varasys, so the
native Rust devices ("PM_G-1 Grid" etc.) were never recognized -> the badge
stayed "no device" even when connected. Now matches pm_g/pm_k/pm_x/grid/polymeter.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Mirror the pm-kit.uf2 block — copy rust/pm-grid/pm-grid.uf2 to the web root if
built, so it downloads from metronome.varasys.io/pm-grid.uf2.
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>
- Split Kit (ST7796/SPI, custom port — mipidsi dropped) from Explorer
(ST7789V/8080 parallel) — they were wrongly lumped as 'ST7789 via mipidsi'.
- Add researched display-driver matrix (crates + status per controller).
- Mark Milestone 2 display 🟡: draws but TEARS; mipidsi confirmed to have
no TE/vsync/double-buffer support, module exposes no TE pin -> open item.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
editor.html + pm_e-2.html called _ensureMidi() (requestMIDIAccess{sysex:true}, which always
prompts) on page load. Gate it behind a permission query — only auto-reconnect if MIDI is
already granted (querying does not prompt); otherwise wait for the user to click the connect
badge / Device-audio button. (editor-beta.html already had no on-load MIDI call.)
Add the brand lockup to the device .appheader (before the PM_E-1 title) and hide it in
.site-head, leaving the header with just nav (pushed right). Device logo sized to 30px.
Applies to editor.html + editor-beta.html.
Editor #app and .device were capped (1400/1000px) and the shared header at 980px, so on
wide screens the logo sat inset from the editor's left edge and content floated narrow in a
wide page. Drop the caps (fill to the 24px body padding) and widen .site-head/.site-foot to
match — logo now lines up with the editor's left edge and the editor uses the full width.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Validated against GeeekPi's lv_port_disp.c (vendor LVGL driver for this kit): it flushes
per-dirty-rectangle windowed writes at 62.5MHz, no TE — exactly this approach. Fix blit_rect
(1024-byte buffer; the 512-byte one was the black-screen bug), switch incremental updates from
full-width 144-row bands to changed 40x40 tiles. Full repaints still use the proven full blit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ST7796S datasheet §10.8: panel rescans GRAM at 60Hz; tear-free writes require feeding
the TE output back to the MCU and writing during vblank. EP-0172 prototype wires no TE,
so large updates tear (small writes hide it — why MP/CP look clean). Add TE (e.g. GP4)
+ optional MISO to the face interconnect for the production board.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace mipidsi with a direct port of pico/main.py's ST7796 driver (per-command CS,
MADCTL 0x48, INVON, no offset, big-endian RGB565) — fixes the geometry/colour mangling.
Render the UI into a 300KB RAM framebuffer (embedded-graphics), then push only the
full-width row bands that changed vs the last frame (FNV row-diff) — flicker-free and
~144/480 rows per playhead step. Full-width windows only: the panel's MX bit mirror-maps
sub-width windows wrongly (that was the black screen). SPI 62.5 MHz per pico-cp to shrink
the tearing window (no TE pin is routed on this board, so hardware vsync isn't available).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per the approved audio re-architecture (prototype-first): prove the click -> codec ->
transformer-isolated XLR chain on bought boards before any custom PCB, keeping the RP2350
firmware.
- pico-wm8960/code.py: CircuitPython bring-up for Pico 2 + SparkFun WM8960 breakout.
Synthesizes the click (familiar piezo pitches) -> I2S -> WM8960 -> HP/speaker; line-in
monitor hook; stereo/pan ready for polymeter spatialization. Uses the proven adafruit_wm8960
driver (no hand-rolled register driver).
- hardware/PROTOTYPE.md: shopping list, wiring, and bench milestones M1-M5 (M4 = the no-buzz
ground-loop test = acceptance gate).
Key findings baked in:
- Buzz was a ground loop; cure = transformer galvanic isolation, NOT +/-15 V (which was only
studio headroom and is dropped).
- WM8960 needs MCLK (CircuitPython I2SOut doesn't emit it); the SparkFun breakout's onboard
24 MHz oscillator supplies it -> resolves Risk R1 with zero extra parts.
Track-format conformance (node tests/run.mjs) stays green; DSL untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verified workflow (not first-principles): host udev rules with uaccess ACL (maps by UID
into the toolbox; group access shows nobody:nobody), prebuilt probe-rs in the toolbox,
probe-rs run --chip RP235x pm-kit.elf for flash+RTT defmt streaming.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopt proper embedded tooling for the blank-screen debug (user has a Pi Debug Probe):
- flip-link linker (baked into pm-rust:2): stack overflow faults cleanly instead of
silently corrupting .bss/.data (the SPI buffer -> black screen class of bug).
- defmt + defmt-rtt + panic-probe: firmware logs boot/heap-free/display/parse/loop
heartbeat over RTT; panics print message+location. .cargo runner = probe-rs run.
- Restore the full live metronome (from 08b0940) as the instrumented target.
- deploy + serve pm-kit.elf (probe-rs decodes defmt strings from the ELF).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
If blue → the 96KB heap memory was colliding (stack/buffer). If still black → the
allocator's presence itself. Narrowing the heap/display interaction.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diagnosing the blank screen: strips everything but the heap init, one allocator
exercise (parse), and the confirmed-working draw_ui. Blue+corners => heap/parse/
display fine, bug is the metronome loop. Black => heap/parse breaks the display.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Temporary: draw_ui (blue + corners) instead of draw_metronome in the loop, keeping
heap+audio+inputs. Blue+corners shown => display/heap fine, draw_metronome is the
bug. Still black => heap/display path. Revert after diagnosis.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blank-screen fix attempt: the single boot-time draw (right after the init DISPON)
was likely lost; the peripheral test worked because it redrew every frame. Redraw
on change AND every ~140ms. (Audio timing checked between redraws; partial/playhead
redraw is the next step to tighten it.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The firmware is now an actual metronome (not a static screen):
- embedded-alloc heap → parses tracks with track-format on-device.
- 4 built-in grooves; clock-driven from the Timer; audio clicks on the master
lane's hits via the GP13 PWM (accent louder/longer), short edge-triggered pulses.
- Controls: A = play/stop, B = grid/notation view; joystick (rotated 90° CCW)
up/down = tempo +/-, left/right = prev/next groove.
- Renders draw_metronome or draw_notation; a cheap draw_progress strip animates the
bar position every frame (full redraw only on change → no flicker).
- Robust: all input reads use unwrap_or (no panics in the loop) — addresses the
self-test crash (likely an ADC unwrap on WouldBlock) and the continuous-buzzer.
Compile + simulator verified (grid renders all 4 grooves incl. triplets/polymeter).
NEEDS ON-DEVICE CHECK: audio timing, joystick directions, and that the crash is gone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gather notes per time-column across lanes; draw one shared stem per voice (hands
up / feet down) spanning the chord, so snare+hat on a beat share an up-stem. Ledger
lines for notes above/below the staff (hi-hat on its ledger line, crash higher).
Stems always clear the highest/lowest notehead; beams grouped per beat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
draw_notation() renders a bar as standard drum notation: 5-line staff + time
signature, voices mapped to staff positions and notehead types (oval drums,
cross hi-hat/cymbals), hands stem-up / feet stem-down, beamed eighths/sixteenths
grouped per beat, accents tinted. Developed entirely in the simulator
(uisim --bin notesim → PNG). Firmware build unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Honest answer to 'do the inputs/speaker work?': they had NO Rust code. Add the
drivers and a live self-test: buttons GP15/GP14 (pull-up), joystick GP26/GP27 via
ADC, speaker GP13 via PWM (~2 kHz click on button press). draw_peripheral_test
(pm-ui) shows button states, joystick dot + X/Y values, and beep activity; layout
verified in the simulator (uisim --bin periphsim) before flashing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>