metronome/pico
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
..
_font_l.b64 PM_K-1 CircuitPython: circle pad grid, small labels, dimmer LED, faster SPI 2026-05-28 22:07:00 -05:00
_font_m.b64 PM_K-1 CircuitPython: circle pad grid, small labels, dimmer LED, faster SPI 2026-05-28 22:07:00 -05:00
gen_font.py PM_K-1 CircuitPython: circle pad grid, small labels, dimmer LED, faster SPI 2026-05-28 22:07:00 -05:00
main.py PM_K-1 firmware: smooth AA fonts, full set list, LED-off-on-stop, touch edge-detect 2026-05-28 17:11:30 -05:00
README.md PM_K-1 docs: make the two-step flash explicit (main.py is not drag-and-drop) 2026-05-28 16:42:18 -05:00

PM_K1 "Kit" — VARASYS PolyMeter firmware for the Raspberry Pi Pico

MicroPython firmware that turns a Raspberry Pi Pico on the 52Pi EP0172 "Pico Breadboard Kit Plus" into a touchscreen polymeter metronome. It runs the same program strings as https://metronome.varasys.io — design a groove in the web editor, copy its program string, paste it into PROGRAMS in main.py, and it plays here.

Everything is in one file: main.py (ST7796 display driver, GT911 touch, WS2812 RGB, buzzer, joystick, the polymeter engine — no external libraries).

The board (EP0172) — fixed pinout

Component Pico pins
3.5″ ST7796 320×480 display SPI0 — SCK GP2, MOSI GP3, CS GP5, DC GP6, RST GP7
GT911 capacitive touch I2C0 — SDA GP8, SCL GP9 (addr 0x5D)
WS2812 RGB LED GP12
Buzzer GP13
Button A / Button B GP15 / GP14
PSP joystick X = ADC0/GP26, Y = ADC1/GP27

The components are wired on the board — you don't breadboard anything; just seat the Pico.

Flash it — TWO separate steps

⚠️ main.py is NOT a drag-and-drop file. The RPI-RP2 drive that appears in BOOTSEL mode is the bootloader, and it only accepts a .uf2 firmware file — anything else (like main.py) is silently discarded on the next reboot. You first flash MicroPython with a .uf2 (drag-and-drop), and then copy main.py over the USB serial link with Thonny or mpremote. Two different steps.

Step 1 — install MicroPython (drag-and-drop a .uf2, one time)

  1. Download the MicroPython firmware .uf2 for your board:
  2. Hold BOOTSEL, plug into USB → the RPI-RP2 drive appears.
  3. Drag the .uf2 file onto that drive. It copies, the Pico reboots on its own, and the drive disappears — that's correct and means MicroPython is installed. (Don't use BOOTSEL again unless you're reinstalling the firmware.)

Step 2 — copy main.py (over USB serial, NOT to a drive)

After step 1 the Pico runs MicroPython and no longer shows up as a USB drive — so you can't drag files to it. Use a tool that talks to it over USB serial:

  • Thonny (easiest): install Thonny, plug the Pico in normally, then bottom-right click the interpreter selector → MicroPython (Raspberry Pi Pico) (you should see a >>> prompt in the Shell). Open main.py, then File ▸ Save as… ▸ Raspberry Pi Pico and save it as exactly main.py.
  • mpremote (command line): pip install mpremote then mpremote cp main.py :main.py

Reset (replug) and it boots straight into the metronome.

Controls

  • Touch: onscreen << / >|| / >> (prev · play/stop · next) and / TAP / +.
  • Joystick: up/down = tempo (push far for ±5), left/right = previous/next groove.
  • Button A (GP15): play / stop. Button B (GP14): tap tempo.
  • RGB LED flashes each beat (amber = accent, cyan = normal, violet = ghost); the buzzer clicks with matching pitch.

Add your own grooves

Edit the PROGRAMS list near the top of main.py — each entry is ("Name", "program string"). Get program strings from the web editor's program box (e.g. v1;t120;kick:4;snare:4=.X.X;hat:4/2). Supported: tempo t<bpm>, lanes sound:grouping[/sub][=pattern][~][!], pattern chars X accent · x normal · g ghost · . - _ rest, grouped meters like 3+3+2, polymeter ~. (Perlane dB gain @n is parsed but ignored — the buzzer is mono.)

If something looks off — calibration

All the knobs are flags in the CONFIG block at the top of main.py:

  • Colours look negative / washed out: toggle INVERT_COLORS.
  • Red and blue swapped: set SWAP_RB = True.
  • Taps land in the wrong place: set TOUCH_DEBUG = True, reset, watch the raw coordinates over the USB serial (Thonny shell) as you tap the corners, then set TOUCH_SWAP_XY / TOUCH_INVERT_X / TOUCH_INVERT_Y to match.
  • Joystick reversed: toggle JOY_INVERT_X / JOY_INVERT_Y; widen JOY_DEADZONE if it drifts.
  • Screen stays black: the backlight is hardwired on, so this usually means the SPI init didn't take — drop SPI_BAUD to 24_000_000 and retry.
  • Garbled / wrong size image: your panel lot may be a 240×320 ILI9341 instead of the 320×480 ST7796. This firmware targets the ST7796 you have (you said 320×480); if a unit ever ships ILI9341, set WIDTH,HEIGHT = 240,320 and use an ILI9341 init sequence.

Notes

  • Audio is a single passive buzzer, so coincident lane hits play one click at the highest priority (accent > normal > ghost); the RGB + screen still show the combined activity.
  • The scheduler is nonblocking and timed off time.ticks_us(), so tempo stays steady while the screen and inputs update.

Hardware reference: 52Pi EP0172 wiki · vendor code. VARASYS — Simplifying Complexity.