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>
7.3 KiB
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
./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 hasin(a patch),norm(expected meaning), astatus, and optionalexpectFaillisting 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 fromassets/.
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.shprecompilesapp.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 Rustrust/pm-gridcrate (build.shno longer bundles the CircuitPython build). Kept for reference — seedocs/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.