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:
parent
4275187008
commit
35726b57ac
1 changed files with 40 additions and 3 deletions
|
|
@ -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),
|
'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),
|
'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 ==============================
|
# ============================== APP ==============================
|
||||||
class 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.rep = None; self.end = None # per-track playback flow: rep=cycles, end=stop|next|+/-N goto
|
||||||
self.continue_on = False; self._advance = False
|
self.continue_on = False; self._advance = False
|
||||||
self._next_pending = None; self._seam_t = 0
|
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._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._beat_ns = 60_000_000_000 // self.bpm
|
||||||
self._note_buf = bytearray([0x90, 0, 0])
|
self._note_buf = bytearray([0x90, 0, 0])
|
||||||
self._clock_byte = bytes([0xF8]); self._start_byte = bytes([0xFA]); self._stop_byte = bytes([0xFC])
|
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:
|
if v != self.bpm:
|
||||||
self.bpm = v; self._beat_ns = 60_000_000_000 // v
|
self.bpm = v; self._beat_ns = 60_000_000_000 // v
|
||||||
self._rebuild_dur_all(); self.dirty = True
|
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)
|
self._sync_broadcast("bpm=%d" % v)
|
||||||
def goto(self, i):
|
def goto(self, i):
|
||||||
was = self.running
|
was = self.running
|
||||||
|
|
@ -708,10 +715,35 @@ class App:
|
||||||
if lvl == 1: return max(8, BRIGHTNESS // 4) # normal
|
if lvl == 1: return max(8, BRIGHTNESS // 4) # normal
|
||||||
if lvl == 3: return max(3, BRIGHTNESS // 16) # ghost
|
if lvl == 3: return max(3, BRIGHTNESS // 16) # ghost
|
||||||
return 0
|
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):
|
def render(self):
|
||||||
self.mtx.clear()
|
self.mtx.clear()
|
||||||
if self.view == 2: self._render_bpm()
|
v = self.view
|
||||||
elif self.view == 1: self._render_pendulum()
|
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()
|
else: self._render_grid()
|
||||||
self.mtx.show()
|
self.mtx.show()
|
||||||
def _render_grid(self):
|
def _render_grid(self):
|
||||||
|
|
@ -927,6 +959,8 @@ class App:
|
||||||
boot = time.monotonic()
|
boot = time.monotonic()
|
||||||
try: os.stat("/trial"); committed = False
|
try: os.stat("/trial"); committed = False
|
||||||
except OSError: committed = True
|
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
|
next_frame = 0.0
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
|
@ -940,6 +974,9 @@ class App:
|
||||||
self._sync_broadcast_full()
|
self._sync_broadcast_full()
|
||||||
if self.running and tnow >= next_frame: # keep pendulum/playhead moving even with no input
|
if self.running and tnow >= next_frame: # keep pendulum/playhead moving even with no input
|
||||||
self.dirty = True; next_frame = tnow + 0.04
|
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:
|
if self.dirty:
|
||||||
self.dirty = False; self.render()
|
self.dirty = False; self.render()
|
||||||
time.sleep(0.0005)
|
time.sleep(0.0005)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue