Commit graph

311 commits

Author SHA1 Message Date
Me Here
34e0d24aad pm_e-2: MIDI out — drive external gear (drum module / e-kit) from the editor
engine.js: add an opt-in per-hit hook in scheduleMeterTick — onMeterHit(sound,
time, lvl) — called only if a page defines it (no-op everywhere else). Lets a
page emit MIDI per scheduled hit, in lockstep with the audio scheduler.

pm_e-2.html: a "🎛 MIDI out" header toggle + output-port picker. When on, each
groove hit is sent as a GM drum Note-On (channel 10; note from SOUND_GM e.g.
kick*->36, snare*->38, hat*->42; velocity by accent/normal/ghost) to the chosen
port via output.send([..], ts) with a timestamp derived from the hits audio time
(performance.now() + (time - audioCtx.currentTime)*1000) for tight sync; a note-off
follows 60ms later. Port list prefers a non-PM output (the external gear) and
refreshes on MIDI connect/disconnect. Independent of the local synth + Device
audio; goes green (blue) when on. Web MIDI (Chrome/Edge/Firefox).

Conformance suite still green (engine.js change is in the scheduler, not the codec).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:41:01 -05:00
Me Here
964dee01d6 pm-synth: port engine.js 808/909/GM voices to Rust + host wav renderer
pm-synth: a polyphonic drum-voice synth, a faithful f32 port of engine.js DRUMS
(tone/ampEnv/v_noise/metalHat/clap recipes; RBJ biquads; exp envelopes). A Synth
mixes active Voices sample-by-sample (transport-agnostic: offline render now,
real-time device buffer fills later). All 808/909 + GM voices ported.

synthrender (host bin): parse a groove -> track-format schedule -> trigger voices
at click times -> 16-bit/48k mono WAV. Applies the editor default kit (kick->
kick909 etc.). Renders four demo grooves to audition off-bench.

This is the reusable half of the audio feature; the device port (no_std +
fixed-point/table osc, since the M0+ has no FPU) comes with the transport.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:34:31 -05:00
Me Here
15392174aa web: hide PM_E-1 from landing (focus PM_E-2); clarify pm_e-2 device controls
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>
2026-06-04 12:23:15 -05:00
Me Here
5f7c85d910 docs(rust-port): record the pm-grid hardening pass (tx_q cap, fonts split) 2026-06-04 11:24:28 -05:00
Me Here
bdf69cfd30 pm-grid: start main.rs modularization (extract fonts module)
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>
2026-06-04 11:24:09 -05:00
Me Here
72dbb2ecd0 pm-grid: hardening - bound the USB-MIDI TX queue + defensive set-list guard
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>
2026-06-04 11:21:06 -05:00
Me Here
dd27d553fe pm-grid: live re-read of programs.json (no reboot needed)
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>
2026-06-04 10:19:43 -05:00
Me Here
219fb267a0 pm-grid: harden FAT-read boot (fix black screen regression)
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>
2026-06-04 07:35:28 -05:00
Me Here
768ec0021f pm-grid: name the drive PM_G-1 + read set lists from it (on-device FAT)
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>
2026-06-04 07:29:30 -05:00
Me Here
394ae65eaf pm-grid: USB Mass Storage (composite MIDI + 1MB flash-backed drive)
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>
2026-06-03 16:25:51 -05:00
Me Here
dce51866d2 docs(rust-port): mark MIDI clock in done 2026-06-03 16:08:29 -05:00
Me Here
d035ee2a06 pm-grid: MIDI clock in (slave tempo to external 24-PPQN clock)
- 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>
2026-06-03 16:07:55 -05:00
Me Here
36989c96de pm-grid: playback-flow auto-advance (rep/end) + MIDI clock out
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>
2026-06-03 15:50:20 -05:00
Me Here
7e2a3b181b pm-grid: live-sync over USB-MIDI SysEx (editor <-> device)
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>
2026-06-03 15:40:23 -05:00
Me Here
47ffb46aa2 pm-grid: fix dropped notes in chords (queue USB-MIDI packets)
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>
2026-06-03 15:31:07 -05:00
Me Here
56bff7e599 pm-grid: USB-MIDI audio + make Rust the shipping Grid firmware
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>
2026-06-03 15:19:38 -05:00
Me Here
512890baa2 pm-grid Ticker: top-row beat strip + shift name down a row
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>
2026-06-03 14:57:45 -05:00
Me Here
e46ff02c0c pm-grid: swap X/Y tempo buttons + full-screen strobe on the downbeat
- 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>
2026-06-03 14:52:04 -05:00
Me Here
86cd4a0242 deploy.sh: serve pm-grid.uf2 (Pico Scroll Pack firmware)
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>
2026-06-03 14:42:38 -05:00
Me Here
604927f53a Add pm-grid: Rust firmware for the Pico Scroll Pack (RP2040)
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>
2026-06-03 07:22:48 -05:00
Me Here
713770232d docs(rust-port): correct display driver story + flag ST7796 tearing
- 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>
2026-06-03 06:47:29 -05:00
Me Here
f564b75b1e COORDINATION: solo now; all prior-agent work verified + committed 2026-06-02 13:47:20 -05:00
Me Here
5dcef691c1 Add untracked notation deliverables (build/compile depend on these)
- src/notation.js — web notation engine inlined into pm_e-2.html (@BUILD:include)
- rust/pm-ui/src/notation/ — the notation module pm-ui/lib.rs imports
- rust/glyphgen/ + rust/assets/bravura/ (Bravura.otf + OFL.txt) — host atlas generator + font src
- rust/Cargo.toml (workspace) + rust/.gitignore
- assets/bravura.woff2.b64 (web font subset, @BUILD:bravura@) + info-pm_e-2.html

Without these a clean checkout couldn't build pm_e-2.html or compile pm-ui. (Left hardware/eda
make_svg* + kicad/_svgtest.json untracked — unrelated scratch.)
2026-06-02 13:46:45 -05:00
Me Here
cb54b4d689 Preserve notation + grammar feature work (verified complete + green)
The parallel agent's full session, committed now that it's solo:
- Grammar: flam/drag/roll ornaments (f/F d/D z/Z, per-lane orns channel) across
  src/engine.js, pico-cp/pico-explorer/pico-scroll app.py, pico/main.py, rust/track-format,
  + golden vectors / conformance (tests/, rust/track-format/tests).
- Live-sync deep-sync: SysEx 0x44 SLSYNC + 0x45 LOGSYNC (docs/livesync-protocol.md, src/livesync.js).
- PM_E-2 notation: web engine (pm_e-2.html, build/deploy/index/embed wiring) + Rust device port
  (pm-ui draw_notation rewrite + LaneView.groups, pm-kit ViewMode, uisim notesim).

Verified: node tests/run.mjs 47 pass / 1 known; ./rust/run.sh green; pm-kit firmware + uisim compile.
2026-06-02 13:45:26 -05:00
Me Here
49a4308c4b pm_e-2: logo-in-device top-left + header/footer fill (match editor.html) 2026-06-02 13:45:26 -05:00
Me Here
e9adbc5f02 COORDINATION: note pm_e-2 MIDI privacy fix 2026-06-02 08:31:43 -05:00
Me Here
46397627e4 editors: never auto-prompt for Web MIDI on load (privacy)
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.)
2026-06-02 08:31:43 -05:00
Me Here
390c974a5f editor: move VARASYS logo into the device top-left (out of the site header)
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.
2026-06-02 08:25:59 -05:00
Me Here
8049ab8d61 COORDINATION: GO for Agent-A Phase 4; pm-kit released + reverted to clean tile build; editor fill note 2026-06-02 07:54:43 -05:00
Me Here
336d1b43bb editor: fill the screen + align header to editor width
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>
2026-06-02 07:52:23 -05:00
Me Here
1eca3ee0fe pm-kit: small-tile incremental updates (sub-rect blit) to cut tearing
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>
2026-06-01 13:52:07 -05:00
Me Here
eef535f9ef DESIGN: route LCD TE pin on production face for tear-free updates
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>
2026-06-01 13:04:42 -05:00
Me Here
bd3629ba4a pm-kit: working display — ported ST7796 driver + double-buffer + dirty row-bands
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>
2026-06-01 11:13:32 -05:00
Me Here
33dc5ff5cb PM_K-1 Phase 1: bench prototype firmware + doc (Pico 2 + WM8960, transformer-isolated XLR)
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>
2026-06-01 09:15:26 -05:00
Me Here
af79fe6f7f rust: probe-flash.md — flash+defmt via Pi Debug Probe in a Silverblue toolbox
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>
2026-06-01 08:49:02 -05:00
Me Here
8f4264f4d2 pm-kit: defmt+probe-rs diagnostics + flip-link toolchain
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>
2026-06-01 08:30:35 -05:00
Me Here
7faca6d0d7 pm-kit: isolation step 2 — heap init only (16KB), no parse, draw_ui
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>
2026-06-01 08:16:46 -05:00
Me Here
9e2e833485 pm-kit: minimal isolation (heap + parse + draw_ui, no inputs/audio)
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>
2026-06-01 08:08:35 -05:00
Me Here
aeda999526 pm-kit: diagnostic — draw confirmed-working pattern to isolate blank screen
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>
2026-06-01 08:02:03 -05:00
Me Here
08b0940d73 pm-kit: redraw periodically (~7fps) so the metronome reliably appears
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>
2026-06-01 07:54:23 -05:00
Me Here
95b960e071 docs: Stage 3 milestone 3 (live metronome) + notation views
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 00:20:43 -05:00
Me Here
8067820d19 site: Rust section now describes the live metronome (was bring-up image)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 00:19:38 -05:00
Me Here
0c788b1153 pm-kit: live metronome — real tracks, clock, audio clicks, controls, view toggle
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>
2026-06-01 00:18:28 -05:00
Me Here
ec29fb7284 pm-ui: notation refinement — shared stems + ledger lines
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>
2026-06-01 00:12:21 -05:00
Me Here
93617e1a91 pm-ui: drum notation view (first pass)
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>
2026-05-31 23:59:57 -05:00
Me Here
04350f9d09 pm-kit: peripheral self-test — buttons + joystick (ADC) + speaker (PWM)
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>
2026-05-31 23:56:13 -05:00
Me Here
0ea442d68d pm-kit: draw the real metronome screen on the panel (static sample data)
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>
2026-05-31 23:48:50 -05:00
Me Here
676d9879fa pm-ui: first real metronome screen (header/BPM/transport + polymeter lane grid)
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>
2026-05-31 23:38:48 -05:00
Me Here
d7b393b7c2 docs: Stage 3 — display milestone confirmed on hardware (ST7796 + CS-low fix)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 23:33:30 -05:00
Me Here
b154ccf493 pm-kit: hold CS low — fix ST7796 1/4 screen (mipidsi toggles CS mid-command)
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>
2026-05-31 23:01:04 -05:00