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>
121 lines
8.4 KiB
Markdown
121 lines
8.4 KiB
Markdown
# 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 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 `RPI‑RP2`
|
||
(<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. **Power‑cycle** (so `boot.py` takes 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: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 power‑cycles.
|
||
|
||
## 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 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.
|