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

7.3 KiB
Raw Permalink Blame History

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.jspatchToSetup / laneStrToCfg / setupToPatch / laneCfgToStr
  • Firmware: pico-cp/app.pyparse_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.pyapp.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-gridpm-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.