Commit graph

325 commits

Author SHA1 Message Date
Me Here
76392ab20f pm-kit: beat-pendulum on PIO too (unified PIO stepper, drop bit-bang driver)
Move the play-mode pendulum onto the same PIO/DMA stepper as jog: at each beat the
CPU just reverses direction and sets the sweep rate (rate = STEPPER_ARC / beat,
capped at STEPPER_MAX_RATE -> auto-shrink); the state machine sweeps continuously
between beats, so the pendulum stays smooth even while the screen redraws its
graphic. _pend_start kicks the first sweep on play; stop de-energizes via off().

self.pend is now a single PioStepper shared by play + jog (jog reuses it instead
of recreating). Removed the superseded bit-bang Pendulum class and the unused
_pend_last. README updated (pendulum motion is PIO-driven).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:38:38 -05:00
Me Here
f2cf3e3ed9 pm-kit: jog stops promptly on release (drop the decel ramp)
Releasing the joystick used to ramp the speed down through zero (~0.3s), so the
motor coasted after release. Decel isn't needed (only fast *starts* stall), so
release now stops immediately; keep the gentle accel ramp on start. Also only
rewrite the PIO clock when the rate actually changes (no 100Hz redundant writes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:29:11 -05:00
Me Here
44193a07c1 pm-kit: PIO/DMA stepper for jog mode (hardware-timed, CPU-free pulses)
Move jog-mode stepping off the CPU loop onto a PIO state machine: one hardcoded
instruction (out pins,4 [31] = 0x7F04, no adafruit_pioasm needed) shifts a 4-bit
coil nibble to GP18..21 every 32 PIO cycles; one 32-bit word packs all 8 half-step
phases; background_write(loop=) DMA-feeds it continuously. half-steps/s = clock/32,
so speed + accel = setting sm.frequency. Pulses now run on dedicated hardware, so a
display refresh or GC pause can't stall them - which was the ~1s "smooth then jump".

Jog loop is now a light 100Hz CPU controller (joystick + accel ramp + frequency);
the live step/rate readout is restored since the motor runs from PIO/DMA. Bit-bang
Pendulum keeps a deinit() so jog can hand GP18..21 to PIO. Beat-pendulum still on
bit-bang (PIO port next).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:24:12 -05:00
Me Here
9651e8bc6a pm-kit: fix jumpy jog (stop drawing mid-spin) - smooth steady spin
The ~1s hitch was the once-per-second readout: show_stats() allocates text
bitmaps (GC pause) and display.refresh() blocks the SPI blit, both stalling the
step loop exactly every second. Now the rate is measured silently while spinning
and the readout (steps + peak) is redrawn only when you release; a gc.collect()
on release + before spinning keeps the heap clean. Steady spin does zero display
work -> smooth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:13:07 -05:00
Me Here
a7e8061a9b pm-kit: stepper/pendulum tuning via settings.json (no recompile)
_load_settings now also reads stepper_max_rate / stepper_accel / stepper_jog_start
/ pend_swing_deg / stepper_steps_per_rev and recomputes the derived PEND_THETA +
STEPPER_ARC, so the motor speed/accel/swing can be dialed in by editing the file
on the drive instead of rebuilding the firmware. README documents the keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:59:35 -05:00
Me Here
19d646a873 pm-kit: jog mode = direction-only, accelerate to a smooth max speed
The joystick has no useful fine speed control, so jog now treats it as direction
only and runs the motor at STEPPER_MAX_RATE, reached via a trapezoidal accel ramp
(STEPPER_ACCEL from STEPPER_JOG_START) so it doesn't stall trying to start at top
speed; reversing decelerates through zero then accelerates the other way. Default
top rate set to a realistic 600 half-steps/s for the 28BYJ-48; tune via jog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:57:46 -05:00
Me Here
5f9e9dfad7 pm-kit: fix jog-mode step-rate cap (loop overhead, not the motor)
The ~330 steps/s ceiling was the CircuitPython loop, not the stepper: an analog
read + time.sleep(0.0005) every iteration made each pass ~3ms (1/0.003 ~ 330).
Tighten the jog loop - poll the joystick at ~250Hz off the step hot-path, drop
the per-iteration sleep, refresh the readout ~1/s instead of 3+/s, and raise the
commanded ceiling to ~1600 steps/s - so the peak reflects the motor's real limit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:51:34 -05:00
Me Here
36c7406d71 pm-kit: jog-mode step counter + rate readout, and document the pendulum/jog feature
- Jog/test screen now shows a live step count and current/peak step rate; jog
  ceiling raised to ~1000 steps/s so you can probe past a motor's max and read
  the peak rate where it stalls -> set STEPPER_MAX_RATE just below that.
- README: new "Pendulum (stepper motion)" section (wiring GP18-21, the config
  knobs, motion behaviour, jog/test mode) + the A-alone / A+B power-on chords;
  noted the GP19/20/21 conflict with the custom PM_K-1 board's ribbon.

Pure ASCII; conformance 47/47; app.mpy precompiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:43:22 -05:00
Me Here
15755f4d0c pm-kit: hidden stepper jog/test mode (hold A+B at boot)
Hold both buttons at power-on to enter a self-contained jog screen: the joystick
spins the stepper CW/CCW (speed by deflection), with an on-screen direction
needle + RGB LED feedback. Runs in its own loop; power-cycle to return to normal.

- app.py: _jog_loop drawn entirely in the overlay group (cover + labels + needle);
  Pendulum.spin() does a free half-step either way; _jog set when A+B held in init;
  run() branches to it before the normal loop.
- boot.py: editor mode is now "A alone" (A pressed, B not). A+B stays in appliance
  mode, so the jog chord doesn't also flip the drive writable.

Pure ASCII; conformance 47/47; build precompiles app.mpy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:37:57 -05:00
Me Here
5f3c518089 pm-kit: pendulum swing as a single degrees knob (PEND_SWING_DEG=120)
Make the swing arc one source of truth in degrees, driving both the screen
graphic (exactly) and the physical arm (mapped through STEPPER_STEPS_PER_REV).
Set to 120 deg end-to-end. PEND_THETA now derives from it; STEPPER_ARC =
steps_per_rev * deg/360. Graphic geometry verified on-screen at 120 deg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:26:17 -05:00
Me Here
0eb38f1c1e pm-kit: on-screen swinging pendulum graphic, synced to the motor's beat phase
Draw a swinging pendulum on the ST7796 that mirrors the physical stepper arm.
Inverted-metronome style (pivot near the bottom, weighted bob swinging up top),
shown over the practice-log area while playing and swapped back to the log when
stopped (the log is for post-session review, not mid-play).

- _build_scene: a hidden g_pend group (stand + pivot + arm Polygon + bob Circle).
- draw_pendulum(now) computes the bob from the SAME swing phase the motor uses
  (_pend_beat0 / _pend_dir / _beat_ns), so screen and arm move identically and it
  follows tempo ramps. Animated ~30fps from run(); the gated refresh renders it.
- _pend_service now advances the swing clock even when no motor is wired, so the
  graphic works standalone (STEPPER_ENABLED=False still animates the screen).
- tick() toggles g_pend/g_log visibility on the play<->stop transition.

Pure ASCII; conformance 47/47; build.sh precompiles app.mpy fine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 21:07:53 -05:00
Me Here
305d9373d0 pm-kit: beat-synced pendulum stepper on the CircuitPython Kit (display-model motion test)
Add an optional physical pendulum to pico-cp/app.py: a 4-input unipolar stepper
(e.g. ULN2003 on the EP-0172's free GP18-21) swung as a metronome arm in time
with the beat. First motion-feedback test for the display-model Kit.

- New Pendulum driver class (half-step 8-phase, non-blocking step_toward + release).
- _pend_service(now) derives the swing from the live _beat_ns: it reverses
  direction at each beat boundary so the arm hits an extreme exactly on the beat,
  and auto-shrinks the arc when STEPPER_MAX_RATE can't sweep the full travel in a
  beat. Reading _beat_ns live means it follows tempo ramps for free.
- Hooked into tick(): swings while running, de-energizes the coils once on stop
  (covers all stop paths). Swing phase re-aligns to the clock in _reset_clock.
- Config knobs (STEPPER_ENABLED/ARC/MAX_RATE) + P_STEP pins at the top; disabled
  cleanly leaves the pins free.

Stays pure ASCII (USB-MIDI push requirement); conformance suite still 47/47;
build.sh precompiles app.mpy fine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 20:59:12 -05:00
Me Here
d80c35984e pm-daisy: Daisy Pod spike — play the click engine on STM32H7 (host-verified, awaiting hardware)
Develop the full Daisy Pod spike so it can be flashed the moment the board
arrives. Architecture: one shared engine, two front-ends.

- pm-synth: make it `#![no_std]` (mirroring track-format), routing float math
  through `libm` so the SAME f32 code runs on the host and on the Daisy's
  Cortex-M7F (hardware FPU — no fixed-point port needed). Add `Player`, a
  self-running sequencer that owns the Synth + scheduled clicks and renders
  sample-by-sample, looping at the pattern boundary. Integer-only hot path
  (clicks pre-resolved to sample indices); exposes a `fired()` beat counter.
  Add SPIKE_PROGRAM/SPIKE_BARS as the shared source of truth.

- synthrender: render the SAME Player to pm-daisy-preview.wav — the host-side
  "simulator". Bit-identical preview of the hardware output (before its codec);
  far more useful than chip emulation (Renode can't model the audio codec).

- pm-daisy (new, workspace-excluded firmware): thin BSP binary for the Daisy
  Seed/Pod. embedded-alloc heap + board bring-up + SAI-DMA audio interrupt
  feeding Player::next_sample() into stereo frames, USER LED flashing per click.
  Audio loop follows the `daisy` crate's examples/audio.rs. Board revision
  (codec) is a Cargo feature; README documents matching it + both flash paths
  (probe-rs/RTT and USB DFU) + the QSPI-bootloader fallback.

Verified without hardware: host build + preview render (48 kHz, onsets on the
8th-note grid at 124 BPM); firmware cross-compiles + links for thumbv7em-none-
eabihf at ~87 KB (fits the 128 KB internal flash) across all three codec
revisions; track-format conformance + `node tests/run.mjs` (47 pass) still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:41:10 -05:00
Me Here
0bec13abab midi-out: 24-PPQN clock + Start/Stop; shared src/midiout.js; mirror into editor.html
- Extract MIDI-out into a shared partial src/midiout.js (one copy, no drift); both
  editors @BUILD:include it. The page wires three transport hooks: midiOutStart(t0)
  in start(), midiOutStop() in stop(), midiOutClock(ahead) at end of scheduler();
  engine.js calls onMeterHit() per hit.
- Clock-out: a "clock" checkbox (default on) appears with the port picker. When on:
  MIDI Start (0xFA) at the downbeat, 24-PPQN clock (0xF8) scheduled across the audio
  look-ahead window (timestamped, tracks tempo/ramp, stays tight), Stop (0xFC) on
  stop. Guarded against starve-looping at extreme tempos.
- Mirror the feature into editor.html (PM_E-1): header .devctrl pills, the include,
  _wireMidi port refresh, transport hooks, listener, and the _isDevicePort fix
  (recognizes PM_G-1 Grid etc.).

Conformance suite still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 14:11:24 -05:00
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