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