# 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 . 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 1. **Flash CircuitPython:** hold **BOOTSEL**, plug in, drop the CircuitPython `.uf2` onto `RPI‑RP2` ( — 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` 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`), 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. ## 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`** (keys `stepper_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 and a **live step counter + rate readout**. *Tuning:* raise `STEPPER_MAX_RATE` until the motor starts skipping, then back off; if it stalls *starting*, lower `STEPPER_ACCEL` / `STEPPER_JOG_START`. Power‑cycle (no buttons) to exit. ## 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.