- 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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>