metronome/CLAUDE.md
Me Here 400d896518 Add PM_G-1 "Grid" form factor (Pimoroni Pico Scroll Pack) + Rust core/driver plan
New form factor: a plain RP2040 Pico + Pico Scroll Pack (PIM545) -- a 17x7
single-colour LED matrix + 4 buttons. The 7x17 matrix maps onto the editor's
lane x step pad grid.

- pico-scroll/: CircuitPython firmware (DEVICE_ID "G"). Engine/scheduler/SysEx/
  live-sync copied verbatim from pico-explorer (engine byte-identical, so it stays
  on the track-format conformance lineage); vendored bulk-framebuffer IS31FL3731
  driver (pins/map verified from pimoroni-pico); three LED views (Grid/Pendulum/BPM);
  4-button input. Audio over USB-MIDI (no onboard speaker); optional P_BUZZER.
- grid.html + info-grid.html: widget page (canvas mirrors the 3 LED views) + spec
  page with a ~$29 BOM.
- Registered in build.sh (precompile + ASCII assert + pm_g1_circuitpy.zip), deploy.sh,
  embed.js, embed.html, index.html gallery, and both editors' FW_PATHS (device id G).
- docs/rust-port.md: core/driver architecture (pm-core no_std engine+protocol; per-board
  drivers behind embedded-hal/embedded-graphics traits). CLAUDE.md + livesync-protocol.md
  note the new edition + device id.

Python firmware stays in parallel with Rust (no abandonment yet).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 20:30:15 -05:00

6.7 KiB
Raw 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 track-format crate vs the same golden vectors), in container
./rust/run.sh cargo build  # or `bash` for a shell

./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).

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/ — CircuitPython sibling for the Pimoroni Pico Scroll Pack (PIM545: 17×7 mono LED matrix + 4 buttons on a plain RP2040 Pico). Engine/scheduler/SysEx copied from pico-explorer/; vendors a bulk-framebuffer IS31FL3731 driver; renders three LED views (Grid/Pendulum/BPM). No speaker (audio over USB-MIDI). This is the UI prototype for the eventual Rust pm-grid board — 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 .mpy): 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 pure track-format crate is done and passing; drivers/firmware come only after the engine is proven against the vectors.

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.