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) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-31 21:08:33 -05:00
parent 4275187008
commit 35726b57ac

View file

@ -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)