# PM Live-Sync protocol (beta) Bidirectional live mirror between the **PM_E‑1 editor** (web) and a **PM_K‑1 device** (firmware). When armed, either side can edit a groove, change tempo/volume, start/stop, or select a set‑list item, and the other side reflects it in real time. It rides the **existing USB‑MIDI SysEx channel** (manufacturer `0x7D`) that the device link already uses for RTC / version / programs / firmware — no new transport, no new browser permission. - **Editor side:** implemented in `src/livesync.js` + hooks in `editor-beta.html`. - **Device side:** to be implemented in `pico-cp/app.py` (this document is the contract). - **Browser support:** Web MIDI = Chrome / Edge / Firefox (no Safari), same as the existing "Device audio" feature. --- ## 1. Frames Every message is one SysEx frame: ``` F0 7D F7 ``` `` lives in the free `0x40` block (existing ops: `0x01` RTC, `0x02/0x03` version, `0x10` programs, `0x21/22/23` firmware, `0x7E/0x7F` NAK/ACK): | op | name | direction | payload | |------|-------|------------------|-------------------------------------------| | 0x40 | HELLO | either → either | `` | | 0x41 | FULL | either → either | `;;;;;` | | 0x42 | DELTA | either → either | `;;` | | 0x43 | BYE | either → either | `` | - **Payload is 7‑bit ASCII** — never emit a byte > `0x7F` (it corrupts the SysEx stream and, per `build.sh`, would also break the firmware‑update path). All share‑language patch strings are already ASCII. - `` — a short per‑session id (the editor uses e.g. `e1a2b3c`). Used to drop your own echoes (see §4). - `` — a monotonically increasing integer per sender. Informational / duplicate‑drop; ordering is guaranteed by USB‑MIDI so no reordering logic is required. - `` — `0` or `1`. - `` / `` — set‑list and item index of the loaded program, or `-1`. - `` — a share‑language patch string (see §3). It contains `;` and `/`, so it is **always the tail**: parse the first 5 `;`‑fields, then rejoin the rest as the patch. --- ## 2. DELTA event grammar (``) One mutation, no `;` inside. Reuses the share‑language tokens (see `src/engine.js` / README "Share language"). | evt | meaning | |------------------------------|----------------------------------------------------| | `play` | start transport | | `stop` | stop transport | | `bpm=` | set tempo (clamped to the firmware's BPM range) | | `vol=` | master volume, 0–100 | | `sel=/` | cue/load a set‑list item | | `beat=//` | per‑step dynamics; level `0/1/2/3` = mute/normal/accent/ghost | | `lane=//`| lane field edit (see below) | `` and `` are **0‑based** indices into the current program's lane list / that lane's step list (same order both sides). `lane=` fields and values: | field | value | |-----------|------------------------------------------------| | `sound` | voice name (`kick`, `snare`, `hatClosed`, …) | | `groups` | grouping string, e.g. `2+2+3` | | `sub` | subdivision int: 1 / 2 / 3 / 4 / 6 | | `swing` | `0` or `1` | | `gain` | dB int, e.g. `-3` | | `poly` | `0` or `1` | | `enabled` | `0` or `1` (0 = silenced lane) | > Structural changes that re‑shape the lane list (add lane, remove lane, > reorder) are **not** sent as deltas. Send a fresh **`0x41` FULL** instead — it > is simpler and self‑healing. The editor does exactly this (a coalesced > full‑state push ~150 ms after the last structural/practice edit). --- ## 3. What each side emits vs. applies The two halves are **asymmetric in what they emit but symmetric in what they apply** — each must apply *every* op/evt listed above. **Editor emits:** - fine `0x42` deltas for `play`/`stop`, `bpm`, `vol`, `sel`, `beat` - a coalesced `0x41` FULL for any lane‑field / add / remove / practice (trainer, ramp, segment bars, countdown) edit - `0x41` FULL on connect and in reply to a received `0x40` **Device should emit** (from its on‑device input handlers): - `play`/`stop` when button A toggles transport - `bpm=` when the joystick / tap changes tempo (throttle to ≤ ~10/s) - `sel=/` on set‑list navigation - `beat=//` on a touch beat edit (`app.py` ~573–625) - a `0x41` FULL after any lane add/remove/reorder or multi‑field lane edit - a periodic `0x41` FULL **heartbeat** (~every 3–5 s) — the device is the convergence authority (see §4) - `0x41` FULL in reply to a received `0x40` The `patch` in a `0x41` is produced by the device's existing program serializer (the inverse of `parse_program()` in `app.py`). It must round‑trip through the editor's `patchToSetup()` — i.e. the same grammar already used for `programs.json` `prog` strings, plus a leading `t` and optional `vol`. --- ## 4. Echo / loop suppression and conflict policy Two rules keep the mirror from oscillating: 1. **Applying a remote change never re‑broadcasts.** Wrap every apply in an "applying remote" flag (the editor uses `_applyingRemote`) and have all of your broadcast hooks early‑out while it is set. This is the primary guard. 2. **Drop your own origin.** On receive, if `origin == myOrigin`, ignore the frame. (Belt‑and‑suspenders; also lets the editor's `?loopback=1` self‑test work by relabeling echoes as a peer.) **Convergence:** the **device is authoritative**. Its periodic `0x41` heartbeat is treated as ground truth, so if both sides edited the same field in the same instant, they reconcile within one heartbeat. To avoid flicker, a receiver should **diff the incoming `patch` against its current state and skip the rebuild if they're equal** (the editor does this in `_applyFull`), only reconciling transport. This is single‑user‑friendly (last‑writer‑wins per field). True simultaneous multi‑editor use is out of scope for the beta. --- ## 5. Handshake & lifecycle ``` editor "Live sync" ON ─► 0x40 HELLO ─────────────► device ◄──────────── 0x41 FULL ◄── (device's current state) editor 0x41 FULL ──────────────────────────────► (editor's current state) … steady state: 0x42 deltas both ways, device 0x41 heartbeat … editor "Live sync" OFF ─► 0x43 BYE ────────────► device ``` On connect the editor sends **both** a `0x40` (asking for the device's state) and a `0x41` (offering its own), so whichever side the user considers "source of truth" wins immediately. A device that boots with sync idle should simply answer `0x40` with a `0x41` and start emitting deltas once it has heard from a peer. --- ## 6. Firmware checklist (`pico-cp/app.py`) - [ ] **Dispatch** `0x40/0x41/0x42` in the SysEx handler (~`app.py:1361‑1415`, alongside `0x01/0x02/0x10/0x21‑23`). Ignore frames whose origin is your own. - [ ] **HELLO (`0x40`)** → reply `0x41` FULL built from current `App` state (running, sl/idx, serialized program). - [ ] **FULL (`0x41`)** → diff vs. current program; if different, load it (reuse `parse_program()` / the `programs.json` load path); then reconcile `running` (start/stop). Wrap in your remote‑apply flag. - [ ] **DELTA (`0x42`)** → apply `play/stop/bpm/vol/sel/beat/lane` to `App` state, wrapped in the remote‑apply flag so the on‑device handlers don't re‑broadcast. - [ ] **Broadcast** a `0x42` from each on‑device input handler (button A, joystick tempo, touch beat edit, set‑list nav, lane editor), guarded by the remote‑apply flag. Structural lane changes → `0x41` FULL. - [ ] **Heartbeat:** emit `0x41` FULL every ~3–5 s while a peer is connected. - [ ] **BYE (`0x43`)** → mark the peer gone (stop heartbeating/emitting until the next HELLO). - [ ] **Throttle** high‑rate sources (joystick tempo) and keep frames small — the RP2040 USB‑MIDI RX buffer is tiny (the firmware updater already chunks at 64 bytes), and live traffic shares the bus with MIDI clock, note‑out, and the editor's Active‑Sensing heartbeat. Don't let a flood stall a concurrent firmware push. ### Built‑in vs. user set lists (must match the editor) The PM_K‑1's built‑in playlists (Styles / Practice / Song) are **baked into firmware and read‑only**; on‑device edits **copy‑on‑write** into the user "My edits" list. The editor follows the same rule (`userSetlists()` excludes the seeded titles). So a **remote edit that targets a built‑in must follow the same copy‑on‑write semantics** on the receiving side, or the two halves will disagree about where the edit landed. When in doubt, after such an edit send a `0x41` FULL with the resulting (copied) program so both sides converge on the same target. ### Out of scope for the beta - Streaming the device practice log (`history.json`) up to the browser. - Mirroring device `settings.json` (LED brightness, MIDI config, etc.). - Multi‑peer / multi‑editor arbitration beyond last‑writer‑wins. --- ## 7. Per‑device emit/apply matrix Both targets implement the **full apply path** for every verb. They differ in what they **emit**, because on‑device editing differs: | Device | Emits | Applies | |-------------|----------------------------------------------------|---------------------------------------------| | **PM_K‑1** Kit (touchscreen + joystick) | `play` / `stop` / `bpm` / `sel` / `beat` / `lane` (FULL on structural lane edits) | all of the above | | **PM_X‑1** Explorer (6 buttons, read‑only beats) | `play` / `stop` / `bpm` / `sel` only (no on‑device beat/lane editing) | all of the above | Editors don't need to special‑case the source — both DELTA streams look identical on the wire, and the **device id is only exposed on the version query** (SysEx `0x02` → `0x03` reply, `;`; pre‑0.0.23 firmware sends bare version → assume `K`).