metronome/pico-cp/README.md
Me Here f2cf3e3ed9 pm-kit: jog stops promptly on release (drop the decel ramp)
Releasing the joystick used to ramp the speed down through zero (~0.3s), so the
motor coasted after release. Decel isn't needed (only fast *starts* stall), so
release now stops immediately; keep the gentle accel ramp on start. Also only
rewrite the PIO clock when the rate actually changes (no 100Hz redundant writes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:29:11 -05:00

158 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 *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
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.
## 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 EP0172 kit. On the custom PM_K1 board GP19/20/21 are already taken by SIG/CLIP LED + groundlift,
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 **deenergize when stopped**. The onscreen 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 endtoend, in degrees (default **120**). Single source of truth:
drives the screen graphic exactly and the motor.
- `STEPPER_STEPS_PER_REV` — your motor's halfsteps per full turn (28BYJ48 halfstep ≈ 4096); maps
degrees → steps.
- `STEPPER_MAX_RATE` — top halfsteps/sec the motor sustains smoothly. **Jog mode spins at this rate**, and
the pendulum **autoshrinks** its arc (rather than desync) when a beat is too short to sweep the full angle.
- `STEPPER_ACCEL` — ramp (halfsteps/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 pullin rate).
- *Tune without recompiling:* these five are also read from **`/settings.json`** (keys `stepper_max_rate`,
`stepper_accel`, `stepper_jog_start`, `pend_swing_deg`, `stepper_steps_per_rev`) — edit in editor mode,
powercycle.
- **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`** (gentle ramp on start so it doesn't stall; releasing stops it
promptly, no coasting), with an onscreen needle + RGB LED and a **live step count + rate readout**. The step pulses are generated by **PIO + DMA**
(hardwaretimed on a state machine), so the motor stays smooth even while the screen redraws — there's no CPU
step loop to stall. *Tuning:* hold to spin; raise `STEPPER_MAX_RATE` until the motor skips, then back off; if
it stalls *starting*, lower `STEPPER_ACCEL` / `STEPPER_JOG_START`. Powercycle (no buttons) to exit.
*(The beatpendulum during play still uses the simple step loop for now; it moves to the PIO driver next.)*
## programs.json
```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.