metronome/pico-cp
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
..
__pycache__ PM_K-1 0.0.16: BPM floor 30 -> 5; hamburger ☰ menu + Settings / Help / About 2026-05-30 07:50:55 -05:00
app.py PM_K-1 0.0.21: live-sync protocol (HELLO/FULL/DELTA/BYE) - device side 2026-05-30 09:59:19 -05:00
boot.py PM_K-1: appliance model — push-programming over USB-MIDI, on-device practice log, swing fix 2026-05-29 00:38:08 -05:00
code.py PM_K-1 0.0.10: ship precompiled app.mpy (fixes boot OOM) + push .mpy over the air 2026-05-29 14:01:57 -05:00
font_l.bin PM_K-1 CircuitPython: fix MemoryError + red/blue swap (from on-board test) 2026-05-28 21:30:03 -05:00
font_m.bin PM_K-1 CircuitPython: fix MemoryError + red/blue swap (from on-board test) 2026-05-28 21:30:03 -05:00
font_s.bin PM_K-1 CircuitPython: circle pad grid, small labels, dimmer LED, faster SPI 2026-05-28 22:07:00 -05:00
gen_assets.py PM_K-1 firmware Phase 1: VARASYS logo, MIDI/USB status icons, square/outline beats, beat gridlines, stopwatch + bar counter 2026-05-29 08:56:45 -05:00
logo.bin PM_K-1 firmware Phase 1: VARASYS logo, MIDI/USB status icons, square/outline beats, beat gridlines, stopwatch + bar counter 2026-05-29 08:56:45 -05:00
midi.bin PM_K-1 firmware Phase 1: VARASYS logo, MIDI/USB status icons, square/outline beats, beat gridlines, stopwatch + bar counter 2026-05-29 08:56:45 -05:00
programs.json PM_K-1 0.0.8: built-in playlists (baked, read-only) vs user playlists (separate) 2026-05-29 12:29:09 -05:00
protect-firmware.sh PM_K-1: add firmware-protect helper (hide files so users only see editor + programs) 2026-05-28 23:58:12 -05:00
README.md PM_K-1 0.0.14: gapless seam + continuous ramp + MIDI Clock Out (master); speaker rename 2026-05-30 07:11:19 -05:00
usb.bin PM_K-1 firmware Phase 1: VARASYS logo, MIDI/USB status icons, square/outline beats, beat gridlines, stopwatch + bar counter 2026-05-29 08:56:45 -05:00

PM_K1 "Kit" — CircuitPython edition (USB drive · push programming · MIDI audio · practice log)

The CircuitPython firmware for the 52Pi EP0172 Pico kit, set up as a selfcontained appliance. It runs the same programstring language as https://metronome.varasys.io. The simpler MicroPython firmware (../pico/main.py) stays as a rocksolid fallback — and the Pico can't be bricked (BOOTSEL → drag a MicroPython .uf2 back).

What it does: drives the 3.5″ touchscreen with a lanes/pads display + antialiased text, plays the speaker + RGB beat light, logs your practice to /history.json, accepts new set lists pushed from the web editor over USBMIDI, and plays through your computer's speakers over USBMIDI.

Two poweron modes (set by boot.py)

  • Appliance mode — default (just plug in / power up). The firmware owns the filesystem, so it saves your practice log and writes set lists the editor pushes over USBMIDI. The drive is then readonly to the computer — which also protects the firmware from accidental deletion.
  • Editor mode — hold BUTTON A while plugging in. The drive is writable by the computer, so you can drag programs.json / code.py / fonts on from any OS or browser (the universal fallback). Reset afterwards to return to appliance mode.

Install

  1. Flash CircuitPython: hold BOOTSEL, plug in, drop the CircuitPython .uf2 onto RPIRP2 (https://circuitpython.org/board/raspberry_pi_pico/ — Pico 2 / W builds also fine). A CIRCUITPY drive appears.
  2. Copy the whole bundle onto CIRCUITPY: boot.py, code.py (loader) + app.mpy (the application, precompiled), programs.json, font_s.bin / font_m.bin / font_l.bin, logo.bin / midi.bin / usb.bin (logo + MIDI/USB status icons), editor.html (offline editor), and the helper scripts. If an old app.py is on the drive, delete it — the firmware ships as precompiled app.mpy. (Why: CircuitPython compiling the ~57 KB source at boot fragments the heap and runs the RP2040 out of memory; a .mpy loads without compiling. code.py is a tiny stable loader; the one-click updater pushes a new app.mpy. The .bin assets ride in the bundle — if one is missing the firmware just falls back to text and never fails to boot.)
  3. Powercycle (so boot.py takes effect). It boots into appliance mode and runs.

Program it from the web (push over USBMIDI)

In the editor (Chrome / Edge / Firefox), build a set list → setlist menu → 📟 Save to device. The editor sends it to the Pico over USBMIDI (SysEx); the firmware writes /programs.json, reloads, and acknowledges — the editor shows Saved ✓. 📥 Load from device reads it back.

Universal fallback (any browser / OS, even Safari): Save to device downloads programs.json when no device answers — boot the Pico in editor mode (hold A) and drag the file onto the CIRCUITPY drive.

Firmware updates (oneclick, A/B with autorollback)

code.py is a small stable loader; the application is the precompiled app.mpy (it carries APP_VERSION). To update: the editor's ⋯ menu → ⬆ Update firmware… queries the device's version, fetches the latest app.mpy from the site, shows device vs latest, and on confirm pushes it over USBMIDI (base64, in flowcontrolled chunks; the device decodes each chunk to disk and verifies the .mpy header before installing). It goes to a trial slot (old build kept as app.bak) and reboots; if the new build doesn't boot, the loader automatically rolls back to app.bak. A build that runs cleanly for ~5 s is confirmed. No BOOTSEL, no dragging. (Updating CircuitPython itself still uses BOOTSEL + a .uf2, but that's rare. And the Pico is unbrickable as the ultimate backstop.)

Play through the computer's speakers

The Pico is a USBMIDI device and sends a note per click (GM drum note per lane, velocity by accent). In the editor click 🎹 Device audio, grant MIDI access, and press play on the device — the editor voices the groove through its full synth, out your speakers, locked to the device's clock. While a host is listening the screen shows a green MIDI badge and the speaker automutes (the computer plays instead). The editor also syncs the device clock, so the practice log gets real wallclock timestamps.

Playlists, editing & Continue

  • Built-in playlists (Styles / Practice / Song) are baked into the firmware — read-only, updated with firmware. Your own playlists live in programs.json (synced from the editor's Save to device).
  • Switch playlist: tap the set-list tab (above the title; grey = built-in, cyan = yours). Item: joystick left/right.
  • Edit on the device: tap a beat to cycle it (off → normal → accent → ghost); tap the instrument name for the lane editor (sound · beats · subdivision · swing · mute, plus + Lane / Remove). The title turns red (unsaved); tap the title to Save or Revert. Editing a built-in saves a copy into a My edits playlist (built-ins never change). Editing your own updates it in place.
  • Continue (auto-advance): tap CONT (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 (turn it on for the Song playlist).

Controls & the practice log

  • Joystick: up/down = tempo, left/right = previous/next groove.
  • Button A (GP15): play / stop. Button B (GP14): tap tempo.
  • Screen: VARASYS logo + firmware version + MIDI/USB status icons up top. Running time and bar count show of the segment total when the track has a bar length (b<n>), e.g. 1:23 of 2:00 and bar 3 of 16. A track with a tempo ramp (rmp) shows a ramp arrow + amount/every-bars (e.g. +4/2b); a gap-trainer track (tr) shows a play|rest symbol + bars (e.g. 2/2b). Main beats are squares, subdivisions are circles, with vertical gridlines lining the beats up across lanes.
  • RGB LED = run state: dim green when stopped ("on"), dim red while playing, with the beat pulsing brighter on top. (The screen background stays black — recoloring it forces a full-screen repaint.)
  • The firmware performs ramps (tempo steps every N bars) and gap-trainer cycles (silent rest bars).
  • Touchscreen: the bottom shows the practice log for the current track (time · BPM · duration · bars) — newest first. Plays under 5 s aren't logged. Tap a row to arm it (turns amber), tap again to delete.
  • RGB LED flashes the beat (amber accent / cyan normal / violet ghost); the speaker clicks to match.
  • The log is saved to /history.json (next to programs.json) in appliance mode and survives powercycles.

programs.json

{ "title": "PolyMeter",
  "programs": [ { "name": "Four on the floor", "prog": "t120;kick:4;snare:4=.x.x;hatClosed:4/2" } ] }

Each prog is a program string from the editor (tempo, lanes, patterns, /2 subdivision, /2s swing, (3,8) Euclid, ~ polymeter, @-3 dB). The push above is the easy way to update it.

Calibration (flags at the top of code.py)

  • Red/blue swapped: flip MADCTL between 0x48 (default) and 0x40.
  • Colours look negative: toggle INVERT_COLORS.
  • Taps land wrong: set TOUCH_DEBUG = True, read the raw coords over USB serial, then set TOUCH_SWAP_XY / TOUCH_INVERT_X / TOUCH_INVERT_Y.
  • Joystick reversed: toggle JOY_INVERT_X / JOY_INVERT_Y.
  • Computer audio: MIDI_ENABLED (default on); MUTE_SPEAKER forces the speaker off even standalone.
  • LED too bright/dim: LED_BRIGHTNESS (0..1, default 0.15).
  • Screen tearing: SPI panels have no tearingeffect sync; SPI_BAUD (default 62.5 MHz) is pushed fast to minimise it — lower only if unstable.
  • Blank / garbled: the panel lot may differ; drop SPI_BAUD, and if it's a 240×320 ILI9341 rather than the 320×480 ST7796, the init/size need changing (this targets the 320×480 you have).
  • RGB LED uses the core neopixel_write (no library to install).

If code.py ever errors, CircuitPython prints the traceback on the screen and over USB serial — send me that. The fonts are the baked antialiased blobs from ../pico/gen_font.py. protect-firmware.sh (hide the firmware files) is mainly for editor mode — appliance mode already keeps the drive readonly.