metronome/CLAUDE.md
Me Here 9b5d24edc0 docs(CLAUDE.md): refresh stale Rust/firmware status, document mobile PWA
Audited with the claude-md-management plugin's claude-md-improver skill:
- Rust port: codec + scheduler done and RP2350-ready; per-board firmware
  crates exist; PM_G-1 Grid now ships native Rust firmware (rust/pm-grid).
- Mark pico-scroll (CircuitPython) as the superseded Grid prototype.
- Document mobile.html / mobile-sessions.html as the installable PWA plus its
  manifest + mobile-sw.js service worker.
- Device-ID note: .mpy for CircuitPython editions, .uf2 for Grid.
- rust/run.sh covers the workspace; add the pm-grid firmware build command.

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

70 lines
7.3 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.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
VARASYS PolyMeter: a polymetric groove-trainer / metronome **engine** that ships in three guises from one repo — a set of self-contained web pages (editor + form-factor gallery + embeddable widget), MicroPython/CircuitPython **firmware** for real hardware, and a native **Rust** port. `README.md` is the product-level reference (share-language grammar, page list, features, keyboard shortcuts) — read it for "what does this token mean"; this file is for how the pieces fit and how to work on them.
## Commands
```sh
./build.sh # assemble self-contained pages into dist/ (also precompiles firmware .mpy + zips bundles)
./deploy.sh # build, stamp version, copy to the Caddy web root, smoke-test (auto-run after changes — see memory)
./release.sh [X.Y.Z] # bump VERSION (optional) + tag v<VERSION>; requires clean tree
node tests/run.mjs # track-format conformance: every golden vector through engine.js AND pico-cp/app.py
node tests/run.mjs -v # + print expected/actual diffs for unexpected failures
./rust/run.sh # cargo test (Rust workspace — track-format crate vs the same golden vectors), in container
./rust/run.sh cargo build # or `bash` for a shell
./rust/pm-grid/build.sh # build the PM_G-1 Grid firmware (pm-grid.uf2)
./hardware/eda/run.sh # interactive shell in the KiCad/ngspice container (lands in hardware/kicad/)
./hardware/eda/run.sh kicad-cli sch erc pm_k1_core.kicad_sch # ERC on the board schematic
./hardware/eda/run.sh ngspice -b ../eda/sim/<deck>.cir # run a SPICE deck
```
There is no lint step and no single-test filter — `node tests/run.mjs` is the one gate; its exit code is non-zero on any unexpected failure or round-trip break, so it doubles as CI.
## The track format is the contract
The "program" / "patch" / share-language string is the spine of the whole project: the same string drives the web editor, a hardware device, and the Rust crate. Its grammar is **hand-implemented in three places that can silently drift**, so it is formally specced and conformance-tested:
- **Spec (source of truth):** `docs/track-format.md`
- **Web:** `src/engine.js``patchToSetup` / `laneStrToCfg` / `setupToPatch` / `laneCfgToStr`
- **Firmware:** `pico-cp/app.py``parse_program` / `_parse_lane` / `lane_to_str` / `_prog_str`
- **Rust:** `rust/track-format/src/lib.rs`
- **Golden vectors:** `tests/fixtures/track-format.json` — each case has `in` (a patch), `norm` (expected meaning), a `status`, and optional `expectFail` listing impls known to differ today.
**Rule: any change to the grammar or its meaning must update the spec, add/adjust a golden vector, and keep all three implementations passing.** When you fix a divergence in one impl, delete it from that case's `expectFail`. The test adapters parse the *real* `engine.js` and `app.py` (the Python one via `ast` extraction) rather than copies, so a code change is what the suite actually sees.
## Web build system
Every deployed page is **one self-contained `.html` file, zero runtime dependencies** — no framework, no CDN, no audio samples (all voices are synthesized in Web Audio). Pages stay in sync by sharing code through build markers that `build.sh` resolves:
- `/*@BUILD:include:src/<file>@*/` inlines a shared partial. The important ones: `engine.js` (audio scheduler + DSL), `setlists.js` (seed set lists baked into every page), `base.css`, `header.html`/`footer.html`/`chrome.js`, `progbox.{html,js}` (program box), `infoembed.{html,js}` (info-page live widget), `livesync.js` (beta only).
- `@BUILD:favicon@` / `@BUILD:logo-dark@` / `@BUILD:logo-light@` inline base64 blobs from `assets/`.
`build.sh` asserts no `@BUILD:` markers survive. **`dist/` is generated and git-ignored — never edit it by hand.** Edit the source `*.html` in the repo root and the partials in `src/`; `deploy.sh` always builds first. Each page exists as a lean widget (`<device>.html`, what `?embed=1` serves) plus a spec page (`info-<device>.html` that embeds it). `editor.html` is the main app; `editor-beta.html` is identical plus `livesync.js` (live mirror to a connected device over Web-MIDI SysEx — protocol in `docs/livesync-protocol.md`). `mobile.html` is the installable phone/tablet **PWA** (with `mobile-sessions.html` as its practice journal); `build.sh` ships its PWA support files (`manifest.webmanifest` + the `mobile-sw.js` service worker for offline use).
State (set lists, practice log, theme) lives in `localStorage`; nothing is uploaded — share links encode everything in the URL hash (`#p=` patch, `#sl=` base64url set list).
## Firmware editions
All run the same DSL and `programs.json`. When you touch the grammar, the `pico-cp/app.py` half is covered by the conformance suite; the others are not, so keep them in step by hand.
- `pico/main.py` — single-file **MicroPython**, the simple no-computer fallback (52Pi EP-0172 Kit).
- `pico-cp/`**CircuitPython** appliance (USB-drive, push-programming over USB-MIDI, on-device practice log, one-click A/B firmware update). `build.sh` precompiles `app.py``app.mpy` (the RP2040 OOMs compiling the ~56KB source at boot).
- `pico-explorer/` — CircuitPython sibling for the Pimoroni Explorer (RP2350, buttons-only, no touch).
- `pico-scroll/`**superseded** CircuitPython prototype for the Pimoroni Pico Scroll Pack (PIM545: 17×7 mono LED matrix + 4 buttons on a plain RP2040 Pico). It was the UI prototype for the Grid; **the shipping Grid firmware is now the Rust `rust/pm-grid` crate** (`build.sh` no longer bundles the CircuitPython build). Kept for reference — see `docs/rust-port.md`.
Firmware device IDs (reported on the SysEx `0x02→0x03` version query, used by the editor's firmware-push to pick the right firmware — `.mpy` for the CircuitPython editions; Grid is a Rust `.uf2`): `K` Kit, `X` Explorer, `G` Grid.
**`pico-cp/app.py` and `pico-explorer/app.py` must stay pure ASCII** — they are pushed over USB-MIDI as 7-bit data, and a stray non-ASCII char gets mangled to NUL and bricks the device. `build.sh` enforces this with an assert.
## Hardware (PM_K-1 custom board) & Rust port — containerized only
Per the standing rule (see memory), **all EDA and Rust tooling runs in containers via the `run.sh` wrappers — never install these on the host.** Add tools to the respective `Containerfile`. `hardware/eda/` holds the KiCad 9 + ngspice environment; the board design is `hardware/DESIGN.md` + `BOM*.csv` + `kicad/` + the code-defined circuits in `eda/circuits/`. The Rust port is staged inside-out (`docs/rust-port.md`): the `track-format` codec **and** scheduler are done, passing the golden vectors, and build for the RP2350. Per-board firmware crates now exist under `rust/` (`pm-kit`, `pm-grid`, `pm-explorer`, plus support crates `pm-synth`/`pm-ui`/`uisim`/`glyphgen`); `pm-kit` runs on real hardware and **PM_G-1 "Grid" ships the native Rust firmware today** (`rust/pm-grid` → `pm-grid.uf2`, built by `rust/pm-grid/build.sh`).
## Versioning
`VERSION` holds the formal version. `deploy.sh` stamps the served pages: a clean tree on a commit tagged `v<VERSION>``X.Y.Z`; anything else → `X.Y.Z-dev.<utc-ts>.<sha>[.dirty]`. Source files keep a placeholder `APP_VERSION`; only the deployed copy is stamped.