pm-grid: USB-MIDI audio + make Rust the shipping Grid firmware
Audio (the Scroll Pack has no speaker, so MIDI is the only path):
- usb-device 0.3 + usbd-midi 0.5 on the rp2040-hal UsbBus; enumerates as a
class-compliant MIDI device 'PM_G-1 Grid'.
- tick() emits a GM note-on per lane hit on channel 10 (note from the ported
SOUND_GM map, velocity by level) via send_bytes([0x09,0x99,note,vel]) — raw
4-byte packets, so arbitrary GM drum notes work without the named Note enum.
- USB polled every loop iteration AND during the boot splash (so the host can
enumerate during the ~2.5s animation).
Debug: defmt/defmt-rtt + panic-probe + flip-link; runner probe-rs run --chip
RP2040 (Pi Debug Probe). build.sh emits pm-grid.uf2 + pm-grid.elf; deploy serves
both; key info! log points + 1Hz heartbeat.
Web: drop CircuitPython from the PM_G-1 product. info-grid.html features the
Rust .uf2 download + accurate controls/views (X/Y swap, Ticker); build.sh +
deploy.sh no longer bundle/serve pm_g1_circuitpy.zip or pico-scroll-app.{py,mpy}.
pico-scroll/ stays as the reference port; editor FW_PATHS.G left for graceful
degradation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
512890baa2
commit
56bff7e599
8 changed files with 185 additions and 81 deletions
20
build.sh
20
build.sh
|
|
@ -23,9 +23,9 @@ echo "precompiled dist/app.mpy ($(stat -c%s dist/app.mpy) bytes <- $(stat -c%s p
|
||||||
# bundles each ship their own precompiled binary; the served URLs follow the same one-target-per-file rule.
|
# bundles each ship their own precompiled binary; the served URLs follow the same one-target-per-file rule.
|
||||||
( cd pico-explorer && "$MPYC" app.py -o "$ROOT/dist/explorer-app.mpy" )
|
( cd pico-explorer && "$MPYC" app.py -o "$ROOT/dist/explorer-app.mpy" )
|
||||||
echo "precompiled dist/explorer-app.mpy ($(stat -c%s dist/explorer-app.mpy) bytes <- $(stat -c%s pico-explorer/app.py) source)"
|
echo "precompiled dist/explorer-app.mpy ($(stat -c%s dist/explorer-app.mpy) bytes <- $(stat -c%s pico-explorer/app.py) source)"
|
||||||
# PM_G-1 "Grid" firmware (Pimoroni Pico Scroll Pack on a plain RP2040 Pico). Same mpy-cross (CircuitPython 10.2.1).
|
# PM_G-1 "Grid" ships the native Rust firmware now (rust/pm-grid → pm-grid.uf2, built by
|
||||||
( cd pico-scroll && "$MPYC" app.py -o "$ROOT/dist/scroll-app.mpy" )
|
# rust/pm-grid/build.sh and served by deploy.sh). The old CircuitPython build (pico-scroll/app.py)
|
||||||
echo "precompiled dist/scroll-app.mpy ($(stat -c%s dist/scroll-app.mpy) bytes <- $(stat -c%s pico-scroll/app.py) source)"
|
# is no longer bundled or served; the source stays in-repo as the reference port.
|
||||||
|
|
||||||
python3 - <<'PY'
|
python3 - <<'PY'
|
||||||
import os, pathlib, re
|
import os, pathlib, re
|
||||||
|
|
@ -69,12 +69,6 @@ assert not _xbad, "pico-explorer/app.py has non-ASCII at %r -- keep it ASCII (ve
|
||||||
pathlib.Path("dist/pico-explorer-app.py").write_text(_xsrc) # editor reads APP_VERSION from here
|
pathlib.Path("dist/pico-explorer-app.py").write_text(_xsrc) # editor reads APP_VERSION from here
|
||||||
pathlib.Path("dist/pico-explorer-app.mpy").write_bytes(pathlib.Path("dist/explorer-app.mpy").read_bytes())
|
pathlib.Path("dist/pico-explorer-app.mpy").write_bytes(pathlib.Path("dist/explorer-app.mpy").read_bytes())
|
||||||
print("copied pico-explorer-app.py + pico-explorer-app.mpy")
|
print("copied pico-explorer-app.py + pico-explorer-app.mpy")
|
||||||
_gsrc = pathlib.Path("pico-scroll/app.py").read_text() # PM_G-1 Grid firmware (Pico Scroll Pack)
|
|
||||||
_gbad = [(i, c) for i, c in enumerate(_gsrc) if ord(c) > 0x7F]
|
|
||||||
assert not _gbad, "pico-scroll/app.py has non-ASCII at %r -- keep it ASCII (version regex + clean source)" % (_gbad[:5],)
|
|
||||||
pathlib.Path("dist/pico-scroll-app.py").write_text(_gsrc) # editor reads APP_VERSION from here
|
|
||||||
pathlib.Path("dist/pico-scroll-app.mpy").write_bytes(pathlib.Path("dist/scroll-app.mpy").read_bytes())
|
|
||||||
print("copied pico-scroll-app.py + pico-scroll-app.mpy")
|
|
||||||
import zipfile # PM_K-1 CircuitPython drive bundle (download → unzip onto CIRCUITPY)
|
import zipfile # PM_K-1 CircuitPython drive bundle (download → unzip onto CIRCUITPY)
|
||||||
with zipfile.ZipFile("dist/pm_k1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z:
|
with zipfile.ZipFile("dist/pm_k1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z:
|
||||||
for f in ("code.py", "boot.py", "programs.json", "font_s.bin", "font_m.bin", "font_l.bin",
|
for f in ("code.py", "boot.py", "programs.json", "font_s.bin", "font_m.bin", "font_l.bin",
|
||||||
|
|
@ -92,11 +86,5 @@ with zipfile.ZipFile("dist/pm_x1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z
|
||||||
z.write("dist/explorer-app.mpy", "app.mpy")
|
z.write("dist/explorer-app.mpy", "app.mpy")
|
||||||
z.write("dist/editor.html", "editor.html")
|
z.write("dist/editor.html", "editor.html")
|
||||||
print("zipped pm_x1_circuitpy.zip")
|
print("zipped pm_x1_circuitpy.zip")
|
||||||
# PM_G-1 Grid drive bundle (Pico Scroll Pack on a plain Pico). No font/icon blobs - the LED firmware draws directly.
|
# PM_G-1 Grid is the native Rust firmware now — no CircuitPython bundle (see rust/pm-grid).
|
||||||
with zipfile.ZipFile("dist/pm_g1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z:
|
|
||||||
for f in ("code.py", "boot.py", "programs.json", "README.md"):
|
|
||||||
z.write("pico-scroll/" + f, f)
|
|
||||||
z.write("dist/scroll-app.mpy", "app.mpy")
|
|
||||||
z.write("dist/editor.html", "editor.html")
|
|
||||||
print("zipped pm_g1_circuitpy.zip")
|
|
||||||
PY
|
PY
|
||||||
|
|
|
||||||
14
deploy.sh
14
deploy.sh
|
|
@ -63,7 +63,13 @@ fi
|
||||||
# BOOTSEL-drag pm-grid.uf2 onto the RPI-RP2 drive to flash the PM_G-1 Grid LED metronome.
|
# BOOTSEL-drag pm-grid.uf2 onto the RPI-RP2 drive to flash the PM_G-1 Grid LED metronome.
|
||||||
if [[ -f "$SRC_DIR/rust/pm-grid/pm-grid.uf2" ]]; then
|
if [[ -f "$SRC_DIR/rust/pm-grid/pm-grid.uf2" ]]; then
|
||||||
cp "$SRC_DIR/rust/pm-grid/pm-grid.uf2" "$DEST_DIR/pm-grid.uf2"
|
cp "$SRC_DIR/rust/pm-grid/pm-grid.uf2" "$DEST_DIR/pm-grid.uf2"
|
||||||
echo " pm-grid.uf2 ($(stat -c '%s' "$DEST_DIR/pm-grid.uf2") bytes) # Rust RP2040 firmware (LED-first alpha)"
|
echo " pm-grid.uf2 ($(stat -c '%s' "$DEST_DIR/pm-grid.uf2") bytes) # Rust RP2040 firmware (the PM_G-1 Grid)"
|
||||||
|
fi
|
||||||
|
# ELF with defmt info — `probe-rs run --chip RP2040 pm-grid.elf` flashes over the Pi Debug Probe and
|
||||||
|
# streams defmt RTT logs/panics (decoded from the ELF). Served for local probe-based debugging.
|
||||||
|
if [[ -f "$SRC_DIR/rust/pm-grid/pm-grid.elf" ]]; then
|
||||||
|
cp "$SRC_DIR/rust/pm-grid/pm-grid.elf" "$DEST_DIR/pm-grid.elf"
|
||||||
|
echo " pm-grid.elf ($(stat -c '%s' "$DEST_DIR/pm-grid.elf") bytes) # probe-rs flash + defmt RTT logging"
|
||||||
fi
|
fi
|
||||||
cp "$DIST_DIR/pm_k1_circuitpy.zip" "$DEST_DIR/pm_k1_circuitpy.zip"; echo " pm_k1_circuitpy.zip ($(stat -c '%s' "$DEST_DIR/pm_k1_circuitpy.zip") bytes)" # PM_K-1 CircuitPython bundle
|
cp "$DIST_DIR/pm_k1_circuitpy.zip" "$DEST_DIR/pm_k1_circuitpy.zip"; echo " pm_k1_circuitpy.zip ($(stat -c '%s' "$DEST_DIR/pm_k1_circuitpy.zip") bytes)" # PM_K-1 CircuitPython bundle
|
||||||
cp "$DIST_DIR/pico-cp-app.py" "$DEST_DIR/pico-cp-app.py"; echo " pico-cp-app.py ($(stat -c '%s' "$DEST_DIR/pico-cp-app.py") bytes)" # served for version reading + reference
|
cp "$DIST_DIR/pico-cp-app.py" "$DEST_DIR/pico-cp-app.py"; echo " pico-cp-app.py ($(stat -c '%s' "$DEST_DIR/pico-cp-app.py") bytes)" # served for version reading + reference
|
||||||
|
|
@ -71,9 +77,9 @@ cp "$DIST_DIR/pico-cp-app.mpy" "$DEST_DIR/pico-cp-app.mpy"; echo " pico-cp-app.
|
||||||
cp "$DIST_DIR/pm_x1_circuitpy.zip" "$DEST_DIR/pm_x1_circuitpy.zip"; echo " pm_x1_circuitpy.zip ($(stat -c '%s' "$DEST_DIR/pm_x1_circuitpy.zip") bytes)" # PM_X-1 Explorer CircuitPython bundle
|
cp "$DIST_DIR/pm_x1_circuitpy.zip" "$DEST_DIR/pm_x1_circuitpy.zip"; echo " pm_x1_circuitpy.zip ($(stat -c '%s' "$DEST_DIR/pm_x1_circuitpy.zip") bytes)" # PM_X-1 Explorer CircuitPython bundle
|
||||||
cp "$DIST_DIR/pico-explorer-app.py" "$DEST_DIR/pico-explorer-app.py"; echo " pico-explorer-app.py ($(stat -c '%s' "$DEST_DIR/pico-explorer-app.py") bytes)" # served for version reading
|
cp "$DIST_DIR/pico-explorer-app.py" "$DEST_DIR/pico-explorer-app.py"; echo " pico-explorer-app.py ($(stat -c '%s' "$DEST_DIR/pico-explorer-app.py") bytes)" # served for version reading
|
||||||
cp "$DIST_DIR/pico-explorer-app.mpy" "$DEST_DIR/pico-explorer-app.mpy"; echo " pico-explorer-app.mpy ($(stat -c '%s' "$DEST_DIR/pico-explorer-app.mpy") bytes)" # PM_X-1 firmware (the editor pushes this when device id = X)
|
cp "$DIST_DIR/pico-explorer-app.mpy" "$DEST_DIR/pico-explorer-app.mpy"; echo " pico-explorer-app.mpy ($(stat -c '%s' "$DEST_DIR/pico-explorer-app.mpy") bytes)" # PM_X-1 firmware (the editor pushes this when device id = X)
|
||||||
cp "$DIST_DIR/pm_g1_circuitpy.zip" "$DEST_DIR/pm_g1_circuitpy.zip"; echo " pm_g1_circuitpy.zip ($(stat -c '%s' "$DEST_DIR/pm_g1_circuitpy.zip") bytes)" # PM_G-1 Grid CircuitPython bundle
|
# PM_G-1 Grid is the native Rust firmware now (pm-grid.uf2 above) — the CircuitPython grid
|
||||||
cp "$DIST_DIR/pico-scroll-app.py" "$DEST_DIR/pico-scroll-app.py"; echo " pico-scroll-app.py ($(stat -c '%s' "$DEST_DIR/pico-scroll-app.py") bytes)" # served for version reading
|
# artifacts (pm_g1_circuitpy.zip / pico-scroll-app.{py,mpy}) are no longer built or served.
|
||||||
cp "$DIST_DIR/pico-scroll-app.mpy" "$DEST_DIR/pico-scroll-app.mpy"; echo " pico-scroll-app.mpy ($(stat -c '%s' "$DEST_DIR/pico-scroll-app.mpy") bytes)" # PM_G-1 firmware (the editor pushes this when device id = G)
|
rm -f "$DEST_DIR/pm_g1_circuitpy.zip" "$DEST_DIR/pico-scroll-app.py" "$DEST_DIR/pico-scroll-app.mpy" # remove stale Python grid downloads
|
||||||
rm -f "$DEST_DIR/player-asbuilt.html" # renamed to teacher.html
|
rm -f "$DEST_DIR/player-asbuilt.html" # renamed to teacher.html
|
||||||
rm -f "$DEST_DIR/concepts.html" # Concepts is now the landing (/)
|
rm -f "$DEST_DIR/concepts.html" # Concepts is now the landing (/)
|
||||||
# info-*.html are first-class pages again: each form factor has a lean widget page
|
# info-*.html are first-class pages again: each form factor has a lean widget page
|
||||||
|
|
|
||||||
|
|
@ -147,12 +147,16 @@ On `embassy` / `rp-hal`:
|
||||||
- WS2812 → `ws2812-pio`. USB-MIDI → `usbd-midi` / `embassy-usb`.
|
- WS2812 → `ws2812-pio`. USB-MIDI → `usbd-midi` / `embassy-usb`.
|
||||||
- GT911 touch (Kit) over I²C.
|
- GT911 touch (Kit) over I²C.
|
||||||
|
|
||||||
#### `pm-grid` — Scroll Pack firmware 🟢 BUILT (LED-first milestone), pending on-device check
|
#### `pm-grid` — Scroll Pack firmware 🟢 BUILT (LED + USB-MIDI audio), pending on-device check
|
||||||
The Rust sibling of `pico-scroll/app.py` (`rust/pm-grid/`). Target is a **plain RP2040** (Cortex-M0+,
|
The Rust sibling of `pico-scroll/app.py` (`rust/pm-grid/`), and **the firmware the PM_G-1 ships now** —
|
||||||
|
the CircuitPython build is dropped from the product (info-grid.html / build.sh / deploy.sh no longer
|
||||||
|
bundle or serve it; source stays as the reference port). Target is a **plain RP2040** (Cortex-M0+,
|
||||||
`thumbv6m-none-eabi`) — NOT the Pico 2 — so it has its own HAL (`rp2040-hal` 0.10 + `rp2040-boot2`),
|
`thumbv6m-none-eabi`) — NOT the Pico 2 — so it has its own HAL (`rp2040-hal` 0.10 + `rp2040-boot2`),
|
||||||
`.cargo/config.toml`, `memory.x` (BOOT2 + flash + 264 KB RAM) and `build.sh`+`uf2.py` (RP2040 family
|
`.cargo/config.toml`, `memory.x` (BOOT2 + flash + 264 KB RAM) and `build.sh`+`uf2.py` (RP2040 family
|
||||||
id `0xe48bff56`). `thumbv6m-none-eabi` added to `rust/Containerfile`. Compiles clean → **48 KB
|
id `0xe48bff56`). `thumbv6m-none-eabi` added to `rust/Containerfile`. Compiles clean → **`pm-grid.uf2`**
|
||||||
`pm-grid.uf2`** (BOOTSEL drag-flash; no probe needed). Kept out of the host workspace like `pm-kit`.
|
(BOOTSEL drag-flash) + **`pm-grid.elf`** (probe-rs + defmt). Both served by deploy.sh; the info page
|
||||||
|
links the `.uf2`. Kept out of the host workspace like `pm-kit`. Debug build uses `defmt`/`defmt-rtt` +
|
||||||
|
`panic-probe` + `flip-link`, runner `probe-rs run --chip RP2040` (the user's Pi Debug Probe).
|
||||||
|
|
||||||
What's implemented (faithful port of `pico-scroll`): the **IS31FL3731 driver** (vendored bulk
|
What's implemented (faithful port of `pico-scroll`): the **IS31FL3731 driver** (vendored bulk
|
||||||
144-byte framebuffer, one I²C block write per frame — the right architecture, mirrors the
|
144-byte framebuffer, one I²C block write per frame — the right architecture, mirrors the
|
||||||
|
|
@ -170,10 +174,18 @@ list; X/Y=tempo ∓ with auto-repeat), the **built-in set lists**, and three LED
|
||||||
`_render_grid` / `_render_pendulum`.
|
`_render_grid` / `_render_pendulum`.
|
||||||
Boot splash scrolls "PM-G1 GRID" (liveness + pixel-map check).
|
Boot splash scrolls "PM-G1 GRID" (liveness + pixel-map check).
|
||||||
|
|
||||||
**Deferred to the next milestone** (matches pm-kit's order — none built yet): **USB-MIDI** note-out
|
**Audio — USB-MIDI ✅ DONE** (the Scroll Pack has NO speaker, so this is the real audio path):
|
||||||
(the Scroll Pack has NO speaker, so this is the real audio path) + MIDI clock, **live-sync SysEx**
|
`usb-device` 0.3 + `usbd-midi` 0.5 (the `rp2040-hal` `UsbBus`). Enumerates as a class-compliant MIDI
|
||||||
(0x40-0x43 + version query), firmware push (0x10/0x21-0x23), on-device practice log, settings.json,
|
device ("PM_G-1 Grid"); `tick` emits a **GM note-on per lane hit on channel 10** (note from the ported
|
||||||
and playback-flow auto-advance (`rep`/`end`/continue). An optional piezo on a free GPIO is also TODO.
|
`SOUND_GM` map, velocity by level 120/90/45) via `UsbMidiClass::send_bytes([0x09,0x99,note,vel])` —
|
||||||
|
raw 4-byte packets, sidestepping the named-`Note` enum so arbitrary GM drum notes work. USB is polled
|
||||||
|
every loop iteration **and during the boot splash** (1.5 ms cadence) so the host can enumerate. Play
|
||||||
|
through the editor's **Device audio**.
|
||||||
|
|
||||||
|
**Still deferred**: MIDI clock in/out, **live-sync SysEx** (0x40-0x43 + version query), firmware push
|
||||||
|
(0x10/0x21-0x23), on-device practice log, settings.json, playback-flow auto-advance (`rep`/`end`/
|
||||||
|
continue), optional piezo. Note: without the SysEx version query, the editor's firmware-push won't
|
||||||
|
target the Grid (intended — it's UF2-flashed now).
|
||||||
|
|
||||||
### Stage 4 — native A/B + secure boot
|
### Stage 4 — native A/B + secure boot
|
||||||
Replace the `.mpy`-level A/B hack (`code.py` loads `app.mpy`, rolls back to `app.bak`) with the
|
Replace the `.mpy`-level A/B hack (`code.py` loads `app.mpy`, rolls back to `app.bak`) with the
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>VARASYS PM_G-1 Grid - wiring, parts & firmware (Pimoroni Pico Scroll Pack / RP2040)</title>
|
<title>VARASYS PM_G-1 Grid - wiring, parts & firmware (Pimoroni Pico Scroll Pack / RP2040)</title>
|
||||||
<meta name="description" content="PM_G-1 Grid - the Pimoroni Pico Scroll Pack (PIM545, 17x7 LED matrix + 4 buttons) on a Raspberry Pi Pico as a polymeter metronome with live-sync to the web editor. Pinout, parts list, and the precompiled CircuitPython firmware bundle." />
|
<meta name="description" content="PM_G-1 Grid - the Pimoroni Pico Scroll Pack (PIM545, 17x7 LED matrix + 4 buttons) on a Raspberry Pi Pico as a polymeter metronome. Pinout, parts list, and the native Rust firmware (flash the .uf2)." />
|
||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
|
||||||
<script>
|
<script>
|
||||||
/* ?embed=1 -> strip site chrome (base.css [data-embed]) + auto-size to the host iframe */
|
/* ?embed=1 -> strip site chrome (base.css [data-embed]) + auto-size to the host iframe */
|
||||||
|
|
@ -59,12 +59,11 @@
|
||||||
<b>IS31FL3731</b> over I²C (individually brightness-controlled), and <b>4 buttons</b> (A / B / X / Y).
|
<b>IS31FL3731</b> over I²C (individually brightness-controlled), and <b>4 buttons</b> (A / B / X / Y).
|
||||||
No soldering, no touchscreen, no joystick, no RGB - and <b>no onboard speaker</b>. About 65 × 25 × 10 mm
|
No soldering, no touchscreen, no joystick, no RGB - and <b>no onboard speaker</b>. About 65 × 25 × 10 mm
|
||||||
on top of the Pico. Power is over VSYS (USB or battery).</p>
|
on top of the Pico. Power is over VSYS (USB or battery).</p>
|
||||||
<p>It runs the same <b>polymeter engine</b> and <b>program strings</b> as the web editor. The 7-row × 17-column
|
<p>It runs the <b>native Rust firmware</b> (<code>rust/pm-grid</code>), built on the same <b>polymeter engine</b>
|
||||||
|
(the shared <code>track-format</code> crate) and <b>program strings</b> as the web editor. The 7-row × 17-column
|
||||||
matrix maps directly onto the editor's <b>lane × step</b> pad grid: each lane is a row, each step a column, and
|
matrix maps directly onto the editor's <b>lane × step</b> pad grid: each lane is a row, each step a column, and
|
||||||
LED brightness encodes accent / normal / ghost. Beat editing is done in the browser; <b>Live sync</b> mirrors edits
|
LED brightness encodes accent / normal / ghost. <b>Audio is over USB-MIDI</b> - turn on the editor's
|
||||||
to the device in real time (HELLO/FULL/DELTA over USB-MIDI) and the device mirrors play/stop/tempo/track back.
|
<b>🎹 Device audio</b> to hear the clicks through your computer (the Scroll Pack has no speaker of its own).</p>
|
||||||
<b>Audio is over USB-MIDI</b> - turn on the editor's <b>🎹 Device audio</b> to hear the clicks through your
|
|
||||||
computer (or solder a piezo to a free GPIO and set <code>P_BUZZER</code> in <code>app.py</code>).</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<details class="spec" open>
|
<details class="spec" open>
|
||||||
|
|
@ -78,9 +77,8 @@
|
||||||
<tr><td class="part">SDA / SCL</td><td>GP4 / GP5</td></tr>
|
<tr><td class="part">SDA / SCL</td><td>GP4 / GP5</td></tr>
|
||||||
<tr class="grp"><td colspan="2">Buttons (digital, pull-up)</td></tr>
|
<tr class="grp"><td colspan="2">Buttons (digital, pull-up)</td></tr>
|
||||||
<tr><td class="part">A (play/stop) / B (track)</td><td>GP12 / GP13</td></tr>
|
<tr><td class="part">A (play/stop) / B (track)</td><td>GP12 / GP13</td></tr>
|
||||||
<tr><td class="part">X (-bpm) / Y (+bpm)</td><td>GP14 / GP15</td></tr>
|
<tr><td class="part">X (+bpm) / Y (-bpm)</td><td>GP14 / GP15</td></tr>
|
||||||
<tr class="grp"><td colspan="2">Audio (optional - not on the Scroll Pack)</td></tr>
|
<tr class="grp"><td colspan="2">Audio - over USB-MIDI (the Scroll Pack has no speaker)</td></tr>
|
||||||
<tr><td class="part">Piezo PWM <span class="spec">- only if you wire one; set <code>P_BUZZER</code></span></td><td>any free GPIO</td></tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -92,21 +90,21 @@
|
||||||
<table class="bom">
|
<table class="bom">
|
||||||
<thead><tr><th>Button</th><th>Tap</th><th>Hold</th></tr></thead>
|
<thead><tr><th>Button</th><th>Tap</th><th>Hold</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="part">A</td><td>play / stop</td><td>cycle view (Grid → Pendulum → BPM)</td></tr>
|
<tr><td class="part">A</td><td>play / stop</td><td>cycle view (Ticker → Grid → Pendulum)</td></tr>
|
||||||
<tr><td class="part">B</td><td>next track</td><td>next set list</td></tr>
|
<tr><td class="part">B</td><td>next track</td><td>next set list</td></tr>
|
||||||
<tr><td class="part">X</td><td>tempo -1</td><td>repeat (-5 after ~1.5 s)</td></tr>
|
<tr><td class="part">X</td><td>tempo +1</td><td>repeat (+5 after ~1.5 s)</td></tr>
|
||||||
<tr><td class="part">Y</td><td>tempo +1</td><td>repeat (+5 after ~1.5 s)</td></tr>
|
<tr><td class="part">Y</td><td>tempo -1</td><td>repeat (-5 after ~1.5 s)</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<table class="bom" style="margin-top:10px">
|
<table class="bom" style="margin-top:10px">
|
||||||
<thead><tr><th>View</th><th>What the 17×7 matrix shows</th></tr></thead>
|
<thead><tr><th>View</th><th>What the 17×7 matrix shows</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr><td class="part">Ticker</td><td>the track name infinite-scrolls along the left; a beat strip on the top row marks the beats with a bright playhead; the BPM is pinned right, rotated 90° CCW (a hundreds dot-bar + the last two digits). The whole matrix flashes on the downbeat.</td></tr>
|
||||||
<tr><td class="part">Grid</td><td>lanes as rows, steps as columns; brightness = accent / normal / ghost; a bright playhead column tracks the beat (bars > 17 steps scale to fit - no steps dropped)</td></tr>
|
<tr><td class="part">Grid</td><td>lanes as rows, steps as columns; brightness = accent / normal / ghost; a bright playhead column tracks the beat (bars > 17 steps scale to fit - no steps dropped)</td></tr>
|
||||||
<tr><td class="part">Pendulum</td><td>a column bounces across the bar like a metronome arm, full-height flash on each beat</td></tr>
|
<tr><td class="part">Pendulum</td><td>a column bounces across the bar like a metronome arm, full-height flash on each beat</td></tr>
|
||||||
<tr><td class="part">BPM</td><td>the current tempo as three 3×5 digits</td></tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="sub" style="margin-top:8px">Tap tempo lives in the web editor. The mapping is deliberately simple (this is a UI prototype) and easy to re-bind at the top of <code>app.py</code>.</p>
|
<p class="sub" style="margin-top:8px">The button mapping is deliberately simple (this is a UI prototype) and easy to re-bind in <code>rust/pm-grid/src/main.rs</code>.</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
@ -124,40 +122,36 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="sub" style="margin-top:10px">Reference: <a href="https://shop.pimoroni.com/products/pico-scroll-pack" target="_blank" rel="noopener">Pico Scroll Pack product page</a>
|
<p class="sub" style="margin-top:10px">Reference: <a href="https://shop.pimoroni.com/products/pico-scroll-pack" target="_blank" rel="noopener">Pico Scroll Pack product page</a>
|
||||||
· <a href="https://github.com/pimoroni/pimoroni-pico/tree/main/libraries/pico_scroll" target="_blank" rel="noopener">vendor code (pico_scroll)</a>
|
· <a href="https://github.com/pimoroni/pimoroni-pico/tree/main/libraries/pico_scroll" target="_blank" rel="noopener">vendor code (pico_scroll)</a>.</p>
|
||||||
· <a href="https://circuitpython.org/board/raspberry_pi_pico/" target="_blank" rel="noopener">CircuitPython for Raspberry Pi Pico</a>.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="spec" open>
|
<details class="spec" open>
|
||||||
<summary>Firmware - self-contained appliance (USB drive · web-driven editing via Live sync · MIDI audio · practice log)</summary>
|
<summary>Firmware - native Rust (flash the <code>.uf2</code>)</summary>
|
||||||
<div class="spec-body">
|
<div class="spec-body">
|
||||||
<p class="sub">The firmware turns the Pico + Scroll Pack into a self-contained appliance: it mounts as a
|
<p class="sub">The Grid runs <b>native Rust firmware</b> (<code>rust/pm-grid</code>), sharing the same
|
||||||
<b>USB drive</b> carrying the (precompiled) firmware, your tracks and an offline copy of this editor;
|
<code>track-format</code> engine crate as the web editor and the other devices. It's a button-driven LED
|
||||||
drives the <b>17×7 matrix</b> with <b>web-driven editing</b> via <b>Live sync</b>; <b>logs your practice</b> to
|
metronome: <b>three views</b> (Ticker, Grid, Pendulum), the <b>built-in set lists</b>, tempo <b>ramp</b> +
|
||||||
<code>history.json</code>; takes new set lists <b>pushed from the editor over USB-MIDI</b>; and plays
|
<b>gap-trainer</b>, and a whole-matrix flash on the downbeat. <b>Audio plays through your computer over
|
||||||
out your <b>computer's speakers over USB-MIDI</b>. By default the firmware owns the drive (read-only to
|
USB-MIDI</b> - the Scroll Pack has no speaker. Flashing is a one-step <code>.uf2</code> drag (no CircuitPython,
|
||||||
the computer - so it can log and can't be accidentally erased); hold <b>button A</b> at power-on for
|
no drive bundle).</p>
|
||||||
editor mode (drive writable).</p>
|
|
||||||
<p>
|
<p>
|
||||||
<a class="dl" href="/pm_g1_circuitpy.zip" download>Download CircuitPython bundle ↓</a>
|
<a class="dl" href="/pm-grid.uf2" download>Download firmware (pm-grid.uf2) ↓</a>
|
||||||
<a class="dl alt" href="https://codeberg.org/VARASYS/metronome/src/branch/main/pico-scroll" target="_blank" rel="noopener">Source + README ↗</a>
|
<a class="dl alt" href="https://codeberg.org/VARASYS/metronome/src/branch/main/rust/pm-grid" target="_blank" rel="noopener">Source ↗</a>
|
||||||
</p>
|
</p>
|
||||||
<ol class="steps">
|
<ol class="steps">
|
||||||
<li>Flash <b>CircuitPython for Raspberry Pi Pico</b>
|
<li><b>Flash it:</b> hold <b>BOOTSEL</b> on the Pico, plug in USB (it appears as the <code>RPI-RP2</code> drive),
|
||||||
(<a href="https://circuitpython.org/board/raspberry_pi_pico/" target="_blank" rel="noopener">download</a>)
|
and drag <b>pm-grid.uf2</b> onto it. It flashes and reboots - "PM-G1 GRID" scrolls once, then the Ticker view.</li>
|
||||||
via BOOTSEL, unzip the bundle onto <code>CIRCUITPY</code>, and power-cycle. It boots into appliance mode.</li>
|
<li><b>Play through your computer:</b> plug the Pico into your computer, open the
|
||||||
<li><b>Edit on the web:</b> open the <a href="/editor-beta.html">editor (beta)</a> in Chrome / Edge / Firefox,
|
<a href="/editor.html">editor</a> in Chrome / Edge / Firefox, click <b>🎹 Device audio</b>, and press
|
||||||
click <b>🔗 Live sync</b>, and the matrix mirrors your edits live (beats, tempo, track changes).</li>
|
<b>A</b> on the device - each click sounds through your speakers over USB-MIDI (GM channel 10).</li>
|
||||||
<li><b>Save a set list to the device</b> for offline use: set-list <b>···</b> menu →
|
<li><b>Controls:</b> <b>A</b> tap = play/stop, hold = cycle view; <b>B</b> tap = next track, hold = next set list;
|
||||||
<b>📟 Save to device</b>. It's pushed over USB-MIDI; the device persists it to
|
<b>X / Y</b> = tempo up / down (auto-repeats while held).</li>
|
||||||
<code>/programs.json</code>.</li>
|
|
||||||
<li><b>Play through your computer:</b> click <b>🎹 Device audio</b>, then press <b>A</b> on the device -
|
|
||||||
the full groove sounds through your speakers over USB-MIDI, in sync (the Scroll Pack has no speaker of its own).</li>
|
|
||||||
<li><b>Firmware updates:</b> ··· menu → <b>⬆ Update firmware</b> - the editor reads
|
|
||||||
the device id (G = Grid), fetches the matching <code>pico-scroll-app.mpy</code>, and pushes it
|
|
||||||
over USB-MIDI. The device A/B-updates with automatic rollback if a build won't boot.</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
|
<p class="sub" style="margin-top:8px">Built from <code>rust/pm-grid</code> via its <code>build.sh</code> (RP2040 /
|
||||||
|
<code>thumbv6m</code>). For development, a Raspberry Pi Debug Probe flashes it and streams logs over
|
||||||
|
<code>probe-rs</code> + <code>defmt</code> (the <code>pm-grid.elf</code> is served alongside for log decoding).
|
||||||
|
Live-sync editing, on-device practice log and editor firmware-push are on the roadmap (not in the Rust build yet).</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,15 @@
|
||||||
target = "thumbv6m-none-eabi" # RP2040 = Cortex-M0+ (the plain Pico on the Scroll Pack)
|
target = "thumbv6m-none-eabi" # RP2040 = Cortex-M0+ (the plain Pico on the Scroll Pack)
|
||||||
|
|
||||||
[target.thumbv6m-none-eabi]
|
[target.thumbv6m-none-eabi]
|
||||||
|
# flip-link: stack below statics, so an overflow faults cleanly instead of corrupting .bss/.data.
|
||||||
|
linker = "flip-link"
|
||||||
|
# `cargo run` flashes over the Raspberry Pi Debug Probe and streams defmt logs/panics via RTT.
|
||||||
|
runner = "probe-rs run --chip RP2040"
|
||||||
rustflags = [
|
rustflags = [
|
||||||
"-C", "link-arg=--nmagic",
|
"-C", "link-arg=--nmagic",
|
||||||
"-C", "link-arg=-Tlink.x", # cortex-m-rt's linker script (INCLUDEs our memory.x)
|
"-C", "link-arg=-Tlink.x",
|
||||||
|
"-C", "link-arg=-Tdefmt.x", # defmt's linker section for log-string interning
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[env]
|
||||||
|
DEFMT_LOG = "debug"
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,18 @@ edition = "2021"
|
||||||
description = "PM_G-1 'Grid' firmware (RP2040 / Pimoroni Pico Scroll Pack PIM545). 17x7 IS31FL3731 LED metronome — Rust sibling of pico-scroll/app.py."
|
description = "PM_G-1 'Grid' firmware (RP2040 / Pimoroni Pico Scroll Pack PIM545). 17x7 IS31FL3731 LED metronome — Rust sibling of pico-scroll/app.py."
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rp2040-hal = { version = "0.10", features = ["rt", "critical-section-impl"] }
|
rp2040-hal = { version = "0.10", features = ["rt", "critical-section-impl", "defmt"] }
|
||||||
rp2040-boot2 = "0.3"
|
rp2040-boot2 = "0.3"
|
||||||
cortex-m = "0.7"
|
cortex-m = "0.7"
|
||||||
cortex-m-rt = "0.7"
|
cortex-m-rt = "0.7"
|
||||||
panic-halt = "0.2" # plain Pico is flashed via UF2/BOOTSEL — no probe, so just halt on panic
|
defmt = "0.3"
|
||||||
|
defmt-rtt = "0.4" # logs over RTT, read by `probe-rs run` (the Pi Debug Probe)
|
||||||
|
panic-probe = { version = "0.3", features = ["print-defmt"] }
|
||||||
embedded-hal = "1"
|
embedded-hal = "1"
|
||||||
track-format = { path = "../track-format" }
|
track-format = { path = "../track-format" }
|
||||||
embedded-alloc = "0.6" # track-format parses into Vec/String → needs a global allocator
|
embedded-alloc = "0.6" # track-format parses into Vec/String → needs a global allocator
|
||||||
|
usb-device = "0.3" # USB device stack (rp2040-hal provides the UsbBus)
|
||||||
|
usbd-midi = "0.5" # USB-MIDI class — the Scroll Pack's only audio path (no speaker)
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ IMG="pm-rust:2"
|
||||||
cargo build --release
|
cargo build --release
|
||||||
OBJCOPY="$(rustc --print sysroot)/lib/rustlib/$(rustc -vV | sed -n "s/host: //p")/bin/llvm-objcopy"
|
OBJCOPY="$(rustc --print sysroot)/lib/rustlib/$(rustc -vV | sed -n "s/host: //p")/bin/llvm-objcopy"
|
||||||
"$OBJCOPY" -O binary target/thumbv6m-none-eabi/release/pm-grid pm-grid.bin
|
"$OBJCOPY" -O binary target/thumbv6m-none-eabi/release/pm-grid pm-grid.bin
|
||||||
|
cp target/thumbv6m-none-eabi/release/pm-grid pm-grid.elf # ELF for probe-rs flash + defmt decode
|
||||||
'
|
'
|
||||||
python3 "$DIR/uf2.py" "$DIR/pm-grid.bin" "$DIR/pm-grid.uf2"
|
python3 "$DIR/uf2.py" "$DIR/pm-grid.bin" "$DIR/pm-grid.uf2"
|
||||||
echo "→ $DIR/pm-grid.uf2 (hold BOOTSEL on the Pico, drag this onto the RPI-RP2 drive)"
|
echo "→ $DIR/pm-grid.uf2 (hold BOOTSEL on the Pico, drag this onto the RPI-RP2 drive)"
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,17 @@ use alloc::vec::Vec;
|
||||||
use embedded_alloc::LlffHeap as Heap;
|
use embedded_alloc::LlffHeap as Heap;
|
||||||
|
|
||||||
use cortex_m::delay::Delay;
|
use cortex_m::delay::Delay;
|
||||||
|
use defmt::info;
|
||||||
|
use defmt_rtt as _; // global defmt logger over RTT (read by `probe-rs run`)
|
||||||
use embedded_hal::digital::InputPin;
|
use embedded_hal::digital::InputPin;
|
||||||
use embedded_hal::i2c::I2c;
|
use embedded_hal::i2c::I2c;
|
||||||
use panic_halt as _;
|
use panic_probe as _; // prints the panic over defmt, then halts
|
||||||
use rp2040_hal as hal;
|
use rp2040_hal as hal;
|
||||||
use rp2040_hal::fugit::RateExtU32;
|
use rp2040_hal::fugit::RateExtU32;
|
||||||
use rp2040_hal::Clock;
|
use rp2040_hal::Clock;
|
||||||
|
use usb_device::prelude::*;
|
||||||
|
use usb_device::bus::UsbBusAllocator;
|
||||||
|
use usbd_midi::UsbMidiClass;
|
||||||
|
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static HEAP: Heap = Heap::empty();
|
static HEAP: Heap = Heap::empty();
|
||||||
|
|
@ -400,8 +405,9 @@ impl App {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Advance the per-lane step clocks up to `now_ns`. Sets `beatflash` for the loudest hit.
|
/// Advance the per-lane step clocks up to `now_ns`. Sets `beatflash` for the loudest hit and
|
||||||
fn tick(&mut self, now_ns: i64) {
|
/// calls `emit(note, velocity)` for every lane hit (the caller sends it over USB-MIDI).
|
||||||
|
fn tick<F: FnMut(u8, u8)>(&mut self, now_ns: i64, mut emit: F) {
|
||||||
if !self.playing {
|
if !self.playing {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -432,9 +438,13 @@ impl App {
|
||||||
} else {
|
} else {
|
||||||
self.track.lanes[li].levels[s]
|
self.track.lanes[li].levels[s]
|
||||||
};
|
};
|
||||||
if lvl > 0 && !self.muted && prio(lvl) > prio(fired_best) {
|
if lvl > 0 && !self.muted {
|
||||||
|
let note = gm_note(self.track.lanes[li].sound.as_str());
|
||||||
|
emit(note, midi_vel(lvl)); // → USB-MIDI note-on
|
||||||
|
if prio(lvl) > prio(fired_best) {
|
||||||
fired_best = lvl;
|
fired_best = lvl;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
self.next[li] += self.durs[li][s].max(1);
|
self.next[li] += self.durs[li][s].max(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -468,6 +478,38 @@ fn lvl_bright(lvl: u8) -> u8 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sound name → General MIDI drum note (channel 10). Ports pico-scroll/app.py's SOUND_GM.
|
||||||
|
fn gm_note(sound: &str) -> u8 {
|
||||||
|
match sound {
|
||||||
|
"kick" | "kick808" | "kick909" => 36,
|
||||||
|
"snare" | "snare808" | "snare909" => 38,
|
||||||
|
"clap" | "clap808" | "clap909" => 39,
|
||||||
|
"rim" => 37,
|
||||||
|
"hatClosed" | "hat808" | "hat909" => 42,
|
||||||
|
"hatOpen" | "openHat808" => 46,
|
||||||
|
"ride" | "ride909" => 51,
|
||||||
|
"crash" | "crash909" => 49,
|
||||||
|
"tomLow" => 41,
|
||||||
|
"tom808" | "tomMid" => 45,
|
||||||
|
"tomHigh" => 48,
|
||||||
|
"tambourine" => 54,
|
||||||
|
"cowbell" | "cowbell808" => 56,
|
||||||
|
"woodblock" | "jamblock" => 76,
|
||||||
|
"claves" => 75,
|
||||||
|
_ => 37, // beep / unknown → GM_DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Level → MIDI velocity (accent / normal / ghost). Ports pico-scroll/app.py's MIDI_VEL.
|
||||||
|
fn midi_vel(level: u8) -> u8 {
|
||||||
|
match level {
|
||||||
|
2 => 120,
|
||||||
|
1 => 90,
|
||||||
|
3 => 45,
|
||||||
|
_ => 90,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================== RENDERING ==============================
|
// ============================== RENDERING ==============================
|
||||||
fn render<I: I2c>(m: &mut Matrix<I>, app: &App, now_ns: i64) {
|
fn render<I: I2c>(m: &mut Matrix<I>, app: &App, now_ns: i64) {
|
||||||
// downbeat strobe: the whole matrix at full brightness on "the 1"
|
// downbeat strobe: the whole matrix at full brightness on "the 1"
|
||||||
|
|
@ -624,7 +666,8 @@ fn draw_pendulum<I: I2c>(m: &mut Matrix<I>, app: &App, now_ns: i64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Boot splash: scroll "PM-G1 GRID" once, right-to-left. Doubles as a liveness + pixel-map check.
|
/// Boot splash: scroll "PM-G1 GRID" once, right-to-left. Doubles as a liveness + pixel-map check.
|
||||||
fn splash<I: I2c>(m: &mut Matrix<I>, delay: &mut Delay) {
|
/// `poll` is called frequently during the per-frame delay so USB enumeration isn't stalled.
|
||||||
|
fn splash<I: I2c, P: FnMut()>(m: &mut Matrix<I>, delay: &mut Delay, mut poll: P) {
|
||||||
let cols = build_name_cols("PM-G1 GRID");
|
let cols = build_name_cols("PM-G1 GRID");
|
||||||
let n = cols.len() as i32;
|
let n = cols.len() as i32;
|
||||||
let mut off = -16i32;
|
let mut off = -16i32;
|
||||||
|
|
@ -642,7 +685,10 @@ fn splash<I: I2c>(m: &mut Matrix<I>, delay: &mut Delay) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.show();
|
m.show();
|
||||||
delay.delay_ms(45);
|
for _ in 0..30 {
|
||||||
|
poll();
|
||||||
|
delay.delay_us(1500); // ~45ms/frame, polling USB every 1.5ms
|
||||||
|
}
|
||||||
off += 1;
|
off += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -657,6 +703,7 @@ fn main() -> ! {
|
||||||
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
||||||
unsafe { HEAP.init(core::ptr::addr_of_mut!(HEAP_MEM) as usize, HEAP_SIZE) }
|
unsafe { HEAP.init(core::ptr::addr_of_mut!(HEAP_MEM) as usize, HEAP_SIZE) }
|
||||||
}
|
}
|
||||||
|
info!("== pm-grid boot == heap {} free", HEAP.free());
|
||||||
|
|
||||||
let mut pac = hal::pac::Peripherals::take().unwrap();
|
let mut pac = hal::pac::Peripherals::take().unwrap();
|
||||||
let core = hal::pac::CorePeripherals::take().unwrap();
|
let core = hal::pac::CorePeripherals::take().unwrap();
|
||||||
|
|
@ -691,7 +738,31 @@ fn main() -> ! {
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut mtx = Matrix::new(i2c, &mut delay);
|
let mut mtx = Matrix::new(i2c, &mut delay);
|
||||||
let _ = splash(&mut mtx, &mut delay);
|
|
||||||
|
// --- USB-MIDI: the Scroll Pack has no speaker, so clicks play through the host (the editor's
|
||||||
|
// "Device audio"). We send a GM note-on per lane hit on channel 10. ---
|
||||||
|
let usb_bus = UsbBusAllocator::new(hal::usb::UsbBus::new(
|
||||||
|
pac.USBCTRL_REGS,
|
||||||
|
pac.USBCTRL_DPRAM,
|
||||||
|
clocks.usb_clock,
|
||||||
|
true,
|
||||||
|
&mut pac.RESETS,
|
||||||
|
));
|
||||||
|
let mut midi = UsbMidiClass::new(&usb_bus, 1, 1).unwrap();
|
||||||
|
let mut usb_dev = UsbDeviceBuilder::new(&usb_bus, UsbVidPid(0x16c0, 0x5e4))
|
||||||
|
.strings(&[StringDescriptors::default()
|
||||||
|
.manufacturer("VARASYS")
|
||||||
|
.product("PM_G-1 Grid")
|
||||||
|
.serial_number("PMG1")])
|
||||||
|
.unwrap()
|
||||||
|
.device_class(0)
|
||||||
|
.build();
|
||||||
|
info!("usb-midi configured (channel 10)");
|
||||||
|
|
||||||
|
// boot splash (polls USB throughout so the host can enumerate during the animation)
|
||||||
|
splash(&mut mtx, &mut delay, || {
|
||||||
|
usb_dev.poll(&mut [&mut midi]);
|
||||||
|
});
|
||||||
|
|
||||||
// buttons (active-low, internal pull-ups): A=GP12 B=GP13 X=GP14 Y=GP15
|
// buttons (active-low, internal pull-ups): A=GP12 B=GP13 X=GP14 Y=GP15
|
||||||
let mut btn_a = pins.gpio12.into_pull_up_input();
|
let mut btn_a = pins.gpio12.into_pull_up_input();
|
||||||
|
|
@ -701,6 +772,7 @@ fn main() -> ! {
|
||||||
|
|
||||||
let now_us = || timer.get_counter().ticks() as i64;
|
let now_us = || timer.get_counter().ticks() as i64;
|
||||||
let mut app = App::new(now_us() * 1000);
|
let mut app = App::new(now_us() * 1000);
|
||||||
|
info!("groove: bpm={} lanes={}", app.tempo, app.track.lanes.len());
|
||||||
|
|
||||||
// input edge/hold state
|
// input edge/hold state
|
||||||
let (mut pa, mut pb, mut px, mut py) = (false, false, false, false);
|
let (mut pa, mut pb, mut px, mut py) = (false, false, false, false);
|
||||||
|
|
@ -708,11 +780,15 @@ fn main() -> ! {
|
||||||
let (mut held_x, mut held_y) = (0i64, 0i64);
|
let (mut held_x, mut held_y) = (0i64, 0i64);
|
||||||
let (mut nextrep_x, mut nextrep_y) = (0i64, 0i64);
|
let (mut nextrep_x, mut nextrep_y) = (0i64, 0i64);
|
||||||
let mut last_frame_us = 0i64;
|
let mut last_frame_us = 0i64;
|
||||||
|
let mut hb_us = 0i64;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let us = now_us();
|
let us = now_us();
|
||||||
let now_ns = us * 1000;
|
let now_ns = us * 1000;
|
||||||
|
|
||||||
|
// ---- USB: poll every iteration to stay enumerated and flush MIDI ----
|
||||||
|
usb_dev.poll(&mut [&mut midi]);
|
||||||
|
|
||||||
// ---- inputs (active-low) ----
|
// ---- inputs (active-low) ----
|
||||||
let a = btn_a.is_low().unwrap_or(false);
|
let a = btn_a.is_low().unwrap_or(false);
|
||||||
let b = btn_b.is_low().unwrap_or(false);
|
let b = btn_b.is_low().unwrap_or(false);
|
||||||
|
|
@ -766,8 +842,10 @@ fn main() -> ! {
|
||||||
px = x;
|
px = x;
|
||||||
py = y;
|
py = y;
|
||||||
|
|
||||||
// ---- scheduler ----
|
// ---- scheduler: advance clocks, send a USB-MIDI note-on per lane hit (ch10) ----
|
||||||
app.tick(now_ns);
|
app.tick(now_ns, |note, vel| {
|
||||||
|
let _ = midi.send_bytes([0x09, 0x99, note, vel]); // cable 0, note-on, channel 10
|
||||||
|
});
|
||||||
|
|
||||||
// ---- ticker scroll advance (~120ms) ----
|
// ---- ticker scroll advance (~120ms) ----
|
||||||
// (uses the frame clock implicitly; scroll_off wraps mod scroll_total)
|
// (uses the frame clock implicitly; scroll_off wraps mod scroll_total)
|
||||||
|
|
@ -780,6 +858,19 @@ fn main() -> ! {
|
||||||
render(&mut mtx, &app, now_ns);
|
render(&mut mtx, &app, now_ns);
|
||||||
}
|
}
|
||||||
|
|
||||||
delay.delay_us(2000);
|
// heartbeat (~1 Hz) for probe-rs/defmt debugging
|
||||||
|
if us - hb_us > 1_000_000 {
|
||||||
|
hb_us = us;
|
||||||
|
info!(
|
||||||
|
"alive: playing={} bpm={} step={} usb={} heap={}",
|
||||||
|
app.playing,
|
||||||
|
app.tempo,
|
||||||
|
app.step.first().copied().unwrap_or(-1),
|
||||||
|
usb_dev.state() as u8,
|
||||||
|
HEAP.free()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
delay.delay_us(1000); // ~1 kHz loop: tight enough for USB polling + click timing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue