Commit graph

332 commits

Author SHA1 Message Date
Me Here
da7c94e67f Implement per-track playback flow (rep / end / relative goto)
Adds the per-track end-action model designed in docs/track-format.md §3, end to
end across both engines, both firmwares, and the editors.

Grammar (parsed + serialized by engine.js and both app.py):
  rep=<n>     cycles before the end-action fires (default 1)
  end=stop    stop after rep cycles
  end=next    advance one track (sugar for end=+1)
  end=<±N>    relative goto after rep cycles (e.g. end=-2 = D.S.)
  (absent)    loop forever — the metronome default

Firmware runtime (pico-cp + pico-explorer): _on_new_bar now consults a per-track
_end_plan() and fires stop / gapless-advance / relative-goto at the right bar.
A cycle = b<bars>, else one master bar; fire bar = rep * cycle. Explicit end=
governs; with no end, the global Continue toggle stays a default (=end=next, still
needs b<bars>) so existing set-lists and the CONT UI are unchanged. _prepare_next
takes a target index; the seam machinery, _do_advance and live-sync all carry rep/end.

Editors (editor.html + editor-beta.html): state.rep/state.end thread through
applySetup / currentSetup / currentPatch so load -> edit -> save preserves the
flow; authoring is via the program-string field (no graphical control yet).

Tests: the 3 playback-flow vectors now pass on both engines (39 pass / 3 known).
Runtime decision logic (_end_plan / _goto_target) unit-tested for stop, rep,
relative goto clamp/wrap, and legacy-Continue precedence. Codec round-trip
verified idempotent. Both firmwares compile + mpy-cross clean.

Also: untrack stale __pycache__/*.pyc build artifacts and gitignore them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:37:06 -05:00
Me Here
9701f49913 Firmware: parse euclid, GM note-numbers, and unknown-sound fallback
Close three real parser divergences the conformance suite flagged on the device
side (pico-cp + pico-explorer) — cases where the firmware produced a different
groove/sound than the web for the same patch:

- Euclidean (k,n,rot) shorthand (e.g. kick:4(3,8)) — was silently dropped to a
  plain bar; now expands to the same hits as engine.js (added _euclid + parsing).
- GM note-number lane sounds (e.g. 36:4) — now resolve to the voice name (GM_NUM).
- Unknown sound names fall back to beep, matching the web.

vol/cd are NOT carried by the firmware by design: they are web-authoring fields
(the device has a hardware volume knob and no count-in). Documented as an
intentional, permanent host difference rather than a bug; the vol-and-countin
vector stays as expectFail[py] to mark the boundary.

tests/adapters/py_adapter.py: extract the new SOUND_GM/GM_NUM/_euclid nodes.
fixtures: euclid/unknown-sound/gm-note-number now pass on both engines.
docs §6 updated. node tests/run.mjs: 33 pass / 9 known, round-trips stable.
pico-explorer parser spot-checked identical to pico-cp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:15:25 -05:00
Me Here
0dc9daf54f PM_K-1 hardware: consolidated BOM + LAYOUT.md + PCB-layout tutorial
- gen_bom.py + BOM_board.csv: authoritative BOM generated from board.net (70 line items,
  167 placements), grouped with MPNs; refs match the integrated netlist; DNP ICs flagged.
  (Supersedes the early hand-written BOM.csv, which used per-block refs.)
- LAYOUT.md: routing rulebook for board.net -- 4-layer stackup, the grounding/star-point
  strategy, switcher loop isolation, analog separation, USB diff pair, RP2350/crystal/flash,
  thermal, DNP blocks, pre-fab confirm list, DRC checklist.
- pcb_layout_tutorial.md: beginner orientation -- use KiCad; the schematic/netlist=contract
  vs layout=physical-realization paradigm; the import->place->route->pour->DRC->Gerber
  workflow; vocabulary; how our files fit; learning resources; honest expectations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:15:15 -05:00
Me Here
bf74c860e5 Track format: unify default (no-pattern) groove across web + firmware
A lane with no =pattern produced different defaults on web vs device — a real,
shipped divergence the new conformance suite caught (e.g. hatClosed:4/2 in
"Four-on-the-floor" played steady 8ths in the browser but quarter-notes on the
device). Adopt one rule everywhere: every subdivision sounds at normal level,
accents fall ONLY on group starts (the grouping is the accent map).

- pico-cp/app.py, pico-explorer/app.py: off-beat subdivisions sound at normal (1)
  instead of resting (0); group-start accenting was already correct.
- src/engine.js: default beatsOn accents group starts only (was: every beat);
  laneCfgToStr isDefault check updated to match so round-trips stay idempotent.
- docs + fixtures: document the rule; default-pattern vectors now pass on both.

Audible effect (intended): device subdivided hat/ride lanes gain their off-beat
strokes (now match the web); web stops over-accenting every beat. Lanes with an
explicit =pattern are unchanged. Verified green via node tests/run.mjs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:03:34 -05:00
Me Here
47edf4eb2a PM_K-1 hardware: FULL-BOARD integration -- single master netlist (board.py)
circuits/board.py re-implements every block (power tree, RP2350 core, audio chain, RTC,
MIDI-DNP, interconnects, SIG/CLIP-DNP, speaker-DNP) with shared net objects and SKiDL
auto-assigned reference designators -> one coherent board.net for PCB layout.

167 components, unique refs U1-U18 / K1-K3 / J1-J5. ERC 0 errors; netlist 0 errors.
Remaining ERC warnings are all benign unconnected-pin notes on intentionally-spare pins
(relay NO contacts, 4 unused ULN2003 channels, spare GPIOs, 2 unused 74LVC14 inverters,
RTC CLKOUT, TPS65131 BSW) and OC<->GPIO notes (comparator/opto outputs read by the MCU).
MCLK-less: PCM5102A SCK tied to GND (internal PLL).

This is the complete schematic deliverable: board.net + BOM.csv ready for layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 23:56:42 -05:00
Me Here
754ed1c22d Formalize track format: spec + golden-vector conformance suite
Single source of truth for the track ("program"/"patch") grammar, which was
implemented by hand in src/engine.js and pico-cp/app.py with no cross-check and
had quietly drifted.

- docs/track-format.md: formal grammar, container (programs.json) schema with a
  version field, the new per-track playback-flow model (rep/end + relative goto;
  default = loop forever), normalization rules, and a list of known divergences.
- tests/: golden vectors + a runner that loads the REAL engine.js and app.py
  grammar (no copies; app.py via ast extraction) and compares both against the
  spec. Exit non-zero on unexpected mismatch or round-trip break -> usable as CI.

Surfaces real divergences for follow-up: default accent pattern (no =pattern)
differs web vs device and affects shipped presets; euclid not parsed on device;
vol/cd dropped on device; unknown-sound fallback; tempo clamp; empty patch.
The rep/end playback-flow vectors are the acceptance test for building that.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 23:54:20 -05:00
Me Here
25d0c57d79 PM_K-1 hardware: fix indicator.py scoping bug (p15 rebind in threshold())
The committed indicator.py errored (UnboundLocalError: p15) -- an augmented assign on a
global Net inside threshold() made it local. Flipped to rt[1]+=p15. Now ERC 0 / netlist 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 23:49:50 -05:00
Me Here
367951c903 PM_K-1 hardware: SIG/CLIP indicator + monitor speaker amp (both DNP)
indicator.py: peak-detect (Schottky+RC) on STAGE1_OUT (signal-present) and MIX_OUT (clip)
-> LM393 (powered from +15V) vs tunable threshold dividers -> open-collector outputs pulled
to +3V3 = SIG_LED/CLIP_LED (drive face LEDs + read on GPIO19/20).
speaker.py: PAM8302A filterless class-D (pinout verified, Diodes SO-8) fed from MIX_OUT
(single-ended, RIN=68k -> ~+5.7dB), SD pulled high, BTL out -> SPK_P/SPK_N.
Both ERC 0 errors; netlists 0 errors. Threshold/gain values tunable; DNP per form factor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 23:49:18 -05:00
Me Here
7eea02f1d2 PM_K-1 hardware: interconnects (digital ribbon + analog + MIDI headers + USB-C/ESD)
circuits/interconnect.py maps the board nets onto connectors per DESIGN.md s7:
- J3 digital ribbon 2x13 (Pico-pinout): display SPI, touch I2C, joystick ADC, buttons,
  WS2812, panel-switch inputs, SIG/CLIP LED lines, power.
- J4 analog 2x5: balanced out hot/cold, shield, balanced in hot/cold, speaker (kept off
  the fast digital ribbon).
- J5 MIDI 1x8: OUT/IN/THRU loops + power (DNP MIDI only).
- J1 USB-C receptacle (KiCad lib symbol) + USBLC6-2SC6 ESD + 5.1k CC pulldowns; D+/D-
  from the core's 27R series, VBUS to the power tree.
ERC 0 errors; netlist 0 errors. Confirm USB-C connector variant (24-pin sym vs 16-pin
GCT USB4085) + USBLC6 footprint at layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 23:33:21 -05:00
Me Here
8f662598e4 PM_K-1 hardware: MIDI block (DNP) -- opto IN + buffered OUT/THRU
circuits/midi.py, a do-not-populate option (USB-MIDI is the default). H11L1 opto-isolated
IN (breaks the MIDI ground loop), 74LVC14-buffered OUT (TX through two inverters) and THRU
(re-buffered IN). Connector is a face choice; core exposes the loop nets MIDI_IN/OUT/THRU_A/B
plus MIDI_TX/RX to the RP2350 UART. ERC 0 errors; netlist 0 errors.
CONFIRM: H11L1 pinout (standard; datasheet fetch timed out) + 3.3V-MIDI series-R values.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 23:28:59 -05:00
Me Here
8fe9ade210 PM_X-1 0.0.5: clear logo/divider overlap + simpler always-on amp
User reported the y=28 divider crossed the bottom of the V in the VARASYS
logo. The logo is actually 29 px tall (gen_assets.py blob: w=156 h=29), so
positioned at y=8 it ran to y=37 - the divider sat ~halfway through the V.

Fix: logo now at y=4 (top-margin 4 px), divider at y=34 (~1 px clearance
below the logo bottom at y=33). Everything below the divider shifted down
6 px: BPM/time y=44, bar y=56, train y=64, tab y=78, title y=94, GRID_TOP=116,
LOG_TOP=224 with 5 rows (was 6).

Simpler amp control. 0.0.4's per-click _amp(True)/_amp(False) toggling was
both timing-sensitive (the brief 22 ms PWM burst would race with amp settling
time) and polarity-guessed. Replaced with: hold amp_en at the polarity-correct
"enable" value the whole time. One config flag (AMP_EN_ON_VALUE, default True)
controls polarity. If the user still hears no sound after dragging this on,
flip the flag to False and re-flash.

Also still check Settings -> Speaker. If a saved /settings.json from an earlier
build has speaker=auto, Live sync's heartbeat will silence the piezo - cycle
to "Always" with A in the Settings menu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 23:28:09 -05:00
Me Here
5aac3ab172 PM_K-1 hardware: stop tracking generated SKiDL artifacts (.erc/.log/_sklib.py)
These build outputs leaked into git (the .gitignore only covered .net/.pdf/etc). Added
the patterns and untracked the files (kept on disk). Keeps the repo to source only and
stops generated churn from being swept into unrelated commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 22:54:28 -05:00
Me Here
c625a8aaa2 Firmware push fix on both Kit (0.0.24) + Explorer (0.0.4)
Diagnosed from the user's console output - 25 chunks pushed cleanly at
~124ms each, then stalled. Two coupled causes:

1) Bus contention. tick() and Live sync share self.midi with the chunk
   ACKs. While the device was processing a chunk, a Note On / Clock Out /
   Live-sync FULL heartbeat could land on the same MIDI OUT stream and
   the host's parser dropped the interleaved ACK SysEx.

   Fix: self._fw_pushing flag set on 0x21 BEGIN, cleared on 0x23 COMMIT
   or any error. midi_send / Clock Out / _sync_broadcast / _sync_broadcast_full
   all early-out when _fw_pushing is True. Only ACKs go out during a push.

2) SysEx assembler garbage. self._sx = bytearray() per chunk leaks ~70
   bytes / chunk that only GC'd every 50 chunks. 25 chunks of trash plus a
   slow heap walked the wrong way explains the ramp-up to 174 -> 119 -> 124
   ms ACK times. GC every chunk now (~30ms cost on RP2040/RP2350 with
   small heap) so the assembler buffer is always fresh.

Same patch on both pico-cp/ and pico-explorer/ since the bug is identical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 22:51:46 -05:00
Me Here
3805c5ee00 PM_X-1 0.0.4 + editor push diagnostics
Layout fixes (user reported BPM/time were still bumping the header at 0.0.3):
- All Y coords below the header divider shifted down 6px: BPM 30->38,
  time 32->38, bar 44->50, train 52->58, setlist tab 66->72, title 82->88.
- GRID_TOP 104 -> 110.

Restored the Kit-style footer practice log:
- LOG_TOP=218, LOG_ROWH=14, LOG_ROWS=6.
- MAXLANES dropped from 6 visible to 4 visible (rowh capped at 26 so the grid
  doesn't run into the log). Tracks with more lanes still play silently.
- _build_scene now appends g_log (with a divider above it).
- draw_log() draws the current-track log into the footer; load() + _log_play()
  + the seam apply path all call it. The Practice-log menu entry is kept for
  the full scrollable history.

Editor diagnostics for the firmware push (the user got chunk-1 ACK then the
device's MIDI badge went gray, meaning chunks 2+ never reached it):
- editor.html + editor-beta.html _pushFirmware() now logs every MIDI output
  + input it sees along with which ones _isDevicePort() matched, plus per-
  chunk send/ACK timing for the first 3 chunks and any failed chunk.
- This narrows down whether the failure is (a) wrong-port routing
  (filter doesn't match the Pimoroni Explorer's name), (b) ACK never arriving
  back to the host, or (c) chunks sent fine but the device's RX buffer is
  dropping them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 22:40:11 -05:00
Me Here
ea7bb9bfee PM_X-1 0.0.3: compact 240x320 layout + piezo polarity flag + Pimoroni device filter
Three fixes the user reported on 0.0.2:

1. Layout still overlapped. 0.0.2 ported the Kit's pixel positions verbatim,
   but those were designed for 480 px of height; on 320 the same Y values
   stack on top of each other. This pass actually scales everything down:
   - BPM big number was FONT_L (~30 px tall) -> FONT_M (~16 px tall).
   - Time + bar meters were FONT_M -> FONT_S, tightly stacked at y=32/44
     instead of y=50/78.
   - Setlist tab + CONT y=66 (was 118); track title y=82 (was 134).
   - GRID_TOP=104 (was 138). Frees ~34 px more vertical room for the
     pad grid.
   - Modal panels: PX/PW shrunk to use less of the 240-wide canvas, RH
     22 (was 26), inter-line spacing 13-14 (was 14-16). Title strings
     trimmed ("Practice log" instead of "Practice log (this track)").

2. No sound from the piezo. Two likely causes:
   - SPEAKER_AUTO_MUTE was True. Live sync sends a FULL heartbeat over
     USB-MIDI every 5s; the firmware sees those bytes and treats it as
     "host listening" -> mutes the piezo. Default now False on Explorer
     (toggle to Auto in Settings if you ARE using "Device audio" in the
     editor).
   - AMP_EN polarity. Added AMP_EN_ACTIVE_HIGH config flag (default True)
     and a _amp(on) helper. If your specific board's amp is active-low,
     flip to False at the top of CONFIG.

3. Firmware push stalled at chunk 1. Editor's _isDevicePort() only matched
   "pico" / "circuitpython" / "usb_midi"; the Pimoroni Explorer reports
   under a different name, so the editor broadcast to all MIDI outputs
   and the ACK got lost in routing. Filter now also matches "pimoroni",
   "explorer", "rp2350", and "varasys" (future-proofing).

The .mpy build dropped from 29.8 KB to 29.3 KB (smaller font footprint plus
fewer modal hint strings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 22:05:56 -05:00
Me Here
edb736c1d3 PM_K-1 hardware: RP2350 core (MCU + flash + crystal + USB + boot/reset + SWD)
circuits/mcu_core.py using the authoritative KiCad MCU_RaspberryPi:RP2350A symbol.
Minimal design per RP "Hardware design with RP2350" (RP-008280):
- Core SMPS: VREG_VIN<-3V3, VREG_LX->3.3uH->DVDD, VREG_FB senses DVDD; VREG_AVDD via
  33ohm+4.7uF RC filter; ADC_AVDD filtered; 100nF per power pin.
- 12MHz crystal, MCLK-LESS (RP2350 makes I2S BCK/LRCK/DIN; PCM5102A uses its internal
  PLL) -- no audio oscillator, no MCLK net. Cheaper/simpler/robust; inaudible difference
  for a metronome (decided with the user).
- W25Q128JVS QSPI flash (Fig-8 pinout); BOOTSEL = QSPI_SS via 1k + button; RUN 10k
  pull-up + reset button; SWD header; USB D+/D- via 27ohm series.
- Full GPIO map assigned (DESIGN.md s7.1 + audio control: SPI/I2C/ADC/buttons/LED +
  I2S + relay-enable/mute/gnd-lift + DAC_XSMT).
ERC 0 errors; netlist 0 errors. CONFIRM at layout: crystal load caps, QFN-60 footprint,
and the USB-C connector + USBLC6-2 ESD + CC resistors (USB sub-block; D+/D- exit on 27R).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 21:56:12 -05:00
Me Here
51c81b45e0 PM_X-1 0.0.2: portrait flip + Kit-style layout (no more overlapping bits)
The 320x240 landscape layout was too cramped vertically; multiple elements
sat on the same Y rows, especially the BPM big-number area and the bar/time
meters, plus the run dot collided with the MIDI/USB icons.

Switching to portrait at 240x320 (display.rotation = 270; user holds the
device with A/B/C buttons along the top) gives the same vertical real
estate the PM_K-1 Kit's portrait UI uses, just narrower. Layout now mirrors
the Kit:
- y 0..28: header (logo + version + MIDI/USB icons + run dot)
- y 44:    BPM big (right)
- y 50:    elapsed time (left, FONT_M)
- y 78:    bar counter (left, FONT_M)
- y 100:   ramp / gap-trainer indicators
- y 118:   setlist tab + CONT (single row)
- y 134:   track title (FONT_M)
- y 138+:  pad grid (up to 6 lanes, taller rowh ceiling now 30px)

Plus:
- DISPLAY_ROTATION constant near the top of CONFIG so the user can flip it
  to 90 / 180 if their orientation differs.
- Pad grid uses px0=48 (was 60) since the lane label column has less
  horizontal room at 240 width; max 6-char labels.
- Removed the inline modal hints (e.g. "X/Z move, A select, C close") that
  would have collided with the modal titles at 240 width. The Help screen
  documents the modal nav pattern, which is consistent across modals.
- HELP_PAGES page 1 leads with "Hold portrait with A/B/C on top."
- README documents the rotation flag.

Bumps Explorer to 0.0.2. .mpy can be pushed via the editor's Update firmware
flow (device id reply = X;0.0.1 -> editor fetches the right .mpy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 21:37:12 -05:00
Me Here
05ce1d5ce4 Landing page: explorer.html widget mockup
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>
2026-05-30 20:55:20 -05:00
Me Here
cd619cfeb2 PM_K-1 hardware: power tree (USB 5V -> +/-18V switcher -> clean +/-15V LDOs + 3V3)
circuits/power_tree.py captures the full supply, topology + values verified from the
TPS65131 datasheet (SLVS493E: pinout p3, Typical Application Fig 8-1 p11, BOM Table 8-2
p12, FB equations p13 Vref=1.213V) and the TPS7A49/TPS7A30 LDO datasheets:
- TPS65131 dual boost/inverter: L1/L2=4.7uH, D1/D2=MBRM120, FB dividers set ~+/-18.2V
  (R1=1.4M/R2=100k, R3=1.5M/R4=100k), comp C7/C6 on CP/CN, VREF 220nF, no Q1 (USB),
  PSP/PSN=GND forced-PWM for low audio-band noise.
- TPS7A4901/TPS7A3001 post-regulate to clean +/-15V (shared 8-pin pinout, EN tied to IN).
- AP2112K-3.3 -> digital 3V3.
ERC 0 errors; netlist 0 errors.

CONFIRM before fab: exact LDO Vfb (used ~1.194V/-1.18V for the dividers) and the
AP2112K SOT-23-5 pinout. Switcher validated vs TI reference design; no SPICE (behavioral
models don't validate a switcher) -- layout per the TI EVM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:53:44 -05:00
Me Here
e80ff9d564 Landing page: add PM_X-1 Explorer pane
Adds the Explorer to index.html's VERSIONS list between the Kit and Teacher
panes. file: "/info-explorer.html" since there's no explorer.html widget yet;
the existing replace("/", "/info-") logic is wrapped in an idempotent infoOf()
helper so "Specs & info" doesn't double-prefix it.

info-explorer.html also gets the standard ?embed=1 handler (matches every
other info-*.html), so the landing-page iframe preview strips chrome + auto-
sizes when the pane is selected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 20:49:55 -05:00
Me Here
3192f3debc PM_X-1 0.0.1: Pimoroni Explorer sibling firmware + Kit 0.0.23 device-id reply
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>
2026-05-30 20:43:38 -05:00
Me Here
617bb5a8b2 PM_K-1 hardware: integrate audio chain into one netlist (dedup shared parts)
circuits/audio_chain.py wires stages 1/1b/2/3/4 with shared nets and deduplicated
parts: ONE OPA1612 (U4) does both the Stage-2 filter (A) and Stage-3 summer (B);
ONE ULN2003 (U6) drives all three relays (K1 select, K2 mute, K3 ground-lift).
54 components, ERC 0 errors, netlist 0 errors. Per-stage files remain as the
documented, individually-simulated building blocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:41:39 -05:00
Me Here
5a75dbbbdc PM_K-1 hardware: Stage 4 -- balanced output driver (completes the audio chain)
THAT1646 balanced driver (pinout verified, THAT doc 600078 rev07) closes the chain:
MIX_OUT -> 25-turn level-cal trim -> THAT1646 (+6dB, sense tied local) -> 47ohm
build-out -> fail-safe mute relay K2 -> balanced AOUT_HOT/COLD on the interconnect;
ground-lift relay K3 (de-energized=bonded, soft-lift 100R||10nF) -> CHASSIS.

- Phase: Stage 3 inversion corrected via HOT<-OUT-, COLD<-OUT+.
- Level cal trim ahead of the driver (its +6dB gain is fixed).
- K2 fail-safe: de-energized shorts both legs to GND after the build-out (driver
  current-limited). K3 ground-lift in series with a face panel switch.
- stage4_driver.cir: differential flat +4.76dB (1k=20k), legs antiphase (0 vs pi rad),
  build-out+cable rolloff above audio. ERC 0 errors; netlist 0 errors.

AUDIO CHAIN COMPLETE: stages 1, 1b, 2, 3, 4 all captured + simulated + ERC-clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:27:04 -05:00
Me Here
6b6a58fa56 PM_K-1 hardware: Stage 3 -- summing node (selected input + click)
Inverting summing amp (OPA1612 section) mixes STAGE1_OUT (line/instrument) and
CLICK_OUT (filtered DAC) at unity into MIX_OUT. Each source enters its own 10k into
the op-amp virtual ground, so they sum with no interaction.

stage3_sum.cir confirms: each input alone = 0 dB, both together = +6.02 dB, and each
input's gain is unchanged by the other (virtual-ground isolation). ERC/netlist 0 errors.

Note: inverting summer flips phase -> corrected at the Stage 4 balanced driver via
hot/cold assignment. At integration, this summer can use the parked 2nd half of the
Stage 2 filter OPA1612 (U4) instead of a separate package.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:15:17 -05:00
Me Here
2f44be6f63 PM_K-1 hardware: resolve TQ2SA relay pinout; Stage 2 DAC + reconstruction filter
Relay residuals resolved from the Panasonic TQ-SMD connection diagram + contact-
resistance terminal pairs: coil=1/10, pole1 COM=3/NC=4/NO=2, pole2 COM=8/NC=7/NO=9,
pins 5/6 unused. (NC/NO sense also firmware-correctable via the GPIO drive.)
Stage 1b encoding already matched; docstring updated to "resolved".

Stage 2 (click source): PCM5102A DAC + 2nd-order Sallen-Key reconstruction filter.
- PCM5102A pinout verified (TI SLAS859C, TSSOP-20). 2.1Vrms GND-centered out (no
  DC-block), charge-pump flying cap + VNEG, DEMP/FLT/FMT tied for I2S/normal/no-deemph,
  SCK<-low-jitter MCLK, BCK/DIN/LRCK<-RP2350, XSMT pulled-up soft-mute.
- OPA1612 Sallen-Key LPF on OUTL. stage2_recon.cir confirms flat to 20kHz, -3dB at
  74.8 kHz -- cleans delta-sigma HF residue without touching audio.
- ERC 0 errors; netlist 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:06:26 -05:00
Me Here
e6f425ee6f PM_K-1 hardware: Stage 1b -- Hi-Z instrument DI buffer + line/inst select relay
OPA1641 non-inverting DI buffer (1Mohm in, +12dB) + TQ2SA DPDT relay that both
routes the jack tip (line receiver vs DI buffer) and selects the output. Default
de-energized = LINE (common case, fail-safe). Driven by the shared ULN2003 via
net K1_DRV from GPIO SEL_LINST.

Pinouts verified from datasheets before capture (per the no-guessing rule):
- OPA1641 (TI SBOS484D): 1=NC 2=-IN 3=+IN 4=V- 5=NC 6=OUT 7=V+ 8=NC.
- ULN2003A: GND=8, COM=9, in 1-7 / out 16-10.
- TQ2SA (Panasonic TQ-SMD): pole1 COM=3 throws 2/4, pole2 COM=8 throws 7/9
  (from contact-resistance terminal pairs). NC/NO orientation + coil pins (1/10)
  follow the standard single-side-stable diagram -- flagged in-file for a final
  connection-diagram cross-check (not over-claimed).

ngspice stage1b_di.cir confirms +12.04dB gain, flat across the audio band.
ERC 0 errors; netlist 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:56:55 -05:00
Me Here
075c1786af PM_K-1 hardware: verify THAT1240 against datasheet; fix pinout + part numbers
Verified the receiver from THAT doc 600035 rev05 instead of guessing:
- THAT1240 = 0 dB (unity) -- correct as specced; 1243=-3dB, 1246=-6dB would be wrong.
- SO-8 pinout 1=Ref 2=In- 3=In+ 4=Vee 5=Sense 6=Vout 7=Vcc 8=NC. My initial
  SKiDL pins were mostly wrong; corrected. Netlist now matches the datasheet.
- KiCad Device:D is pin1=K/pin2=A; my clamp diodes were reversed -- fixed so they
  actually clamp (D high->cathode to +15, D low->anode to -15).
- BOM part numbers had a bogus "W16" suffix; corrected to S08-U (SO-8). Noted
  INA134/SSM2141 as pin-compatible 2nd sources for long-term availability.

ERC 0 errors, netlist 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:48:41 -05:00
Me Here
3f7f4b94d9 PM_K-1 hardware: Stage 1 input receiver as SKiDL (code-defined schematic)
Capture method = SKiDL per decision. circuits/stage1_input.py defines the
balanced line receiver + per-leg protection (DC-block film cap, series R, bias R,
clamp diodes to the rails) and emits a KiCad netlist. ERC: 0 errors (2 expected
warnings -- AIN_HOT/COLD reach only one pin until the interconnect block exists).

Container: env vars point SKiDL/KiCad at the symbol/footprint libs.

VERIFY-before-layout flagged in-file: exact THAT124x gain suffix, its SO-8 pin
numbers, clamp-diode orientation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:43:07 -05:00
Me Here
d51c9f1011 PM_K-1 hardware: Stage 1 audio (input receiver) sims + container libs
Audio chain, stage 1 (balanced input receiver + protection) validated in ngspice:
- stage1_cmrr.cir: CMRR vs resistor matching -> 1% = 46dB, 0.1% = 66dB, perfect
  = amp-limited; justifies the laser-trimmed THAT1240 over discrete resistors.
- stage1_phantom.cir: +48V phantom step -> clamped to ~16V blip, steady-state
  ~0.12V; the DC-block cap + clamp + series R make a miswire a non-event.

Container: add kicad-symbols + kicad-footprints (for symbol placement) and skidl.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:27:25 -05:00
Me Here
bcfa5dd7f0 PM_K-1 hardware: reproducible EDA container (KiCad 9 + ngspice)
Pinned toolchain under hardware/eda/ so the design can be checked/simulated
identically in the future (system KiCad is 7.0, which has no CLI ERC):
- Containerfile: Ubuntu 24.04 + KiCad 9 (PPA) + ngspice + python3.
- run.sh: build-if-needed + run with the repo mounted; lands in hardware/kicad.
- sim/input_loading.cir: ngspice deck proving the line(25k) vs instrument(1M)
  input-loading decision — Hi-Z preserves a +16dB pickup resonance the 25k load
  flattens to -3dB.

Verified: KiCad 9.0.9, ngspice-42, ERC runs clean (0 violations) on
pm_k1_core.kicad_sch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:17:54 -05:00
Me Here
87caa933ea PM_K-1 hardware: core-board design-of-record + KiCad scaffold
Heirloom pro-audio modular brain/face design captured under hardware/:
- DESIGN.md: full spec (RP2350, ±15V studio rails via TPS65131+TPS7A LDOs,
  PCM5102A click, THAT1240/1646 balanced click-injector with switchable
  protected line/instrument input, fail-safe mute relay, series ground-lift,
  USB-MIDI default + DNP hardware MIDI, sig/clip detect, ESD/EMI, chassis),
  plus the two-interconnect pinouts (Pico-compatible digital ribbon + separate
  analog/MIDI).
- BOM.csv: manufacturer part numbers + rough costs.
- kicad/: valid KiCad 7 project + documented schematic canvas. PCB
  layout/routing remains the interactive step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 11:42:45 -05:00
Me Here
8726f42d05 PM_K-1 0.0.22: cleanup -- delete the dead _step_dur fallback
By 0.0.20 every site that needed step duration had switched to L['durs'][step]
(the precomputed tuple from _rebuild_dur). _step_dur was kept as a "legacy
fallback" with a comment claiming it was still used by the tap-tempo path --
which was wrong; grep showed zero callers. Ten lines of unreachable code.

Now that 0.0.21 is sounding clean on the Pico 2, time to trim the sediment. No
behavior change; .mpy shrinks slightly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 11:30:49 -05:00
Me Here
47fa6d7ce7 PM_K-1 0.0.21: live-sync protocol (HELLO/FULL/DELTA/BYE) - device side
Implements the device half of the docs/livesync-protocol.md contract that the
other (editor) Claude wrote in src/livesync.js. SysEx opcodes 0x40..0x43 on the
existing 0x7D manufacturer id; ASCII payload reusing the share-grammar tokens.

State + helpers (App.__init__):
- self._sync_origin: short random id ("d" + 8-hex from os.urandom(4)) - lets
  peers drop their own echoes (composite USB MIDI may loop a frame back).
- self._sync_seq: monotonic counter.
- self._sync_armed: True after any HELLO/FULL/DELTA seen (until BYE).
- self._sync_applying: echo guard - True while applying a remote change so
  the broadcast hooks early-out and we never ping-pong.
- self._sync_heartbeat_next: deadline for the periodic 5s FULL.

Transport:
- _sync_send(op, text): builds the SysEx frame, ASCII-safe (replaces high bits
  with '?'), writes via self.midi.write.
- _sync_broadcast(evt): one DELTA, guarded.
- _sync_broadcast_full(): one FULL with origin;seq;running;sl;item;patch; also
  resets the heartbeat timer.

Receive (extends _handle_sysex):
- 0x40 HELLO -> _sync_broadcast_full (reply with our state).
- 0x41 FULL  -> _sync_apply_full(running, patch): diff against current
  _prog_str(); only rebuild if different (avoids flicker on a heartbeat that
  matches local state); reconcile transport either way.
- 0x42 DELTA -> _sync_apply_delta(evt) for the seven verbs:
  - play / stop -> toggle if state differs.
  - bpm=<n>      -> set_bpm.
  - sel=<sl>/<item> -> switch + goto; -1/-1 sentinel ignored.
  - beat=<l>/<s>/<lvl> -> set lanes[l]['levels'][s], recolor pad, mark dirty.
  - lane=<l>/<field>/<value> -> sound/groups/sub/swing/gain/poly/enabled; if
    structural, _regen_levels + _rebuild_dur (or _rebuild_dur_all when lane 0
    changed, so poly lanes follow) + build_grid.
- 0x43 BYE -> _sync_armed = False.

Local mutators broadcast:
- toggle()         -> "play" | "stop"
- set_bpm()        -> "bpm=<n>"
- goto() / switch_setlist() -> "sel=<sl>/<item>"
- _cycle_beat()    -> "beat=<l>/<s>/<lvl>"
- _lane_dirty()    -> FULL (coalesced; matches editor's syncPatchSoon idiom)

Heartbeat in the run loop emits a FULL every 5s while armed.

Plus docs/livesync-protocol.md (the spec the other Claude wrote in parallel)
gets committed too. The patch field round-trips through parse_program <->
_prog_str on the device side and setupToPatch <-> patchToSetup on the editor
side, all already share-grammar compatible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 09:59:19 -05:00
Me Here
eae9057baf PM_E-1 beta: live-sync editor (editor-beta.html) mirroring a connected PM_K-1
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>
2026-05-30 09:09:37 -05:00
Me Here
09144c9892 PM_K-1 0.0.20: per-pad chunking + step grids + smarter refresh
Three changes; one diagnosis: the 0.0.19 freeze + stutter were both symptoms of the
predictive refresh skip being too aggressive at fast subdivisions (scanning ALL lanes
+ a 35ms window meant the next beat is *always* within the skip window at fine
subdivisions, so the only refresh was the 0.5s force-after fallback, audible as both
a flickery freeze AND a periodic 30ms stutter).

PER-PAD CHUNKING (your ask):
- _grid_rebuild_step now builds ONE rectangle per loop iter (was one whole lane row).
- The first chunk on a lane draws the instrument label + caches the lane's geometry;
  subsequent chunks each emit one pad rect/circle.
- Each chunk is ~5-10ms instead of ~30-50ms, so tick() interleaves at sub-beat
  resolution and the grid fills in pad-by-pad over ~600ms after the seam without
  ever blocking long enough to miss a beat at any reasonable BPM.

STEP GRIDS:
- L['durs'] is a tuple of int ns per step, precomputed once per lane.
- tick()'s inner-loop scheduler is now L['next'] += L['durs'][L['step']] -- a tuple
  index, no method call, no dict lookups, no division.
- _rebuild_dur(L) and _rebuild_dur_all() wired into every bpm-change path:
  set_bpm / load / _do_advance / _prepare_next / _slave_tick (Clock In) / _lane_dirty
  (the lane editor; rebuilds master-affects-poly cascade if lane 0 changes structurally),
  + the per-master-step ramp interpolator. The synchronous _step_dur stays as a fallback
  for paths the inner loop doesn't hit (e.g. start-of-segment calculations elsewhere).

SMARTER REFRESH:
- Master-lane-only window (other lanes are subdivisions; a few ms of jitter there is
  imperceptible musically).
- 10ms window (down from 35) so refresh runs in the gap between master beats even at
  fast subdivisions.
- 20Hz throttle (down from 30) so the per-refresh blocking budget is bigger.
- 200ms force fallback (down from 500) so visuals + titles stay live -- this is the
  fix for "the track titles don't get reliably updated."

DEFER set_bpm DRAWS:
- set_bpm no longer calls draw_bpm/draw_meters; the 4Hz UI tick already redraws them.
- Joystick spam used to allocate a fresh text bitmap per nudge -> fragmentation
  pressure + potential GC stutter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 08:58:13 -05:00
Me Here
672f892ea1 PM_K-1 0.0.19: chunked build_grid, predictive refresh, zero-alloc hot path
The stutter and the MemoryError were two faces of the same problem: the main loop
allocates and blocks too much. This pass attacks both.

CHUNKED build_grid -- the biggest single audio fix.
- build_grid() split into _grid_rebuild_start (tear-down + gridlines + state) and
  _grid_rebuild_step (build ONE lane row + its pads).
- run() loop processes one chunk per iter, so tick() runs between every lane. A 16-pad
  lane chunk is ~30-50ms but a tick now interleaves; the old ~300ms blackout is gone.
- The synchronous build_grid() still works (used by load() etc) -- it loops the steps
  to completion in one call, same as before for non-seam paths.
- load() / switch_setlist cancel any in-progress chunked rebuild so a navigation
  doesn't leave the grid half-built.

PREDICTIVE display.refresh -- the second-biggest audio fix.
- display.refresh() blocks SPI for ~30ms streaming pixels. A beat scheduled in that
  window was firing late.
- The refresh decision now scans lanes[i]['next'] (cheap; n<=8) and skips refresh if
  any beat is due within 35ms.
- Force-refresh after 0.5s so visuals stay live at fast subdivision rates.

ZERO-ALLOC HOT PATH -- attacks fragmentation / MemoryError frequency.
- self._note_buf: reused bytearray for every MIDI Note On (was a fresh bytes() / click).
- self._clock_byte / _start_byte / _stop_byte: singleton bytes for transport messages.
- fired_best / fired_prio ints replace fired = [] (was ~1000 list allocs/sec at speed,
  plus a max(key=lambda) call per tick -- gone).
- self._beat_ns cached in set_bpm + the ramp interpolator; _step_dur reads it instead
  of doing 60_000_000_000 / self.bpm every step. Integer // instead of float /.
- The MIDI Clock Out also caches its tick period via _beat_ns // 24.

CRASH SAFETY -- the run loop's while True body is now wrapped in try/except. A
MemoryError logs + gc.collect() + continues. Any other Exception logs + continues. The
metronome now stutters through transient errors instead of dying with a traceback.

This is roughly the ceiling of what straight Python can do here without a custom-C
tick module. The remaining stutter sources are the displayio scene-graph itself + the
RP2040 bytecode interpreter overhead, and those move us to Rust (or a C extension) if
audible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 08:38:18 -05:00
Me Here
93c1fb62e9 PM_K-1 0.0.18: split post-seam redraw -- fast bits now, heavy build_grid deferred
0.0.17 fixed the off-by-one so A plays its full final bar, but B's first beats were
still being eaten -- by build_grid + draw_log blocking the main loop for ~hundreds of
ms right at the seam (a full grid teardown + ~64-128 vectorio rectangle allocations +
draw_log text bitmaps + an SPI refresh). tick() doesn't run during that, so B's first
master steps fire only when the catch-up while-loop runs at the next tick, collapsing
into one click.

Split the post-seam refresh:

- Fast pass: draw_meters / draw_bpm / draw_status / draw_train. Cheap (each just swaps
  one TileGrid). Runs immediately so the new title + bpm + status are correct on screen.
- Heavy pass: build_grid + draw_log. Deferred by 0.6s after the seam, gated by
  self._heavy_redraw_at. B's intro plays clean; the grid catches up half a beat or so
  into B with a single brief jitter (rather than B's first 1-2 beats being lost).

Plus gc.collect() at the start of build_grid AND draw_log. Both allocate enough small
objects that a fragmented heap will MemoryError partway through; collecting first gives
them a clean run.

load() / switch_setlist() clear _heavy_redraw_at so a user-initiated navigation doesn't
trigger a second deferred rebuild on top of the immediate one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 08:25:19 -05:00
Me Here
da71604c0d PM_K-1 0.0.17: fix gapless-seam off-by-one + GC hygiene around modals / parse
The continuous-playback gap between tracks was a one-master-step early trigger of
_on_new_bar(N): nb = _m_steps // steps maps to the LAST step of bar N-1, not the
downbeat of bar N. So _seam_t (= lanes[0]['next'] at that moment) was one master step
short of the proper boundary -- A lost its last step, and B's "bar 0 step 0" landed at
that early slot. With kick:2 (2 steps/bar) that's half a bar each side; with kick:4 a
quarter; with kick:1 a whole bar.

Fix: nb = (self._m_steps - 1) // L['steps'] -- fires at the downbeat of the new bar.
Same adjustment applied to the on-screen "bar X of N" counter (mbars) so it advances
on the downbeat, not on the last beat of the previous bar.

Memory hygiene: gc.collect() at the entry of every modal (_show_menu / _show_settings /
_show_help / _show_about / _show_saverevert / _show_laneedit) so each modal allocates
its bitmaps against a defragmented heap. _prepare_next gc.collect()s before
parse_program and catches MemoryError -> the segment loops instead of crashing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 08:13:49 -05:00
Me Here
c9f2288bdd 0.0.16 fix: strip the two stray ☰ characters from app.py comments
Previous build aborted at the ASCII guard; the .mpy on the server didn't update. This
adds the patch as a new commit (no history rewrite).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:53:00 -05:00
Me Here
5153e35a52 PM_K-1 0.0.16: BPM floor 30 -> 5; hamburger ☰ menu + Settings / Help / About
BPM lower bound dropped 30 -> 5 (engine + ramp + slave-clock-in interval filter; pure
clamp choice, no engine reason for the higher floor). Very slow practice is now
possible. Sub-musical (<5) sits below the slave-decay timeout so wasn't worth pursuing.

Hamburger menu (☰ at the far right of the header, 3 thin recolorable rects; tap zone
covers the corner). Reuses the existing overlay / _ovbtns pattern - no new framework.

- Main menu (_show_menu): Save edits / Revert edits (dimmed when not _dirty) / Continue
  on-off (live) / Settings > / Help > / About / Done.
- Settings sub-modal: LED brightness 5..50% in 5% steps, Speaker mode (Auto / Always /
  Off, combining MUTE_SPEAKER + SPEAKER_AUTO_MUTE), MIDI Out on/off, MIDI Channel 1..16,
  Clock Out on/off, Clock In on/off. < value > adjusters reusing the lane-editor row
  pattern. Each tap calls _save_settings(); the modal redraws live.
- Help sub-modal: 3 paginated pages (Transport & Nav / Editing / Status & Hardware)
  with < Done > nav.
- About sub-modal: version, free RAM, uptime, CircuitPython version, site URL. Defensive
  about gc.mem_free and sys.implementation (CircuitPython-specific).
- Persistence to /settings.json: read in __init__ (_load_settings overrides defaults),
  written on every change (settings are small + infrequent). NAKs silently if read-only.

Verified in the harness: all 4 modals render, settings.json round-trips (channel 10->11,
clock_out off->on persisted), tap zones populate as expected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:50:55 -05:00
Me Here
12a31b87a8 PM_K-1 0.0.15: MIDI Clock In (slave) - follow an external 24 PPQN master
New config: MIDI_CLOCK_IN (default OFF) + MIDI_CLOCK_IN_TRANSPORT (default ON).

_feed_midi now intercepts 0xF8 (clock tick) -> smoothed bpm tracker (exponential, a=1/8;
rejects out-of-range intervals so noise can't drag bpm wild), 0xFA / 0xFB (Start /
Continue) -> start playback without echoing 0xFA on output (would feedback), 0xFC ->
stop. _slaved flag is True while ticks are arriving and decays after 1s of silence.

While slaved: per-master-step continuous ramp is OFF (the master's tempo wins) and
Clock Out emission is suppressed (no feedback loop if the user enables both).

Verified in the harness: target 60/120/180 BPM all track exactly after 60 ticks
(2.5 quarter notes of smoothing); 0xFA -> running, 0xFC -> stopped; slave flag
correctly decays.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:38:54 -05:00
Me Here
f637a65abd Push reliability: 10s ACK timeout + per-chunk flush + periodic GC
Push to 0.0.14 stalls at ~150 chunks with the MIDI badge going gray (=device silent for
>1s). It's an occasional slow flash flush on the device side — the file buffer fills,
flush takes several seconds, the editor's 4s timeout cuts it off too early.

- Editor: bump per-chunk ACK timeout 4s -> 10s; progress log every 25 chunks now with
  elapsed seconds so you can see it advancing through a slow chunk.
- Device 0.0.14: flush per chunk (small, predictable per-chunk cost instead of
  infrequent multi-second bursts) + gc.collect() every 50 chunks to keep the heap
  fresh during a long push.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:28:12 -05:00
Me Here
99174d1bf8 PM_K-1 0.0.14 (fix): late-toggled Continue arms the seam too
If Continue was toggled on mid-segment, _prepare_next never ran during bar (bars-1)
and the seam stayed un-armed. Fall back to prep on the spot at the boundary itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:12:00 -05:00
Me Here
fd8446658d PM_K-1 0.0.14: gapless seam + continuous ramp + MIDI Clock Out (master); speaker rename
Speaker rename (production device has a full audio circuit, not a buzzer):
- MUTE_BUZZER -> MUTE_SPEAKER, self.buz -> self.spk, P_BUZ -> P_SPK
- New SPEAKER_AUTO_MUTE flag (default True): mute the speaker when a MIDI host is
  detected (the old hardcoded behavior; now a setting).

Gapless seam between tracks (Continue):
- _prepare_next() pre-parses the next playlist item during the LAST bar so the swap is
  allocation-free. _do_advance() swaps lanes/bpm/bars/ramp/trainer in with
  lanes[0]['next'] = seam_t (the wall-clock time of the boundary step we just hit), no
  _reset_clock - the next tick fires step 0 of the new track exactly at the boundary.
  tick() breaks out at the seam so the old voice's boundary beat is NOT fired (it'd be
  the new track's step 0 a few ms later). Visuals (build_grid + draws) are deferred
  one display-refresh cycle behind the audio via _need_redraw, so the audio doesn't
  wait for them.

Continuous ramp:
- Replaced the bar-boundary set_bpm step with per-master-step linear interpolation:
  bpm = _ramp_base + amt * ((m_steps/mlen) % bars) / ramp.every (clamped 30..300).
  The integer-clamped bpm glides smoothly across the segment. draw_bpm() is now lazy
  (skips the bitmap alloc if the displayed integer hasn't changed), and the periodic
  meter tick in run() also redraws BPM so the big number follows the ramp.

MIDI Clock Out (master):
- New flags: MIDI_CHANNEL (default 10 = GM drum), MIDI_CLOCK_OUT (default OFF),
  MIDI_CLOCK_OUT_TRANSPORT (default ON). midi_send() now uses the configured channel.
  In tick(), when running + MIDI_CLOCK_OUT, stream 0xF8 at 24 PPQN with the interval
  computed live from self.bpm (so it follows the continuous ramp). toggle() sends
  0xFA on Start and 0xFC on Stop when transport is enabled.

Verified in harness: seam keeps lanes[0]['next'] = seam_t (no _reset_clock); ramp 80
glides via +0.25/step (visible as 80->81 in 4 master steps at rmp80/4/4); Clock Out
math sound (60/120/180 BPM -> 41.67/20.83/13.89 ms tick interval).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 07:11:19 -05:00
Me Here
b1bb792df6 info-kit.html: drop the MicroPython firmware section; CircuitPython is THE firmware
Promote the CircuitPython "appliance" section to the single, default-open firmware
section, and update the hero/about/meta to match (drop "copy one file" / PROGRAMS list /
MicroPython flash dance phrasing -> precompiled bundle + on-device editing + Save to
device).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 06:21:17 -05:00
Me Here
30f02305e5 Editor: pause the Device-audio heartbeat during a firmware push
Push advanced to ~150/506 chunks then stalled intermittently. With Device audio on, the
heartbeat (0xFE every 250ms + a clock SysEx every ~3s) shares the MIDI link with the
firmware chunks and intermittently costs a chunk its ACK. Pause the heartbeat for the
duration of _pushFirmware and resume after.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 15:14:58 -05:00
Me Here
937e7c332d Editor: fix firmware push stall - small chunks + send only to the Pico
Trace showed the push reaching "pushing" then stalling. Two causes: (1) _send went to
ALL MIDI outputs incl. "Midi Through Port-0" (a loopback that just echoes back); (2) the
512-char chunks overran the Pico's USB-MIDI receive buffer, so the device never saw the
end of a chunk's SysEx and never ACKed.

- _send now targets only the device port (name match pico/circuitpython/usb_midi; falls
  back to all outputs if none match) - no loopback echo.
- Firmware chunks 512 -> 64 base64 chars (a SysEx that fits the RX buffer); log progress
  every 50 chunks + a "committing" line so the console shows it advancing.

Editor-only; hard-reload to pick it up (no firmware change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 15:09:04 -05:00
Me Here
c5cc329185 Editor: add console breadcrumbs to updateFirmware to diagnose a silent no-op
User reports the updater does nothing with no visible error. Every code path shows a
dialog, so it's bailing before/at one of them (likely browser dialog-suppression making
confirm() return false, a stale/cached editor, or an uncaught throw). Log each step so a
console trace pinpoints where it stops.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 15:00:56 -05:00
Me Here
73d18ab1f3 PM_K-1: keep firmware versioning in the 0.0.x series (0.1.0 -> 0.0.13)
Per request, stay on 0.0.x; the lane-editor build is relabeled 0.0.13 (no code change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:55:36 -05:00
Me Here
dbc9fa7fdc PM_K-1 0.1.0: on-device lane editor - edit/add/remove lanes
Tap the instrument name -> a modal to change Sound (cycle the GM voices), Beats (1-12),
Subdivision (1-8), Swing, and Mute, plus + Lane / Remove (1..MAXLANES). Beats/sub changes
regenerate the lane's default accents; sound/swing/mute keep the pattern. Reuses the
existing dirty + Save/Revert + .mpy machinery (edits to a built-in save a copy to
"My edits"). The modal redraws live as you adjust; tap Done or outside to close.

Verified in harness: editor opens (13 hit-zones), sound cycles, beats/sub regen steps,
swing/mute toggle, add/remove lanes, the edited track serializes + round-trips, Done
closes; modal renders cleanly. app.mpy builds (C/v6).

This completes the Phase-2 editing set (beats + lanes + Continue + built-in/user split).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:44:04 -05:00