Commit graph

51 commits

Author SHA1 Message Date
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
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
da7c94e67f Implement per-track playback flow (rep / end / relative goto)
Adds the per-track end-action model designed in docs/track-format.md §3, end to
end across both engines, both firmwares, and the editors.

Grammar (parsed + serialized by engine.js and both app.py):
  rep=<n>     cycles before the end-action fires (default 1)
  end=stop    stop after rep cycles
  end=next    advance one track (sugar for end=+1)
  end=<±N>    relative goto after rep cycles (e.g. end=-2 = D.S.)
  (absent)    loop forever — the metronome default

Firmware runtime (pico-cp + pico-explorer): _on_new_bar now consults a per-track
_end_plan() and fires stop / gapless-advance / relative-goto at the right bar.
A cycle = b<bars>, else one master bar; fire bar = rep * cycle. Explicit end=
governs; with no end, the global Continue toggle stays a default (=end=next, still
needs b<bars>) so existing set-lists and the CONT UI are unchanged. _prepare_next
takes a target index; the seam machinery, _do_advance and live-sync all carry rep/end.

Editors (editor.html + editor-beta.html): state.rep/state.end thread through
applySetup / currentSetup / currentPatch so load -> edit -> save preserves the
flow; authoring is via the program-string field (no graphical control yet).

Tests: the 3 playback-flow vectors now pass on both engines (39 pass / 3 known).
Runtime decision logic (_end_plan / _goto_target) unit-tested for stop, rep,
relative goto clamp/wrap, and legacy-Continue precedence. Codec round-trip
verified idempotent. Both firmwares compile + mpy-cross clean.

Also: untrack stale __pycache__/*.pyc build artifacts and gitignore them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:37:06 -05:00
Me Here
9701f49913 Firmware: parse euclid, GM note-numbers, and unknown-sound fallback
Close three real parser divergences the conformance suite flagged on the device
side (pico-cp + pico-explorer) — cases where the firmware produced a different
groove/sound than the web for the same patch:

- Euclidean (k,n,rot) shorthand (e.g. kick:4(3,8)) — was silently dropped to a
  plain bar; now expands to the same hits as engine.js (added _euclid + parsing).
- GM note-number lane sounds (e.g. 36:4) — now resolve to the voice name (GM_NUM).
- Unknown sound names fall back to beep, matching the web.

vol/cd are NOT carried by the firmware by design: they are web-authoring fields
(the device has a hardware volume knob and no count-in). Documented as an
intentional, permanent host difference rather than a bug; the vol-and-countin
vector stays as expectFail[py] to mark the boundary.

tests/adapters/py_adapter.py: extract the new SOUND_GM/GM_NUM/_euclid nodes.
fixtures: euclid/unknown-sound/gm-note-number now pass on both engines.
docs §6 updated. node tests/run.mjs: 33 pass / 9 known, round-trips stable.
pico-explorer parser spot-checked identical to pico-cp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:15:25 -05:00
Me Here
bf74c860e5 Track format: unify default (no-pattern) groove across web + firmware
A lane with no =pattern produced different defaults on web vs device — a real,
shipped divergence the new conformance suite caught (e.g. hatClosed:4/2 in
"Four-on-the-floor" played steady 8ths in the browser but quarter-notes on the
device). Adopt one rule everywhere: every subdivision sounds at normal level,
accents fall ONLY on group starts (the grouping is the accent map).

- pico-cp/app.py, pico-explorer/app.py: off-beat subdivisions sound at normal (1)
  instead of resting (0); group-start accenting was already correct.
- src/engine.js: default beatsOn accents group starts only (was: every beat);
  laneCfgToStr isDefault check updated to match so round-trips stay idempotent.
- docs + fixtures: document the rule; default-pattern vectors now pass on both.

Audible effect (intended): device subdivided hat/ride lanes gain their off-beat
strokes (now match the web); web stops over-accenting every beat. Lanes with an
explicit =pattern are unchanged. Verified green via node tests/run.mjs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:03:34 -05:00
Me Here
c625a8aaa2 Firmware push fix on both Kit (0.0.24) + Explorer (0.0.4)
Diagnosed from the user's console output - 25 chunks pushed cleanly at
~124ms each, then stalled. Two coupled causes:

1) Bus contention. tick() and Live sync share self.midi with the chunk
   ACKs. While the device was processing a chunk, a Note On / Clock Out /
   Live-sync FULL heartbeat could land on the same MIDI OUT stream and
   the host's parser dropped the interleaved ACK SysEx.

   Fix: self._fw_pushing flag set on 0x21 BEGIN, cleared on 0x23 COMMIT
   or any error. midi_send / Clock Out / _sync_broadcast / _sync_broadcast_full
   all early-out when _fw_pushing is True. Only ACKs go out during a push.

2) SysEx assembler garbage. self._sx = bytearray() per chunk leaks ~70
   bytes / chunk that only GC'd every 50 chunks. 25 chunks of trash plus a
   slow heap walked the wrong way explains the ramp-up to 174 -> 119 -> 124
   ms ACK times. GC every chunk now (~30ms cost on RP2040/RP2350 with
   small heap) so the assembler buffer is always fresh.

Same patch on both pico-cp/ and pico-explorer/ since the bug is identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 22:51:46 -05:00
Me Here
3192f3debc PM_X-1 0.0.1: Pimoroni Explorer sibling firmware + Kit 0.0.23 device-id reply
Adds pico-explorer/ as a parallel CircuitPython firmware target alongside the 52Pi
Kit in pico-cp/. Same engine, same program-string grammar, same programs.json, same
live-sync protocol. Read-only on the device (no on-device beat editing); the web
editor's Live sync mirrors all edits in real time and the Explorer emits its own
play/stop/bpm/sel deltas back.

Hardware (Pimoroni Explorer PIM744):
- RP2350B + 2.8" ST7789V 320x240 LCD (8-bit parallel; CircuitPython's official
  board definition pre-builds the BusDisplay so we just use board.DISPLAY).
- 6 user buttons - A/B/C on the left of the screen, X/Y/Z on the right.
- Piezo speaker on GP12 (PWM) with amp enable on GP13.
- I2C QwSTEMMA on GP20/21 - reserved, unused by the firmware.
- No touchscreen, no joystick, no RGB LED. Run state shows on a tiny on-screen dot.

Buttons:
- A = play/stop. B = tap tempo. C = menu.
- X = prev track (hold-repeat). Z = next track (hold-repeat).
- Y = tempo -1 (hold-repeat; -5 after 1.5s).
- X+Z chord = tempo +1 (mirrors Y).
- In a menu: X/Z move the row cursor, Y decrements, A cycles/increments/selects,
  B = back, C = close.

Files added:
- pico-explorer/{boot.py, code.py, app.py, programs.json, README.md}.
  app.py = 1444 lines (~73KB source -> 29.8KB compiled .mpy).
- info-explorer.html.

Files touched:
- pico-cp/app.py: bump to 0.0.23. Version-query (SysEx 0x02 -> 0x03) reply now
  includes the device id as "K;<version>" (backward-compat: editor parses
  "contains ';'?" - old firmware sent bare version, treated as K).
- editor.html + editor-beta.html: _parseDeviceReply() splits id;version, FW_PATHS
  maps id to .py/.mpy URL pair, so Update firmware now pushes the right binary.
- build.sh + deploy.sh: precompile pico-explorer/app.py -> dist/explorer-app.mpy,
  zip pm_x1_circuitpy.zip alongside pm_k1_circuitpy.zip, ship
  pico-explorer-app.{py,mpy} next to pico-cp-app.{py,mpy}.
- docs/livesync-protocol.md: new section 7 - per-device emit/apply matrix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 20:43:38 -05:00
Me Here
8726f42d05 PM_K-1 0.0.22: cleanup -- delete the dead _step_dur fallback
By 0.0.20 every site that needed step duration had switched to L['durs'][step]
(the precomputed tuple from _rebuild_dur). _step_dur was kept as a "legacy
fallback" with a comment claiming it was still used by the tap-tempo path --
which was wrong; grep showed zero callers. Ten lines of unreachable code.

Now that 0.0.21 is sounding clean on the Pico 2, time to trim the sediment. No
behavior change; .mpy shrinks slightly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 11:30:49 -05:00
Me Here
47fa6d7ce7 PM_K-1 0.0.21: live-sync protocol (HELLO/FULL/DELTA/BYE) - device side
Implements the device half of the docs/livesync-protocol.md contract that the
other (editor) Claude wrote in src/livesync.js. SysEx opcodes 0x40..0x43 on the
existing 0x7D manufacturer id; ASCII payload reusing the share-grammar tokens.

State + helpers (App.__init__):
- self._sync_origin: short random id ("d" + 8-hex from os.urandom(4)) - lets
  peers drop their own echoes (composite USB MIDI may loop a frame back).
- self._sync_seq: monotonic counter.
- self._sync_armed: True after any HELLO/FULL/DELTA seen (until BYE).
- self._sync_applying: echo guard - True while applying a remote change so
  the broadcast hooks early-out and we never ping-pong.
- self._sync_heartbeat_next: deadline for the periodic 5s FULL.

Transport:
- _sync_send(op, text): builds the SysEx frame, ASCII-safe (replaces high bits
  with '?'), writes via self.midi.write.
- _sync_broadcast(evt): one DELTA, guarded.
- _sync_broadcast_full(): one FULL with origin;seq;running;sl;item;patch; also
  resets the heartbeat timer.

Receive (extends _handle_sysex):
- 0x40 HELLO -> _sync_broadcast_full (reply with our state).
- 0x41 FULL  -> _sync_apply_full(running, patch): diff against current
  _prog_str(); only rebuild if different (avoids flicker on a heartbeat that
  matches local state); reconcile transport either way.
- 0x42 DELTA -> _sync_apply_delta(evt) for the seven verbs:
  - play / stop -> toggle if state differs.
  - bpm=<n>      -> set_bpm.
  - sel=<sl>/<item> -> switch + goto; -1/-1 sentinel ignored.
  - beat=<l>/<s>/<lvl> -> set lanes[l]['levels'][s], recolor pad, mark dirty.
  - lane=<l>/<field>/<value> -> sound/groups/sub/swing/gain/poly/enabled; if
    structural, _regen_levels + _rebuild_dur (or _rebuild_dur_all when lane 0
    changed, so poly lanes follow) + build_grid.
- 0x43 BYE -> _sync_armed = False.

Local mutators broadcast:
- toggle()         -> "play" | "stop"
- set_bpm()        -> "bpm=<n>"
- goto() / switch_setlist() -> "sel=<sl>/<item>"
- _cycle_beat()    -> "beat=<l>/<s>/<lvl>"
- _lane_dirty()    -> FULL (coalesced; matches editor's syncPatchSoon idiom)

Heartbeat in the run loop emits a FULL every 5s while armed.

Plus docs/livesync-protocol.md (the spec the other Claude wrote in parallel)
gets committed too. The patch field round-trips through parse_program <->
_prog_str on the device side and setupToPatch <-> patchToSetup on the editor
side, all already share-grammar compatible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 09:59:19 -05:00
Me Here
09144c9892 PM_K-1 0.0.20: per-pad chunking + step grids + smarter refresh
Three changes; one diagnosis: the 0.0.19 freeze + stutter were both symptoms of the
predictive refresh skip being too aggressive at fast subdivisions (scanning ALL lanes
+ a 35ms window meant the next beat is *always* within the skip window at fine
subdivisions, so the only refresh was the 0.5s force-after fallback, audible as both
a flickery freeze AND a periodic 30ms stutter).

PER-PAD CHUNKING (your ask):
- _grid_rebuild_step now builds ONE rectangle per loop iter (was one whole lane row).
- The first chunk on a lane draws the instrument label + caches the lane's geometry;
  subsequent chunks each emit one pad rect/circle.
- Each chunk is ~5-10ms instead of ~30-50ms, so tick() interleaves at sub-beat
  resolution and the grid fills in pad-by-pad over ~600ms after the seam without
  ever blocking long enough to miss a beat at any reasonable BPM.

STEP GRIDS:
- L['durs'] is a tuple of int ns per step, precomputed once per lane.
- tick()'s inner-loop scheduler is now L['next'] += L['durs'][L['step']] -- a tuple
  index, no method call, no dict lookups, no division.
- _rebuild_dur(L) and _rebuild_dur_all() wired into every bpm-change path:
  set_bpm / load / _do_advance / _prepare_next / _slave_tick (Clock In) / _lane_dirty
  (the lane editor; rebuilds master-affects-poly cascade if lane 0 changes structurally),
  + the per-master-step ramp interpolator. The synchronous _step_dur stays as a fallback
  for paths the inner loop doesn't hit (e.g. start-of-segment calculations elsewhere).

SMARTER REFRESH:
- Master-lane-only window (other lanes are subdivisions; a few ms of jitter there is
  imperceptible musically).
- 10ms window (down from 35) so refresh runs in the gap between master beats even at
  fast subdivisions.
- 20Hz throttle (down from 30) so the per-refresh blocking budget is bigger.
- 200ms force fallback (down from 500) so visuals + titles stay live -- this is the
  fix for "the track titles don't get reliably updated."

DEFER set_bpm DRAWS:
- set_bpm no longer calls draw_bpm/draw_meters; the 4Hz UI tick already redraws them.
- Joystick spam used to allocate a fresh text bitmap per nudge -> fragmentation
  pressure + potential GC stutter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 08:58:13 -05:00
Me Here
672f892ea1 PM_K-1 0.0.19: chunked build_grid, predictive refresh, zero-alloc hot path
The stutter and the MemoryError were two faces of the same problem: the main loop
allocates and blocks too much. This pass attacks both.

CHUNKED build_grid -- the biggest single audio fix.
- build_grid() split into _grid_rebuild_start (tear-down + gridlines + state) and
  _grid_rebuild_step (build ONE lane row + its pads).
- run() loop processes one chunk per iter, so tick() runs between every lane. A 16-pad
  lane chunk is ~30-50ms but a tick now interleaves; the old ~300ms blackout is gone.
- The synchronous build_grid() still works (used by load() etc) -- it loops the steps
  to completion in one call, same as before for non-seam paths.
- load() / switch_setlist cancel any in-progress chunked rebuild so a navigation
  doesn't leave the grid half-built.

PREDICTIVE display.refresh -- the second-biggest audio fix.
- display.refresh() blocks SPI for ~30ms streaming pixels. A beat scheduled in that
  window was firing late.
- The refresh decision now scans lanes[i]['next'] (cheap; n<=8) and skips refresh if
  any beat is due within 35ms.
- Force-refresh after 0.5s so visuals stay live at fast subdivision rates.

ZERO-ALLOC HOT PATH -- attacks fragmentation / MemoryError frequency.
- self._note_buf: reused bytearray for every MIDI Note On (was a fresh bytes() / click).
- self._clock_byte / _start_byte / _stop_byte: singleton bytes for transport messages.
- fired_best / fired_prio ints replace fired = [] (was ~1000 list allocs/sec at speed,
  plus a max(key=lambda) call per tick -- gone).
- self._beat_ns cached in set_bpm + the ramp interpolator; _step_dur reads it instead
  of doing 60_000_000_000 / self.bpm every step. Integer // instead of float /.
- The MIDI Clock Out also caches its tick period via _beat_ns // 24.

CRASH SAFETY -- the run loop's while True body is now wrapped in try/except. A
MemoryError logs + gc.collect() + continues. Any other Exception logs + continues. The
metronome now stutters through transient errors instead of dying with a traceback.

This is roughly the ceiling of what straight Python can do here without a custom-C
tick module. The remaining stutter sources are the displayio scene-graph itself + the
RP2040 bytecode interpreter overhead, and those move us to Rust (or a C extension) if
audible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 08:38:18 -05:00
Me Here
93c1fb62e9 PM_K-1 0.0.18: split post-seam redraw -- fast bits now, heavy build_grid deferred
0.0.17 fixed the off-by-one so A plays its full final bar, but B's first beats were
still being eaten -- by build_grid + draw_log blocking the main loop for ~hundreds of
ms right at the seam (a full grid teardown + ~64-128 vectorio rectangle allocations +
draw_log text bitmaps + an SPI refresh). tick() doesn't run during that, so B's first
master steps fire only when the catch-up while-loop runs at the next tick, collapsing
into one click.

Split the post-seam refresh:

- Fast pass: draw_meters / draw_bpm / draw_status / draw_train. Cheap (each just swaps
  one TileGrid). Runs immediately so the new title + bpm + status are correct on screen.
- Heavy pass: build_grid + draw_log. Deferred by 0.6s after the seam, gated by
  self._heavy_redraw_at. B's intro plays clean; the grid catches up half a beat or so
  into B with a single brief jitter (rather than B's first 1-2 beats being lost).

Plus gc.collect() at the start of build_grid AND draw_log. Both allocate enough small
objects that a fragmented heap will MemoryError partway through; collecting first gives
them a clean run.

load() / switch_setlist() clear _heavy_redraw_at so a user-initiated navigation doesn't
trigger a second deferred rebuild on top of the immediate one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 08:25:19 -05:00
Me Here
da71604c0d PM_K-1 0.0.17: fix gapless-seam off-by-one + GC hygiene around modals / parse
The continuous-playback gap between tracks was a one-master-step early trigger of
_on_new_bar(N): nb = _m_steps // steps maps to the LAST step of bar N-1, not the
downbeat of bar N. So _seam_t (= lanes[0]['next'] at that moment) was one master step
short of the proper boundary -- A lost its last step, and B's "bar 0 step 0" landed at
that early slot. With kick:2 (2 steps/bar) that's half a bar each side; with kick:4 a
quarter; with kick:1 a whole bar.

Fix: nb = (self._m_steps - 1) // L['steps'] -- fires at the downbeat of the new bar.
Same adjustment applied to the on-screen "bar X of N" counter (mbars) so it advances
on the downbeat, not on the last beat of the previous bar.

Memory hygiene: gc.collect() at the entry of every modal (_show_menu / _show_settings /
_show_help / _show_about / _show_saverevert / _show_laneedit) so each modal allocates
its bitmaps against a defragmented heap. _prepare_next gc.collect()s before
parse_program and catches MemoryError -> the segment loops instead of crashing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 08:13:49 -05:00
Me Here
c9f2288bdd 0.0.16 fix: strip the two stray ☰ characters from app.py comments
Previous build aborted at the ASCII guard; the .mpy on the server didn't update. This
adds the patch as a new commit (no history rewrite).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:53:00 -05:00
Me Here
5153e35a52 PM_K-1 0.0.16: BPM floor 30 -> 5; hamburger ☰ menu + Settings / Help / About
BPM lower bound dropped 30 -> 5 (engine + ramp + slave-clock-in interval filter; pure
clamp choice, no engine reason for the higher floor). Very slow practice is now
possible. Sub-musical (<5) sits below the slave-decay timeout so wasn't worth pursuing.

Hamburger menu (☰ at the far right of the header, 3 thin recolorable rects; tap zone
covers the corner). Reuses the existing overlay / _ovbtns pattern - no new framework.

- Main menu (_show_menu): Save edits / Revert edits (dimmed when not _dirty) / Continue
  on-off (live) / Settings > / Help > / About / Done.
- Settings sub-modal: LED brightness 5..50% in 5% steps, Speaker mode (Auto / Always /
  Off, combining MUTE_SPEAKER + SPEAKER_AUTO_MUTE), MIDI Out on/off, MIDI Channel 1..16,
  Clock Out on/off, Clock In on/off. < value > adjusters reusing the lane-editor row
  pattern. Each tap calls _save_settings(); the modal redraws live.
- Help sub-modal: 3 paginated pages (Transport & Nav / Editing / Status & Hardware)
  with < Done > nav.
- About sub-modal: version, free RAM, uptime, CircuitPython version, site URL. Defensive
  about gc.mem_free and sys.implementation (CircuitPython-specific).
- Persistence to /settings.json: read in __init__ (_load_settings overrides defaults),
  written on every change (settings are small + infrequent). NAKs silently if read-only.

Verified in the harness: all 4 modals render, settings.json round-trips (channel 10->11,
clock_out off->on persisted), tap zones populate as expected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:50:55 -05:00
Me Here
12a31b87a8 PM_K-1 0.0.15: MIDI Clock In (slave) - follow an external 24 PPQN master
New config: MIDI_CLOCK_IN (default OFF) + MIDI_CLOCK_IN_TRANSPORT (default ON).

_feed_midi now intercepts 0xF8 (clock tick) -> smoothed bpm tracker (exponential, a=1/8;
rejects out-of-range intervals so noise can't drag bpm wild), 0xFA / 0xFB (Start /
Continue) -> start playback without echoing 0xFA on output (would feedback), 0xFC ->
stop. _slaved flag is True while ticks are arriving and decays after 1s of silence.

While slaved: per-master-step continuous ramp is OFF (the master's tempo wins) and
Clock Out emission is suppressed (no feedback loop if the user enables both).

Verified in the harness: target 60/120/180 BPM all track exactly after 60 ticks
(2.5 quarter notes of smoothing); 0xFA -> running, 0xFC -> stopped; slave flag
correctly decays.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:38:54 -05:00
Me Here
f637a65abd Push reliability: 10s ACK timeout + per-chunk flush + periodic GC
Push to 0.0.14 stalls at ~150 chunks with the MIDI badge going gray (=device silent for
>1s). It's an occasional slow flash flush on the device side — the file buffer fills,
flush takes several seconds, the editor's 4s timeout cuts it off too early.

- Editor: bump per-chunk ACK timeout 4s -> 10s; progress log every 25 chunks now with
  elapsed seconds so you can see it advancing through a slow chunk.
- Device 0.0.14: flush per chunk (small, predictable per-chunk cost instead of
  infrequent multi-second bursts) + gc.collect() every 50 chunks to keep the heap
  fresh during a long push.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:28:12 -05:00
Me Here
99174d1bf8 PM_K-1 0.0.14 (fix): late-toggled Continue arms the seam too
If Continue was toggled on mid-segment, _prepare_next never ran during bar (bars-1)
and the seam stayed un-armed. Fall back to prep on the spot at the boundary itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:12:00 -05:00
Me Here
fd8446658d PM_K-1 0.0.14: gapless seam + continuous ramp + MIDI Clock Out (master); speaker rename
Speaker rename (production device has a full audio circuit, not a buzzer):
- MUTE_BUZZER -> MUTE_SPEAKER, self.buz -> self.spk, P_BUZ -> P_SPK
- New SPEAKER_AUTO_MUTE flag (default True): mute the speaker when a MIDI host is
  detected (the old hardcoded behavior; now a setting).

Gapless seam between tracks (Continue):
- _prepare_next() pre-parses the next playlist item during the LAST bar so the swap is
  allocation-free. _do_advance() swaps lanes/bpm/bars/ramp/trainer in with
  lanes[0]['next'] = seam_t (the wall-clock time of the boundary step we just hit), no
  _reset_clock - the next tick fires step 0 of the new track exactly at the boundary.
  tick() breaks out at the seam so the old voice's boundary beat is NOT fired (it'd be
  the new track's step 0 a few ms later). Visuals (build_grid + draws) are deferred
  one display-refresh cycle behind the audio via _need_redraw, so the audio doesn't
  wait for them.

Continuous ramp:
- Replaced the bar-boundary set_bpm step with per-master-step linear interpolation:
  bpm = _ramp_base + amt * ((m_steps/mlen) % bars) / ramp.every (clamped 30..300).
  The integer-clamped bpm glides smoothly across the segment. draw_bpm() is now lazy
  (skips the bitmap alloc if the displayed integer hasn't changed), and the periodic
  meter tick in run() also redraws BPM so the big number follows the ramp.

MIDI Clock Out (master):
- New flags: MIDI_CHANNEL (default 10 = GM drum), MIDI_CLOCK_OUT (default OFF),
  MIDI_CLOCK_OUT_TRANSPORT (default ON). midi_send() now uses the configured channel.
  In tick(), when running + MIDI_CLOCK_OUT, stream 0xF8 at 24 PPQN with the interval
  computed live from self.bpm (so it follows the continuous ramp). toggle() sends
  0xFA on Start and 0xFC on Stop when transport is enabled.

Verified in harness: seam keeps lanes[0]['next'] = seam_t (no _reset_clock); ramp 80
glides via +0.25/step (visible as 80->81 in 4 master steps at rmp80/4/4); Clock Out
math sound (60/120/180 BPM -> 41.67/20.83/13.89 ms tick interval).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:11:19 -05:00
Me Here
73d18ab1f3 PM_K-1: keep firmware versioning in the 0.0.x series (0.1.0 -> 0.0.13)
Per request, stay on 0.0.x; the lane-editor build is relabeled 0.0.13 (no code change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:55:36 -05:00
Me Here
dbc9fa7fdc PM_K-1 0.1.0: on-device lane editor - edit/add/remove lanes
Tap the instrument name -> a modal to change Sound (cycle the GM voices), Beats (1-12),
Subdivision (1-8), Swing, and Mute, plus + Lane / Remove (1..MAXLANES). Beats/sub changes
regenerate the lane's default accents; sound/swing/mute keep the pattern. Reuses the
existing dirty + Save/Revert + .mpy machinery (edits to a built-in save a copy to
"My edits"). The modal redraws live as you adjust; tap Done or outside to close.

Verified in harness: editor opens (13 hit-zones), sound cycles, beats/sub regen steps,
swing/mute toggle, add/remove lanes, the edited track serializes + round-trips, Done
closes; modal renders cleanly. app.mpy builds (C/v6).

This completes the Phase-2 editing set (beats + lanes + Continue + built-in/user split).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:44:04 -05:00
Me Here
ecd1d2a189 PM_K-1 0.0.12: Song = 4-bar sections, timer resets with the bar, smoother MIDI
- Built-in Song playlist: every section is now b4 (~4 bars) so Continue rolls one into
  the next quickly.
- On-screen timer now counts WITHIN the current segment and resets every time the bar
  counter wraps (new _seg_start, reset at each b<n> boundary + on _reset_clock). The
  practice-log duration still uses play_start (total). Unified the segment-boundary
  handling (timer reset + ramp restart + Continue advance) in _on_new_bar.
- MIDI stutter: display.refresh() BLOCKS on the SPI stream and was delaying the next
  beat's note. Cap refresh to ~30Hz and poll the GT911 touch ~30Hz (was every loop) so
  the scheduler fires notes on time; visuals lag a few ms (imperceptible).

Verified in harness: Build(b4,rmp92/4/2) bpm 92->96->reset@bar4, seg_start resets only at
the boundary, Continue arms there; edit tests pass; app.mpy builds (C/v6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:36:03 -05:00
Me Here
13318daf5b PM_K-1 0.0.11: fix boot crash - CircuitPython str has no .isalnum()
With the .mpy loading (no more OOM), the full app ran on hardware for the first time and
hit _slkey()'s c.isalnum() in the set-list dedup -> AttributeError (MicroPython/CircuitPython
str omits isalnum; my CPython harness has it, so it slipped through). Replaced with a
membership test against an explicit alnum set (uses only .lower()). Also compile the .mpy
from inside pico-cp/ so tracebacks read "app.py" instead of "pico-cp/app.py".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:17:12 -05:00
Me Here
7481f91935 PM_K-1 0.0.10: ship precompiled app.mpy (fixes boot OOM) + push .mpy over the air
The single-file app grew to ~57KB; CircuitPython compiling it at boot fragments the
RP2040 heap so badly that the fonts can't get a contiguous block (161KB free, yet a
~16KB alloc fails). Fix: precompile to app.mpy (Adafruit mpy-cross for CP 10.2.1, emits
CircuitPython mpy v6) so the device loads bytecode without compiling -> no fragmentation.

- build.sh precompiles pico-cp/app.py -> dist/app.mpy via tools/mpy-cross (gitignored
  binary); the bundle ships app.mpy (NOT app.py); serves pico-cp-app.mpy + pico-cp-app.py
  (the .py only for the editor's version regex + as readable reference).
- Loader (code.py) imports app.mpy and rolls back app.bak as .mpy.
- One-click updater now pushes the .mpy: editor base64-encodes it and sends it over the
  existing flow-controlled chunked transport (512-char = mult-of-4 chunks); the device
  base64-decodes each chunk to /app.new and verifies the CircuitPython .mpy header
  (magic 'C', v6, >=4KB) before the A/B install. Version still read from the served .py.

Verified: mpy-cross emits magic 'C'/v6; build produces a 21.8KB app.mpy; editing-logic
harness + scene render still pass; and a simulated push (base64 -> 57 chunks -> a2b_base64)
reassembles the .mpy byte-exact and passes the device's header check.

One-time recovery: delete app.py from the drive, copy app.mpy + code.py from the new zip.
After that, updates are one-click again (and can't brick: header check + A/B rollback).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:01:57 -05:00
Me Here
7dd567fb44 PM_K-1 0.0.9: on-device editing (tap beats, save/revert) + Continue auto-advance
- Tap a beat to cycle it (off->normal->accent->ghost); the title turns red (unsaved).
  Tap the title -> SAVE / REVERT modal. Editing a built-in saves a COPY into a "My edits"
  user playlist (built-ins stay read-only); editing a user item updates it in place.
  Saves persist to programs.json (NAKs gracefully in editor mode / read-only).
- New round-trippable serializer (lane_to_str/_prog_str): parser now keeps groups + @db
  gain + ramp start; verified parse->serialize->parse on all 23 built-ins (0 mismatches).
- Continue (CONT) toggle, top-right of the tab line: when on, a playlist auto-advances to
  the next item at the end of each item's b<n> segment (no log spam, keeps the stopwatch).
- Touch routing consolidated: tab=switch playlist / CONT, title=save-revert, pads=cycle,
  log=delete; modal overlay drawn on top.

Verified in the harness: beat cycle+dirty, built-in edit -> My edits persisted (built-ins
untouched), revert, Continue arming at segment end, overlay SAVE-tap, and both renders.

Next (0.1.0): tap the instrument name -> lane-parameter table (reuses this save machinery).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:23:27 -05:00
Me Here
2d243c9ef8 PM_K-1 0.0.8: built-in playlists (baked, read-only) vs user playlists (separate)
The standard editor defaults (Styles / Practice / Song) are baked into app.py as
BUILTIN_SETLISTS (ASCII-fied — emoji/accents would break the 7-bit push + the fonts),
so they update with firmware and the user can't change or delete them. User playlists
live separately in programs.json and are merged after the built-ins.

Device:
- Set-list model: self.setlists = built-ins + user lists (deduped by normalized title,
  so a baked built-in always wins). load()/goto() work within the current list.
- Navigation: a set-list "tab" (small, above the title) shows playlist + position,
  muted for built-in / cyan for user; TAP it to switch playlists. Joystick L/R = item.
- SysEx 0x10 (push) writes programs.json -> rebuild user lists; built-ins untouched.
- Shipped programs.json is now empty ({"setlists":[]}) — built-ins come from firmware.

Editor:
- "Save to device" now syncs only YOUR set lists (filters out the built-in seeds) in the
  new {setlists:[...]} format; warns if you have none. Load-from-device imports both the
  new multi-list and old flat formats.

Verified in the harness: 3 read-only built-ins, set-list switching, user-list merge +
dedup of a pushed "styles", and the ramp engine on a built-in track (80->84->88, +4/4 bars).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 12:29:09 -05:00
Me Here
88104e3d5c PM_K-1 0.0.7: perform tempo ramps + gap trainer, show their indicators, log bars
Device parser now reads the rmp<start>/<amt>/<every> and tr<play>/<mute> tokens it
previously ignored, and the firmware performs them:
- Tempo ramp: steps bpm by <amt> every <every> bars (resets to the start at each b<n>
  segment boundary). Shows an amber ramp arrow + "+amt/everyb" (up/down by sign; no
  starting bpm, per request).
- Gap trainer: cycles <play> audible bars then <mute> silent bars (no click/MIDI/LED;
  playheads keep moving). Shows a play|rest symbol + "play/muteb".
- Practice log entries now record + show bars played.

Verified in the CPython harness: ramp 92->96->100->104->108 (+4 every 2 bars), gap
mute cycle play,play,mute,mute, and the on-screen ramp indicator renders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 11:43:54 -05:00
Me Here
5e71df6b17 PM_K-1: chunked firmware transfer (reliable), LED run/stop indicator, revert bg tint
APP_VERSION -> 0.0.6. Device firmware + editor change in lockstep — one-time manual
copy of 0.0.6 needed (the broken single-shot updater can't deliver it).

Update transport (fixes the failed/bricking updates):
- Editor now pushes app.py in 512-byte flow-controlled chunks: begin(0x21,len) ->
  data(0x22)* -> commit(0x23), waiting for each ACK before the next. A single ~38KB
  SysEx overran the device's USB-MIDI input buffer and arrived corrupt.
- Device writes chunks straight to /app.new, and on commit verifies length + no NUL +
  App().run()/APP_VERSION present before the A/B install; rejects (NAK) otherwise and
  keeps the working build. All errors caught -> never bricks.

Run/stop indicator moved off the screen onto the RGB LED (per feedback that recoloring
the whole background is wrong — it forces a full-screen SPI repaint and fringes the
anti-aliased text):
- Dim GREEN when stopped ("on"), dim RED while playing; the beat pulse flashes brighter
  and now decays back to the running base instead of to black. Background is static black.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 11:33:43 -05:00
Me Here
711a02fcc1 Fix firmware-update brick: app.py must be ASCII for the 7-bit MIDI push (+ guards)
Root cause: a non-ASCII em-dash in an app.py comment. The A/B updater pushes app.py
as 7-bit SysEx (charCode & 0x7F), which turned the em-dash's bytes into a NUL byte ->
corrupt source -> the pushed build crashed on boot (black screen, onboard LED blinking
CircuitPython's error/safe-mode pattern). A dragged copy was fine (valid UTF-8); only
the over-MIDI path mangled it.

- Replace the em-dash with ASCII; app.py is now pure ASCII.
- build.sh now ASSERTS pico-cp/app.py is pure ASCII (fails the build otherwise) so this
  class of bug can never ship again.
- Device 0x20 handler VALIDATES the pushed app.py before installing (reject if it
  contains a NUL byte, or is missing App().run()/APP_VERSION) and now catches ALL
  exceptions (not just OSError) -> a corrupt/truncated/oversized push NAKs and keeps the
  working build instead of bricking. Longer pre-reload sleep so the ACK flushes.

APP_VERSION -> 0.0.5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 10:14:13 -05:00
Me Here
2b113a18cc PM_K-1 firmware: show version (small, dim) top-right of the logo. APP_VERSION -> 0.0.4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 09:49:25 -05:00
Me Here
ca44aa833d PM_K-1 firmware: screen refinements (run-state bg tint, time/bar totals, per-track log, square/circle pads, separated track #)
App.py-only (ships over the one-click updater). APP_VERSION -> 0.0.3.

- Run/stop is now a background tint (gray running / near-black stopped) instead of
  STOP text, reclaiming the space.
- Running time + bar counter show "of total" when the track has a b<n> length:
  "1:23 of 2:00" and "bar N of 16" (bar cycles 1..N); total time derived from
  bars x master-beats-per-bar x 60/bpm. Parser now reads the b<n> token.
- Practice log is filtered to the current track (drops the redundant track column).
- Pads: squares for the main pulse, circles for subdivisions (was square + hollow
  outline); fewer vectorio shapes too.
- Track number set apart from the title (small + dim, right) so it no longer reads
  as part of the title.

On-device editing (tap instrument -> lane table; tap beat -> cycle state; dirty-name
-> confirm save/revert) is deferred to Phase 2, where "save" has a correct destination
(an edited built-in saves as a user copy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 09:41:57 -05:00
Me Here
dec6c61fce PM_K-1 firmware Phase 1: VARASYS logo, MIDI/USB status icons, square/outline beats, beat gridlines, stopwatch + bar counter
Device screen redesign (CircuitPython app.py), built proportional to WIDTH/HEIGHT
so it scales to other panels (one adaptive firmware, per-panel config — not a fork):

- gen_assets.py bakes logo.bin (VARASYS wordmark, no tagline), midi.bin (DIN-5),
  usb.bin (trident) as 4-bit-alpha bitmaps (same packing as the fonts).
- Header: VARASYS logo (brand cyan) replaces the "PM_K-1 KIT" text; MIDI icon goes
  green when a host is listening, USB icon lights when supervisor.runtime.usb_connected.
  load_alpha/make_glyph are non-fatal — a missing .bin falls back to text, never a
  black screen (addresses the corrupt-file failure mode we just hit).
- Pad grid: filled squares on main beats, hollow outline squares (outer+inner rect) on
  off-beats; playhead fills the lit pad. Vertical gridlines at the master lane's beats
  (full height) so beats line up across lanes.
- Stopwatch (m:ss) + bar counter (master-lane cycles), refreshed ~4x/s only on change.

The .bin assets ship in the drive bundle (the A/B updater only pushes app.py), so a
one-time re-copy is needed to pick them up. APP_VERSION -> 0.0.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 08:56:45 -05:00
Me Here
591fd8cfe5 Firmware versioning starts at 0.0.1 (was 1.0.0)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 08:12:27 -05:00
Me Here
e8945ee1d1 PM_K-1: one-click A/B firmware updates over USB-MIDI (+ version check)
Split the CircuitPython firmware into a tiny stable loader (code.py) + the application (app.py,
carries APP_VERSION). The editor's ⋯ → "⬆ Update firmware" queries the device version (SysEx 0x02
-> 0x03 reply), fetches the latest app from the site (/pico-cp-app.py), shows device-vs-latest, and
pushes the new app.py over USB-MIDI (SysEx 0x20). The device installs it to a trial slot (old build
kept as app.bak), reboots, and the loader AUTO-ROLLS-BACK to app.bak if the new build fails to start;
a build that runs cleanly ~5s is confirmed (clears /trial). No BOOTSEL, no dragging; Chromium/Firefox.
app.py forced to pure ASCII so it pushes raw (no base64); SysEx buffer raised to 60KB.

build.sh/deploy.sh: bundle code.py+app.py and serve /pico-cp-app.py. Docs updated.

Verified in CPython: version reply, update install+reboot+ACK, rollback file dance; editor loads clean
with the updater wired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 06:55:58 -05:00
Me Here
7d743c18a1 PM_K-1: appliance model — push-programming over USB-MIDI, on-device practice log, swing fix
Firmware (pico-cp/): the Pico now owns its filesystem by default (boot.py), so it can save the
practice log and write editor-pushed set lists; the drive is read-only to the computer, which also
protects the firmware. Hold button A at power-on for editor mode (drive writable; universal drag).
  - Replaced the on-screen touch buttons with an on-device PRACTICE LOG (time · BPM · duration ·
    track), newest-first, persisted to /history.json next to programs.json. Plays < 5s aren't logged;
    tap a row twice to delete it. Real timestamps once the editor syncs the clock.
  - USB-MIDI SysEx receiver: clock-set (0x01 -> RTC) and program-push (0x10 -> write programs.json,
    reload, ACK/NAK). disable autoreload so our own writes never self-restart.
  - Fixed swing: the parser was discarding the 's' flag, so /2s never swung. Now the scheduler uses a
    per-step duration with long-short (2:1, SWING_RATIO 2/3) pairs on even subdivisions, matching the
    web engine. Verified: ride:4/2s -> 266/133ms vs straight 200/200.

Editor (editor.html): requestMIDIAccess({sysex:true}); Save to device now pushes programs.json as
SysEx to the device (+ clock sync), waits for ACK, shows "Saved ✓", and falls back to downloading the
file (drag onto the drive in editor mode) when no device answers. Heartbeat also keeps the clock synced.
Web MIDI works in Chromium AND Firefox; the drag fallback covers any browser/OS incl. Safari.

Docs (pico-cp/README, info-kit, README) updated for the two modes, push programming, and the log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 00:38:08 -05:00
Me Here
09b20a9e69 PM_K-1: add firmware-protect helper (hide files so users only see editor + programs)
protect-firmware.sh sets the FAT hidden attribute on the firmware files (code.py, boot.py,
font_*.bin, README) on a mounted CIRCUITPY drive, so an end user sees only editor.html +
programs.json and can't accidentally delete the program — the hidden files keep running and
Save to device still works. Documented in pico-cp/README (incl. the read-only boot.py
hard-lock alternative) and bundled into pm_k1_circuitpy.zip. README.md verified accurate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:58:12 -05:00
Me Here
cc56741483 PM_K-1: on-screen MIDI indicator + auto-mute buzzer when a host is listening
The editor's 'Device audio' now sends a MIDI Active-Sensing heartbeat (0xFE, every 250ms)
to the device while on. The firmware reads usb_midi.ports[0]; while it hears the heartbeat
(<1s) it shows a green 'MIDI' badge top-right and silences the buzzer (the computer plays);
~1s after it stops, it reverts to the buzzer and hides the badge. Manual MUTE_BUZZER still
works. Verified headless: host detected -> MIDI shown + buzzer duty 0; timeout -> reverts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:38:53 -05:00
Me Here
ba8d57e7ab PM_K-1 CircuitPython: fix polymeter (~) timing — true ratio polyrhythm
The firmware ran every lane at a fixed beat/sub, ignoring the ~ flag, so a poly lane
(e.g. cowbell:3~) played quarter-notes instead of fitting its cycle into lane 1's bar —
the duple 'and' coincided with a triplet note. Now match the web engine: a poly lane's
whole cycle spans the master lane's bar (dur = master_bar / steps). Verified: claves:5~
over kick:4 -> both cycles = 2.400s (5-over-4); 3-over-2 lands correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:08:48 -05:00
Me Here
b5584454b3 PM_K-1 CircuitPython: add boot.py to guarantee the USB-MIDI port appears
If the device isn't seen as a MIDI input (USB endpoint pressure from drive+serial+HID+MIDI),
boot.py disables the unused HID interface and enables usb_midi — copy it on and power-cycle.
Bundled into pm_k1_circuitpy.zip; documented in the README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:57:23 -05:00
Me Here
7ccc75e399 Phase 3: USB-MIDI audio — play the device through the computer's speakers
Firmware (pico-cp/code.py): on every click, send a USB-MIDI note-on per firing lane —
GM drum note by voice (SOUND_GM), velocity by level (accent/normal/ghost) — via the
default-enabled usb_midi.ports[1]. Polyphonic, so the computer plays the full groove.
New CONFIG: MIDI_ENABLED (default on), MUTE_BUZZER (silence the buzzer when using
computer audio).

Editor (editor.html): a '🎹 Device audio' toggle uses the Web MIDI API
(requestMIDIAccess) to voice incoming notes through the existing synth — Note-On ->
GM_NUM[note] / velocity-to-gain -> playInstrument(). The device is the clock; the
browser is the sound module, locked in sync. Chrome/Edge.

Verified: firmware emits the right notes (kick+hat on beat 1 of four-on-the-floor,
snare's rest skipped); editor loads clean with the toggle + handlers present. Docs
(info-kit, both READMEs) updated. The on-device buzzer/screen still work standalone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:40:08 -05:00
Me Here
d558dccbde Phase 2: editor 'Save/Load to device' + bundle the editor on the drive
Add to the editor's set-list ⋯ menu:
  - 📟 Save to device — writes the active set list as programs.json (the same file the
    PM_K-1 firmware reads). Uses the File System Access API to write straight onto the
    CIRCUITPY drive (Chrome/Edge); falls back to a download to drag on. Reuses
    setupToPatch() per item -> {title, programs:[{name, prog}]}.
  - 📥 Load from device — reads a programs.json back into a new set list (patchToSetup
    per item; reuses the existing import path).
Bundle the built editor.html into pm_k1_circuitpy.zip so the drive carries its own
offline programmer. info-kit + pico-cp/README document the workflow.

Verified: editor loads with no console errors; both menu buttons + all four functions
present; zip contains editor.html. (FSA save needs a real user gesture to test on-device.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:20:35 -05:00
Me Here
6edb89e33c PM_K-1 grid: smaller circles + align beats vertically across lanes
Place step circles by proportional position in the bar (beat = column start) instead
of centring in per-lane slots, so same-meter lanes' beats land at the same x (e.g. the
8-step hat's beats sit directly under the 4-step kick/snare). Cap circle radius at 6
(was up to ~18). Verified by printing per-lane beat x-positions + rendering the grid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:16:36 -05:00
Me Here
ec43c694a1 PM_K-1 CircuitPython: circle pad grid, small labels, dimmer LED, faster SPI
From on-board feedback (works well; minor tweaks):
  - Pad grid uses circles now: big circle on each beat (division), small on the
    subdivisions (vectorio.Circle — native, no extra cost), coloured/lit as before.
  - Lane labels use a new small font (font_s.bin, ~12px via gen_font.py) so they're
    half-size and show more of the voice name (e.g. 'hatClos').
  - LED was blinding -> LED_BRIGHTNESS scale (default 0.15) applied on every write.
  - Residual tearing -> SPI back to 62.5 MHz (vendor speed; smaller tear window on a
    panel with no tearing-effect pin). Both are CONFIG flags.
Verified by rendering the full scene headless. font_s.bin added to gen_font.py + bundle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:07:00 -05:00
Me Here
c499910df4 PM_K-1 CircuitPython: add the lanes/pads view
Replace the single beat-dot row with a full pad grid: each lane is a row of step
pads coloured by dynamics (mute/normal/accent/ghost), with the playhead lit as it
plays (per-lane, so polymeter shows). Header (title/BPM/RUN/item) is compacted above
it; transport stays below. Pads are vectorio rects sharing one 8-colour palette and
recolour in place via color_index (cheap, tear-free); the grid only rebuilds on track
change. Caps at MAXLANES=5 rows (extra lanes still play). Verified by rendering the whole
displayio scene graph headless (layout + playhead lighting correct).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:56:27 -05:00
Me Here
ffededd05b PM_K-1 CircuitPython: self-contained RGB LED + fix screen tearing
From on-board feedback (memory + colours now good):
  - LED: drive the WS2812 via the core neopixel_write module (no neopixel library to
    install) — a tiny RGB class. Self-contained: it works straight from the bundle.
  - Tearing: switch displayio to auto_refresh=False and push a complete frame only when
    the scene changed (dirty flag, capped at the panel's refresh rate) so updates are
    never shown mid-paint. Beat dots now recolour in place (vectorio color_index) instead
    of being rebuilt every beat, shrinking the dirty region.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:43:48 -05:00
Me Here
111da49b5a PM_K-1 CircuitPython: fix MemoryError + red/blue swap (from on-board test)
On hardware the app rendered, beeped and took input, then died with MemoryError at
the text Bitmap alloc — the two ~37KB base64 font strings stayed pinned in RAM. Move
the fonts to small binary files read at boot (font_m.bin / font_l.bin), drop the
base64 + binascii, and gc.collect() before each text bitmap. code.py 56KB -> 20KB and
RAM use drops ~37KB+. Also: cyan rendered as yellow (R/B swapped) -> MADCTL 0x40 -> 0x48.
Bundle + README updated to include the font blobs. (LED still needs the neopixel lib.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:30:03 -05:00