The ~1s hitch was the once-per-second readout: show_stats() allocates text bitmaps (GC pause) and display.refresh() blocks the SPI blit, both stalling the step loop exactly every second. Now the rate is measured silently while spinning and the readout (steps + peak) is redrawn only when you release; a gc.collect() on release + before spinning keeps the heap clean. Steady spin does zero display work -> smooth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
PM_K‑1 "Kit" — CircuitPython edition (USB drive · push programming · MIDI audio · practice log)
The CircuitPython firmware for the 52Pi EP‑0172 Pico kit, set up as a self‑contained appliance.
It runs the same program‑string language as https://metronome.varasys.io. The simpler
MicroPython firmware (../pico/main.py) stays as a rock‑solid 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 + anti‑aliased text, plays the
speaker + RGB beat light, logs your practice to /history.json, accepts new set lists pushed
from the web editor over USB‑MIDI, and plays through your computer's speakers over USB‑MIDI.
Two power‑on 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 USB‑MIDI. The drive is then read‑only to the computer — which also protects the firmware from accidental deletion.
- Editor mode — hold BUTTON A alone 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. - Stepper jog/test mode — hold BUTTON A + B together while plugging in. A hidden screen where the joystick spins the stepper CW/CCW for bring-up (see Pendulum below). This chord stays in appliance mode (the drive is not flipped writable). Power-cycle with no buttons to return to normal.
Install
- Flash CircuitPython: hold BOOTSEL, plug in, drop the CircuitPython
.uf2ontoRPI‑RP2(https://circuitpython.org/board/raspberry_pi_pico/ — Pico 2 / W builds also fine). ACIRCUITPYdrive appears. - 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 oldapp.pyis on the drive, delete it — the firmware ships as precompiledapp.mpy. (Why: CircuitPython compiling the ~57 KB source at boot fragments the heap and runs the RP2040 out of memory; a.mpyloads without compiling.code.pyis a tiny stable loader; the one-click updater pushes a newapp.mpy. The.binassets ride in the bundle — if one is missing the firmware just falls back to text and never fails to boot.) - Power‑cycle (so
boot.pytakes effect). It boots into appliance mode and runs.
Program it from the web (push over USB‑MIDI)
In the editor (Chrome / Edge / Firefox), build a set list → set‑list ⋯ menu → 📟 Save to device.
The editor sends it to the Pico over USB‑MIDI (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 (one‑click, A/B with auto‑rollback)
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 USB‑MIDI
(base64, in flow‑controlled 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 USB‑MIDI 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 auto‑mutes (the computer plays instead). The editor also syncs the device clock, so the practice log gets real wall‑clock 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:00andbar 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 toprograms.json) in appliance mode and survives power‑cycles.
Pendulum (stepper motion) — optional
The Kit can drive a physical metronome pendulum: a 4-input unipolar stepper (e.g. a ULN2003 board + 28BYJ-48) swung in time with the beat, plus a matching pendulum drawn on the screen.
- Wiring: controller IN1..IN4 → GP18, GP19, GP20, GP21; controller GND → a Pico GND (shared ground). Power the motor from the controller's own supply, not the Pico. (These four pins are free on the EP‑0172 kit. On the custom PM_K‑1 board GP19/20/21 are already taken by SIG/CLIP LED + ground‑lift, so the production pendulum will need a pin reassignment.)
- Motion: the arm reaches an extreme exactly on each beat, then reverses; it reads the live beat clock, so it follows tempo ramps. Coils de‑energize when stopped. The on‑screen pendulum (shown over the practice log while playing) mirrors the arm exactly — and animates even with no motor wired.
- Config (top of
code.py):STEPPER_ENABLED— off leaves the four pins free.PEND_SWING_DEG— total swing arc end‑to‑end, in degrees (default 120). Single source of truth: drives the screen graphic exactly and the motor.STEPPER_STEPS_PER_REV— your motor's half‑steps per full turn (28BYJ‑48 half‑step ≈ 4096); maps degrees → steps.STEPPER_MAX_RATE— top half‑steps/sec the motor sustains smoothly. Jog mode spins at this rate, and the pendulum auto‑shrinks its arc (rather than desync) when a beat is too short to sweep the full angle.STEPPER_ACCEL— ramp (half‑steps/sec²) used to reach top speed without stalling; lower it if the motor stalls/buzzes when starting.STEPPER_JOG_START— jog kickoff rate from rest (keep at or below the motor's pull‑in rate).- Tune without recompiling: these five are also read from
/settings.json(keysstepper_max_rate,stepper_accel,stepper_jog_start,pend_swing_deg,stepper_steps_per_rev) — edit in editor mode, power‑cycle.
- Jog / test mode (hold A + B at boot): the joystick sets direction only — L = CCW, R = CW — and
the motor accelerates to
STEPPER_MAX_RATE(reversing decelerates through zero first), with an on‑screen needle + RGB LED. The step count + peak‑rate readout updates when you release (drawing mid‑spin would stall the step loop and make it jumpy, so the spin itself stays glitch‑free). Tuning: hold to spin, release to read the peak; raiseSTEPPER_MAX_RATEuntil the motor skips, then back off; if it stalls starting, lowerSTEPPER_ACCEL/STEPPER_JOG_START. Power‑cycle (no buttons) to exit.
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
MADCTLbetween0x48(default) and0x40. - Colours look negative: toggle
INVERT_COLORS. - Taps land wrong: set
TOUCH_DEBUG = True, read the raw coords over USB serial, then setTOUCH_SWAP_XY/TOUCH_INVERT_X/TOUCH_INVERT_Y. - Joystick reversed: toggle
JOY_INVERT_X/JOY_INVERT_Y. - Computer audio:
MIDI_ENABLED(default on);MUTE_SPEAKERforces the speaker off even standalone. - LED too bright/dim:
LED_BRIGHTNESS(0..1, default 0.15). - Screen tearing: SPI panels have no tearing‑effect 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 anti‑aliased blobs from ../pico/gen_font.py. protect-firmware.sh (hide
the firmware files) is mainly for editor mode — appliance mode already keeps the drive read‑only.