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

69 lines
6.7 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 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.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`).
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/` — 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.