Commit e7e441a9 by PLN (Algolia)

feat(lcxl3): row order is ergonomics — and the lap clock

PLN, from the chair: seated, OLED row 2 reads clearly and row 3 hides behind
the bezel. So the information hierarchy now follows the sightline, not the
protocol's field order:

- HOME: track name / "118BPM 4:32" / ParVagues. The drip moves to the bezel
  row; tempo and TIME PLAYED take the prime row. The elapsed counter is the
  overstretch check PLN asked for ("maybe time played that track eventually
  when in a set recording to help know when overstretching") — refreshed every
  10 s by a field write on the stationary target, no re-summon. Lap reset
  belongs to the future Track </> setlist cursor; today, track load = lap 0.
- DJF overlay: "DJF percs" / "LPF 2.4kHz !!" / (blank). The cell name is gone
  entirely — "C1 C2 C3 are noise i know the button positions ahah".
- Gates: "d1 gate" / "ON" / context. Value rides row 2 everywhere.
- The zero-mark heart animation moved to row 2 with everything else.

Display byte-pinning tests still pass; the ghost mirrors the new layout.
parent acdaacf1
......@@ -492,6 +492,10 @@ class Driver:
# default (orDef 0). Row F stays momentary: gestures are held, and
# the panic chord needs four simultaneous 127s.
self.latch: dict[int, bool] = {}
# The lap clock: elapsed-on-this-track, shown on home row 2 so a
# set's overstretch is visible at a glance. Reset belongs to the
# future Track </> setlist cursor — for now, track load = lap start.
self.track_started = time.time()
def start(self) -> None:
print(f"enabling DAW mode (the only mode that paints, and the only one")
......@@ -577,9 +581,12 @@ class Driver:
if v2 in self.mapped and self.lang.animated(v2, self.values.get(v2, 64)):
self.paint_cell(v2)
def _home_detail(self) -> str:
el = int(time.time() - self.track_started)
return f"{self.bpm:g}BPM {el // 60}:{el % 60:02d}"
def oled_home(self) -> None:
t, pmt, val = self.lang.home_lines(self.track_name,
f"{self.bpm:g} BPM")
t, pmt, val = self.lang.home_lines(self.track_name, self._home_detail())
self.surface.configure_display(L.TGT_STATIONARY, 2)
for field, line in enumerate((t, pmt, val)):
self.surface.set_text(L.TGT_STATIONARY, field, line)
......@@ -675,7 +682,7 @@ class Driver:
def ghost_write(self) -> None:
try:
age = time.time() - self._oled_up_at
t, pmt, val = self.lang.home_lines(self.track_name, f"{self.bpm:g} BPM")
t, pmt, val = self.lang.home_lines(self.track_name, self._home_detail())
home = (t, pmt, val)
cc = getattr(self, "_last_cc", None)
if age >= 1.2:
......@@ -710,9 +717,9 @@ class Driver:
if role != "family_filter" or not (61 <= v <= 67):
return
frame = self.lang.ZERO_FRAMES[int(now * 3) % len(self.lang.ZERO_FRAMES)]
if frame != self._oled_lines[2]:
self.surface.set_text(L.TGT_TEMPORARY, 2, frame)
self._oled_lines = (self._oled_lines[0], self._oled_lines[1], frame)
if frame != self._oled_lines[1]:
self.surface.set_text(L.TGT_TEMPORARY, 1, frame)
self._oled_lines = (self._oled_lines[0], frame, self._oled_lines[2])
self.ghost_write()
def stop(self) -> None:
......@@ -860,6 +867,12 @@ class Driver:
if self.lang and now - last_assert > 2.0:
self.reassert_paint()
last_assert = now
if self.lang and now - last_home > 10.0:
# tick the lap clock on home row 2; a field write on the
# stationary target needs no re-summon
self.surface.set_text(L.TGT_STATIONARY, 1, self._home_detail())
self.ghost_write()
last_home = now
if self.lang and now - last_labels > 20.0:
# Same reason `reassert_paint` exists: a DAW-mode round trip (a
# replug, Ardour grabbing the port, the LED endpoint stalling)
......
......@@ -150,18 +150,20 @@ def touch_lines(v2cc: int, value: int,
pattern — parsed from the .tidal. PLN: showing the cell name there "is less
useful than what it is (sample name or effect like 'rose:4' or '# crush')".
"""
# ROW ORDER IS ERGONOMICS, measured from PLN's chair (2026-08-29): seated,
# row 2 reads clearly and row 3 hides behind the screen's bottom bezel. So
# the row that matters goes SECOND, never third — and the cell name ("C1")
# is dropped from DJF overlays entirely: "i know the button positions ahah".
role, who = G.CC_ROLE.get(v2cc, ("?", 0))
lab = ctx or G.label(v2cc)
if role == "family_filter":
return (f"DJF {FAMILY_NAME.get(who, '?')}", lab, djf_readout(value))
return (f"DJF {FAMILY_NAME.get(who, '?')}", djf_readout(value), "")
if role == "family_mute":
return (f"MUTE {MUTE_NAME.get(who, '?')}", lab, str(value))
# Per-orbit cells lead with the ORBIT — "d6 fx2 / C6 / 100" — because the
# orbit is what PLN thinks in; the physical cell name is the secondary key.
return (f"MUTE {MUTE_NAME.get(who, '?')}", str(value), "")
title = f"d{who} {role}" if isinstance(who, int) and who else lab
if role in ("gate", "gate2"):
return (title, lab, "ON" if value > 0 else "off")
return (title, lab, str(value))
return (title, "ON" if value > 0 else "off", lab)
return (title, str(value), lab)
def control_label(v2cc: int, ctx: str | None = None,
......@@ -230,8 +232,10 @@ def djf_readout(value: int) -> str:
def home_lines(track: str, detail: str = "") -> tuple[str, str, str]:
"""(title, param, value) for the stationary track HUD."""
return (track.upper()[:16], "ParVagues", detail[:16])
"""The stationary track HUD, rows by PLN's chair ergonomics: the name, then
the row that matters (tempo + time played — the overstretch check), and the
ParVagues drip at the bottom where the bezel eats it anyway."""
return (track.upper()[:16], detail[:16], "ParVagues")
# ---------------------------------------------------------------- mapping ----
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment