Mirror the pm-kit.uf2 block — copy rust/pm-grid/pm-grid.uf2 to the web root if
built, so it downloads from metronome.varasys.io/pm-grid.uf2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopt proper embedded tooling for the blank-screen debug (user has a Pi Debug Probe):
- flip-link linker (baked into pm-rust:2): stack overflow faults cleanly instead of
silently corrupting .bss/.data (the SPI buffer -> black screen class of bug).
- defmt + defmt-rtt + panic-probe: firmware logs boot/heap-free/display/parse/loop
heartbeat over RTT; panics print message+location. .cargo runner = probe-rs run.
- Restore the full live metronome (from 08b0940) as the instrumented target.
- deploy + serve pm-kit.elf (probe-rs decodes defmt strings from the ELF).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copies the built Rust .uf2 to the web root when present, so it can be flashed from
the website (alongside the CircuitPython firmware downloads). Conditional — only
served if rust/pm-kit/build.sh has produced it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
A visual mockup of the Pimoroni Explorer Kit (PIM744) - yellow PCB body,
6 coloured buttons (A/B/C left, X/Y/Z right) flanking the central 320x240 LCD,
piezo + mini-breadboard at the bottom, USB-C / RP2350 silkscreen.
Canvas mirrors the firmware UI: VARASYS logo + version + run-state dot in the
header (3-channel lerp for the per-beat pulse, no more red-channel-only blend
bug); setlist tab + track name + big BPM + bar/time meters; pad grid up to
6 lanes with main-beat squares + subdivision circles + vertical gridlines.
Inputs: A = play/stop, B = tap, C = next setlist; X/Z = prev/next track with
350 ms first-repeat + 120 ms repeat; Y = -1 bpm (after 1.5 s held step = -5);
X+Z chord within 100 ms = +1 bpm (mirrors Y). Keyboard: A B C X Y Z + space.
Landing page (index.html) Explorer pane now points at /explorer.html with
h:500 (the LCD is half-height of the Kit's so the widget is more compact).
The /info-explorer.html embed handler still works for the "Specs & info" link.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds pico-explorer/ as a parallel CircuitPython firmware target alongside the 52Pi
Kit in pico-cp/. Same engine, same program-string grammar, same programs.json, same
live-sync protocol. Read-only on the device (no on-device beat editing); the web
editor's Live sync mirrors all edits in real time and the Explorer emits its own
play/stop/bpm/sel deltas back.
Hardware (Pimoroni Explorer PIM744):
- RP2350B + 2.8" ST7789V 320x240 LCD (8-bit parallel; CircuitPython's official
board definition pre-builds the BusDisplay so we just use board.DISPLAY).
- 6 user buttons - A/B/C on the left of the screen, X/Y/Z on the right.
- Piezo speaker on GP12 (PWM) with amp enable on GP13.
- I2C QwSTEMMA on GP20/21 - reserved, unused by the firmware.
- No touchscreen, no joystick, no RGB LED. Run state shows on a tiny on-screen dot.
Buttons:
- A = play/stop. B = tap tempo. C = menu.
- X = prev track (hold-repeat). Z = next track (hold-repeat).
- Y = tempo -1 (hold-repeat; -5 after 1.5s).
- X+Z chord = tempo +1 (mirrors Y).
- In a menu: X/Z move the row cursor, Y decrements, A cycles/increments/selects,
B = back, C = close.
Files added:
- pico-explorer/{boot.py, code.py, app.py, programs.json, README.md}.
app.py = 1444 lines (~73KB source -> 29.8KB compiled .mpy).
- info-explorer.html.
Files touched:
- pico-cp/app.py: bump to 0.0.23. Version-query (SysEx 0x02 -> 0x03) reply now
includes the device id as "K;<version>" (backward-compat: editor parses
"contains ';'?" - old firmware sent bare version, treated as K).
- editor.html + editor-beta.html: _parseDeviceReply() splits id;version, FW_PATHS
maps id to .py/.mpy URL pair, so Update firmware now pushes the right binary.
- build.sh + deploy.sh: precompile pico-explorer/app.py -> dist/explorer-app.mpy,
zip pm_x1_circuitpy.zip alongside pm_k1_circuitpy.zip, ship
pico-explorer-app.{py,mpy} next to pico-cp-app.{py,mpy}.
- docs/livesync-protocol.md: new section 7 - per-device emit/apply matrix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New editor-beta.html: a bidirectional live mirror over the existing USB-MIDI
SysEx channel (0x7D). Either the website or the device can edit grooves, change
tempo/volume, start/stop, or select set-list items, and the other reflects it.
- src/livesync.js: LiveSync layer (opcodes 0x40 HELLO / 0x41 FULL / 0x42 DELTA /
0x43 BYE) riding the existing _ensureMidi/_send/onDeviceMidi plumbing. Fine
deltas for transport/bpm/vol/sel/beat, coalesced full-state for structural
edits; echo suppression via origin + _applyingRemote guard; device-authoritative
heartbeat reconciles drift. ?loopback=1 self-test mode (no hardware needed).
- editor-beta.html: copy of editor.html + "Live sync" toggle, SysEx routing,
and broadcast hooks at each mutation choke point (guarded by _applyingRemote).
- docs/livesync-protocol.md: wire spec + firmware checklist for pico-cp/app.py
(firmware half owned by the other instance — editor side + spec only here).
- build.sh / deploy.sh: add editor-beta.html to the build + version-stamp loops.
Editor side only; pico-cp/app.py untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The single-file app grew to ~57KB; CircuitPython compiling it at boot fragments the
RP2040 heap so badly that the fonts can't get a contiguous block (161KB free, yet a
~16KB alloc fails). Fix: precompile to app.mpy (Adafruit mpy-cross for CP 10.2.1, emits
CircuitPython mpy v6) so the device loads bytecode without compiling -> no fragmentation.
- build.sh precompiles pico-cp/app.py -> dist/app.mpy via tools/mpy-cross (gitignored
binary); the bundle ships app.mpy (NOT app.py); serves pico-cp-app.mpy + pico-cp-app.py
(the .py only for the editor's version regex + as readable reference).
- Loader (code.py) imports app.mpy and rolls back app.bak as .mpy.
- One-click updater now pushes the .mpy: editor base64-encodes it and sends it over the
existing flow-controlled chunked transport (512-char = mult-of-4 chunks); the device
base64-decodes each chunk to /app.new and verifies the CircuitPython .mpy header
(magic 'C', v6, >=4KB) before the A/B install. Version still read from the served .py.
Verified: mpy-cross emits magic 'C'/v6; build produces a 21.8KB app.mpy; editing-logic
harness + scene render still pass; and a simulated push (base64 -> 57 chunks -> a2b_base64)
reassembles the .mpy byte-exact and passes the device's header check.
One-time recovery: delete app.py from the drive, copy app.mpy + code.py from the new zip.
After that, updates are one-click again (and can't brick: header check + A/B rollback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Split the CircuitPython firmware into a tiny stable loader (code.py) + the application (app.py,
carries APP_VERSION). The editor's ⋯ → "⬆ Update firmware" queries the device version (SysEx 0x02
-> 0x03 reply), fetches the latest app from the site (/pico-cp-app.py), shows device-vs-latest, and
pushes the new app.py over USB-MIDI (SysEx 0x20). The device installs it to a trial slot (old build
kept as app.bak), reboots, and the loader AUTO-ROLLS-BACK to app.bak if the new build fails to start;
a build that runs cleanly ~5s is confirmed (clears /trial). No BOOTSEL, no dragging; Chromium/Firefox.
app.py forced to pure ASCII so it pushes raw (no base64); SysEx buffer raised to 60KB.
build.sh/deploy.sh: bundle code.py+app.py and serve /pico-cp-app.py. Docs updated.
Verified in CPython: version reply, update install+reboot+ACK, rollback file dance; editor loads clean
with the updater wired.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New pico-cp/ — a CircuitPython port of the PM_K-1 firmware so the Pico mounts as a
CIRCUITPY drive carrying its code + tracks (the MicroPython pico/main.py stays the
simple fallback):
- pico-cp/code.py: displayio BusDisplay driving ST7796 via a custom init_sequence;
smooth anti-aliased text via displayio Bitmap+Palette (reuses the baked font blobs);
vectorio rects for dots/buttons; DIY GT911 touch (16-bit regs, edge-detected);
pwmio buzzer, analogio joystick, digitalio buttons, optional neopixel RGB; the
polymeter engine on a time.monotonic_ns scheduler. Reads /programs.json (falls back
to baked defaults); CircuitPython auto-reloads on file change.
- pico-cp/programs.json: the 23 default grooves. pico-cp/README.md: flash + calibrate.
- build.sh/deploy.sh: bundle + serve /pm_k1_circuitpy.zip. info-kit.html: experimental
'CircuitPython edition — USB drive' section.
Verified in CPython (stubbed displayio): init sequence well-formed, parser handles the
grooves incl. (3,8) euclid + @-4 gain, and code.py's actual make_text renders identical
smooth AA text. Hardware bits (panel/touch/MIDI) await on-board testing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A new, first-actually-buildable form factor for the 52Pi EP-0172 "Pico Breadboard
Kit Plus" (Raspberry Pi Pico; 3.5" ST7796 320x480 cap-touch via GT911, PSP
joystick on ADC0/1, WS2812 RGB on GP12, buzzer GP13, buttons GP14/15):
- pico/main.py — one self-contained MicroPython file: ST7796 direct-draw driver,
GT911 touch (16-bit register addressing), WS2812 RGB (neopixel), PWM buzzer,
ADC joystick, buttons. It parses the project's own program-string language
(verified against the web engine's semantics) and runs a non-blocking
ticks_us scheduler with an on-screen touch UI. CONFIG flags cover panel /
colour / touch / joystick calibration. pico/README.md has flashing +
calibration steps.
- kit.html — lean widget that mirrors the firmware's on-screen UI (portrait
320x480 canvas) plus a joystick / RGB / buzzer / A-B buttons; plays via the
shared engine. info-kit.html — the real EP-0172 pinout, a parts list
(~$45 incl. Pico) and the firmware to flash (downloads /pico-main.py, links
the README + source).
- Landing + embed page list the Kit; build.sh/deploy.sh build the two pages and
serve pico/main.py as /pico-main.py for download.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously every device .html bundled its full narrative (purpose, BOM,
dimensioned drawings, embedding docs) inside a #techinfo block that embed
mode (?embed=1) only CSS-hid — so every embedder and every landing-page
iframe downloaded all of it (showcase 77%, teacher 49% of the file) for
content no embedder ever sees.
Now:
- <device>.html is the lean widget only: header + front view/controls +
title + summary + program box. ?embed=1 still collapses to the bare
widget; the heavy narrative is gone from the payload.
- info-<device>.html (new, one per form factor) carries all the words —
purpose, dimensions, priced BOM, embedding docs — and embeds the live
widget at the top via the existing iframe + auto-resize protocol
(new shared src/infoembed.html + src/infoembed.js).
- Each device links out to its info page ("…dimensions & BOM →"); the
landing panes and viewport bar now offer both Open ↗ and Specs & info ⓘ.
- Dropped the now-dead "Show info" toggle (CSS + progbox.js).
Branding: adopt the official VARASYS "tagline on the bottom" logos from the
brand kit (light-background variant now matches; dark already did). The
tagline is baked into the PNGs, so remove the CSS .brand-tag / .dev-tag
spans and the showcase canvas-drawn tagline. Brand cyan #0AB3F7 / navy
#1C283F already match the official palette.
build.sh / deploy.sh: build + deploy the six new info-*.html pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Shared header/footer/chrome (src/header.html, src/footer.html, src/chrome.js)
now on every page: editor (header above its app toolbar), player, teacher,
stage, micro, showcase, embed. chrome.js defers to DOMContentLoaded so the
footer version stamps regardless of placement. Player's fullscreen toggle
relocated out of the header to a floating control.
- Open = Info: each form-factor page is self-contained — a more-detailed
description (.about) + an expandable "Spec & BOM" (<details class="spec">,
hidden in embed). info-*.html retired; build/deploy/README updated.
Next: teacher-style dimensioned front + top/side views + loading panels for
Stage, Micro and Showcase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Concepts is now the landing (/): index.html is the form-factor gallery with the
LIVE widget embedded in every box (editor/teacher/stage/micro/showcase/initial),
on the shared header/footer. concepts.html retired; every "Concepts" link → /.
- New shared chrome partials src/header.html, src/footer.html, src/chrome.js
(assembled by build.sh) + .site-foot / details.spec styles in base.css. Applied
to the landing + showcase this pass.
- Showcase redesign per spec: the pendulum bar IS the display — each lane's
subdivisions/accents ride along the rod as moving RGB light (all meters combined);
transparent outside the body (no black window); a printed tempo scale on the
vertical axis with a draggable weight to set tempo; start is an external button
(the real unit starts when lifted from its holder).
Next pass: roll the shared header/footer onto the remaining pages (incl. the editor
header-above-toolbar), merge Open=Info into one page per form factor with the
expandable Info & BOM, and add teacher-style dimensioned views to Stage/Micro/Showcase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sync: the visual playhead now advances on a latency-compensated clock
(currentTime − outputLatency||baseLatency) so the on-screen pulse lands when the
click is HEARD, not when it's queued — previously the visual could lead the audio
by the output buffer / Bluetooth latency (up to ~a subdivision). Applied to
editor, player, teacher, and the new pages; also bound the visual queue (vq trim).
No data races: single-threaded; only the rAF draw touches vqPtr/currentStep, and
each vq entry carries the exact scheduled time of its sound.
stage.html — foot-pedal stompbox: two heavy footswitches (Tap=tempo / hold=start-
stop, Next=item / hold=prev), 1/4" expression-pedal input → tempo sweep, big
floor-readable RGB beat light + angled TFT, analog instrument pass-through.
showcase.html — pyramid display piece: an RGB-light pendulum easing to each beat
plus per-lane segment rows showing subdivisions/accents/mutes (canvas).
Both: dual USB-C (data+power and power-thru) to daisy-chain off one source.
Wired into embed.js (stage, showcase variants), build.sh, deploy.sh, the
concepts gallery + landing cards, info-stage.html (~$52) + info-showcase.html
(~$39) with BOMs, and the README.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The full-feature desktop console (big TFT, arcade buttons, instrument pass-through)
is repositioned as the "Teacher" for studio desks and lessons:
- stage.html -> teacher.html, info-stage.html -> info-teacher.html (git mv)
- all links/paths, the embed variant (stage -> teacher), nav, cards, embed docs,
README, build.sh + deploy.sh updated; deploy cleans the stale live stage files
The "Stage" name is now free for a forthcoming foot-pedal stompbox (/stage.html).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The site now opens on a proper front door: a hero (logo + tagline + pitch),
an "Open the Editor →" CTA, and form-factor cards (Editor, Stage, Micro, Embed)
linking out to each page + info. The PE-1 editor app moves from index.html to
editor.html; every "Editor"/"Open" link, the embed.js editor variant, and the
editor's own brand-logo (now → /) are repointed. build.sh + deploy.sh build and
publish both index.html (landing) and editor.html (app).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each form factor now has an information page (purpose + live embedded widget),
with priced BOMs only on the buildable hardware (Stage ≈$59, Micro ≈$28):
- info-editor.html / info-initial.html — purpose only (web app / concept)
- info-stage.html — purpose + the priced BOM moved out of stage.html
- info-micro.html — purpose + a new ~$28 practice-unit BOM
stage.html drops the BOM panel (+ its .bom CSS) and gains a "Spec & BOM" link;
the shared .bom/.sub table CSS lives in src/base.css. "Info" added to every
page nav and to the concept cards. Wired into build.sh + deploy.sh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Every form factor supports ?embed=1: a head flag (set on <html>, no flash)
strips the site chrome (base.css [data-embed]) + page-specific panels, leaving
just the widget, and posts its height so the host can auto-size it.
- Config/settings string preloads via the existing #p=/#sl= hash. Added that
hash handling to micro.html (it previously only loaded built-in tracks).
- New embed.js loader: <div data-varasys-metronome="micro" data-patch="…"> + one
<script> → an auto-sizing iframe to <page>?embed=1#p=…. New embed.html documents
it and dogfoods a live embedded widget.
- "Embed" nav link added across pages; build.sh/deploy.sh build embed.html and
serve embed.js.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Shared site header in src/base.css (.site-head/.site-nav/.brand-logo + theme-
aware logo + .tbtn). Applied to player/stage/micro (replacing their text
topbars) so the VARASYS logo + tagline + Editor/Concepts nav is on every page.
- Rebrand the editor: "Stackable Metronome" → "PE-1 — PolyMeter Editor" (title +
h1), with a Concepts link in its header.
- New concepts.html — the PolyMeter Concepts library: cards for the editor and
each form factor (PM-1 Initial/Stage, PM-µ Micro) + a "more coming" card.
- build.sh + deploy.sh build/deploy concepts.html; deploy.sh now loops over pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rename player-asbuilt.html → stage.html (the pedalboard build). Update
build.sh + deploy.sh (deploy now also removes the old player-asbuilt.html
from the web root) and the cross-links in player.html / stage.html.
- New /micro.html — a stripped-down home-practice metronome on the same RP2040
firmware. Hardware is just: ONE depressable scroll/rotary encoder, a red
7-segment LED display, a speaker, and USB-C for power. The encoder does
everything: spin = tempo, press = start/stop, hold + spin = switch track
(the LED shows the track number, with BPM / TRACK / ▶ indicators). Tracks =
the editor's seed grooves flattened (23). Shares src/engine.js, setlists.js,
base.css; synth-only; steady practice loop (ramps/bars ignored).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New /player-asbuilt.html showing the PM-1 with parts you'd actually solder
for an RP2040 build, alongside the idealized /player.html:
- 128×64 MONOCHROME OLED (SSD1306 class): rendered as a true 1-bit
framebuffer — drawn, then thresholded to crisp on/off pixels and scaled
with image-rendering:pixelated — so the cramped real layout is honest
(position / big BPM / grouping / scrolling name / bar·beat).
- Fixed 16-px WS2812 ("NeoPixel") RGB beat bar on a strip PCB: lights the
first beatsPerBar slots (cyan downbeats, amber group-starts, dim others),
the rest dark — showing the fixed-count hardware honestly.
- EC11 rotary encoder you actually turn (wheel / vertical drag) for tempo,
tactile buttons, MAX98357A-style speaker grille, USB-C, PWR LED, matte case.
Shares the same firmware via src/engine.js + src/setlists.js (same seed set
lists, same scheduler); only the panel rendering differs. The device is fixed
dark hardware; the page chrome follows light/dark/system. build.sh + deploy.sh
now assemble/serve all three pages; player.html links to it ("As-built ↗").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The source index.html now keeps small @BUILD:* markers instead of the
~250KB of base64 blobs (audio samples, logos, favicon), which move to
assets/. build.sh inlines them into a self-contained dist/index.html
(+ dist/player.html); deploy.sh runs the build first and serves dist/.
dist/ is git-ignored. Keeps the single-file deploy while stopping the
samples from eating the editing budget.
Also reframe the main page as the full web app (it is not a mockup —
only the play-only player.html device is): drop "Mockup" from the title,
the source comment, and the README intro; add Build/Files docs and
correct the "no build step" claim.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A self-contained simulator of the RP2040 "PM-1" unit: it plays the share
language (synth voices, same scheduler) and drives an OLED + beat-LED
display like the firmware would. Loads from a #p=/#sl= link, the editor's
saved set lists (localStorage), or a pasted patch / set-list code — with
validation. Transport: play/stop, prev/next item, tempo ±, tap; bar-count
segments auto-advance. deploy.sh now version-stamps and publishes it too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Each meter lane has an 'enable' checkbox right after its number (default on,
green-dim row when off); replaces the right-side 'mute'. Renamed mute→enabled
throughout (scheduler, snapshot, share '!' flag, 1–9 keys, now-playing). Old
saved data still loads (back-compat).
- Features area redesigned into highlighting boxes (Gap trainer / Tempo ramp /
Timers); trainer & ramp boxes light up + un-dim when enabled.
- Set list 'Continue' mode: per-item countdown (saved in each item, 'cd' token),
and when a playing item's countdown hits 0 it auto-loads the next — so a list
with countdowns plays straight through.
- Removed the in-app QR (vendored qrcode.js); 'QR ↗' now opens api.qrserver.com
with the link, behind a banner warning it's a third party (verify it decodes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Share menu now opens a dialog with a copyable link AND a QR code (scan to open
on a phone); generated locally by vendored qrcode-generator (MIT). deploy.sh
publishes qrcode.js alongside index.html.
- README documents the share language grammar, sounds, URL forms, shortcuts,
versioning and deploy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- VERSION holds the formal version (0.0.1), single source of truth.
- deploy.sh stamps the served HTML: "X.Y.Z" on a clean commit tagged v<VERSION>,
else "X.Y.Z-dev.<utc-ts>.<sha>[.dirty]".
- index.html shows the build version in the header badge (APP_VERSION).
- release.sh cuts a formal release (sets VERSION + tags v<VERSION>).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
index.html: single-file browser mockup of the polymetric groove trainer —
stackable meter lanes, subdivisions, odd-meter grouping, per-beat patterns,
GM percussion voices, true ratio polyrhythm (poly toggle), presets, set
lists with a recordable practice log, and keyboard shortcuts.
deploy.sh: copies index.html into the Caddy web root that serves
https://metronome.varasys.io (no restart needed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>