From 35726b57aca6fb15985bf8f075bb7d4acef93039 Mon Sep 17 00:00:00 2001 From: Me Here Date: Sun, 31 May 2026 21:08:33 -0500 Subject: [PATCH] PM_G-1: scrolling boot splash (model name) + transient BPM flash on tempo nudge The splash doubles as a liveness/pixel-map check (if 'PM-G1 GRID' reads correctly, the firmware booted and the LED mapping is right). The BPM flash makes X/Y tempo nudges visible from any view (previously invisible in Grid/Pendulum). Co-Authored-By: Claude Opus 4.8 (1M context) --- pico-scroll/app.py | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/pico-scroll/app.py b/pico-scroll/app.py index 4b1dc26..0697abe 100644 --- a/pico-scroll/app.py +++ b/pico-scroll/app.py @@ -272,6 +272,11 @@ DIGITS = { '4': (5, 5, 7, 1, 1), '5': (7, 4, 7, 1, 7), '6': (7, 4, 7, 5, 7), '7': (7, 1, 2, 2, 2), '8': (7, 5, 7, 5, 7), '9': (7, 5, 7, 1, 7), } +# 3x5 uppercase letters + marks for the boot splash (scrolls the model name "PM-G1 GRID") +LETTERS = { + 'P': (7, 5, 7, 4, 4), 'M': (5, 7, 7, 5, 5), 'G': (7, 4, 5, 5, 7), 'R': (7, 5, 7, 6, 5), + 'I': (7, 2, 2, 2, 7), 'D': (6, 5, 5, 5, 6), '-': (0, 0, 7, 0, 0), ' ': (0, 0, 0, 0, 0), +} # ============================== APP ============================== class App: @@ -301,8 +306,9 @@ class App: self.rep = None; self.end = None # per-track playback flow: rep=cycles, end=stop|next|+/-N goto self.continue_on = False; self._advance = False self._next_pending = None; self._seam_t = 0 - self.view = 0 # 0 = Grid, 1 = Pendulum, 2 = BPM (button B cycles) + self.view = 0 # 0 = Grid, 1 = Pendulum, 2 = BPM (button A-hold cycles) self._beatflash = 0; self._beatflash_off = 0 + self._bpm_flash = 0 # while set, render() briefly shows the BPM view (so X/Y nudges are visible in any view) self._beat_ns = 60_000_000_000 // self.bpm self._note_buf = bytearray([0x90, 0, 0]) self._clock_byte = bytes([0xF8]); self._start_byte = bytes([0xFA]); self._stop_byte = bytes([0xFC]) @@ -531,6 +537,7 @@ class App: if v != self.bpm: self.bpm = v; self._beat_ns = 60_000_000_000 // v self._rebuild_dur_all(); self.dirty = True + self._bpm_flash = time.monotonic() + 0.7 # flash the tempo so the nudge is visible even in Grid/Pendulum self._sync_broadcast("bpm=%d" % v) def goto(self, i): was = self.running @@ -708,10 +715,35 @@ class App: if lvl == 1: return max(8, BRIGHTNESS // 4) # normal if lvl == 3: return max(3, BRIGHTNESS // 16) # ghost return 0 + def _splash(self, text): + # Scroll text across the matrix once at boot, right-to-left, as a 3x5 font on rows 1..5. + # Doubles as a liveness + pixel-map check: if "PM-G1 GRID" reads correctly, the firmware + # booted and the LED mapping is right. + cols = [] + for ch in text: + g = DIGITS.get(ch) or LETTERS.get(ch.upper()) or LETTERS.get(' ') + for cx in range(3): + c = 0 + for ry in range(5): + if g[ry] & (1 << (2 - cx)): c |= (1 << ry) + cols.append(c) + cols.append(0) # 1px gap between glyphs + n = len(cols); m = self.mtx; off = -16 + while off < n: + m.clear() + for x in range(17): + ci = x + off + if 0 <= ci < n: + c = cols[ci] + for ry in range(5): + if c & (1 << ry): m.set(x, ry + 1, BRIGHTNESS) + m.show(); time.sleep(0.05); off += 1 def render(self): self.mtx.clear() - if self.view == 2: self._render_bpm() - elif self.view == 1: self._render_pendulum() + v = self.view + if v != 2 and self._bpm_flash and time.monotonic() < self._bpm_flash: v = 2 # transient tempo readout + if v == 2: self._render_bpm() + elif v == 1: self._render_pendulum() else: self._render_grid() self.mtx.show() def _render_grid(self): @@ -927,6 +959,8 @@ class App: boot = time.monotonic() try: os.stat("/trial"); committed = False except OSError: committed = True + try: self._splash("PM-G1 GRID") # boot banner (scrolls once); wrapped so a splash bug never blocks the app + except Exception: pass next_frame = 0.0 while True: try: @@ -940,6 +974,9 @@ class App: self._sync_broadcast_full() if self.running and tnow >= next_frame: # keep pendulum/playhead moving even with no input self.dirty = True; next_frame = tnow + 0.04 + if self._bpm_flash: # keep rendering through the tempo flash, then one frame to revert + if tnow >= self._bpm_flash: self._bpm_flash = 0 + self.dirty = True if self.dirty: self.dirty = False; self.render() time.sleep(0.0005)