Commit 35fb6edb by PLN (Algolia)

fix(rig): the LCXL checks knew only the v2 board, and Ardour launched off-graph

The Bridge's RIG UP ran tools/gig-up.sh --converge, whose converge never
starts lcxl3-driver (boot policy manual), and whose LCXL checks grep the
v2 name 'Launch Control XL' — so a v3 board plugged in read as absent,
painted by nobody, and the gate still said GO. Meanwhile the desktop
launcher runs the ROOT gig-up.sh, a second launcher with the leds() the
gate lacks — two launchers, each owning half the surface story.

- launchers: Ardour launches under pw-jack like gig-up does (bare ardour
  starts its own jackd and splits the graph from scsynth)
- gig-up: 'LCXL present' matches LCXL3 too; 'LCXL LEDs' is
  generation-aware (LCXL3 -> lcxl3-driver, original -> lcxl-leds-watch)
- lcxl-path: follows the v3 path LCXL3 -> driver's 'ParVagues LCXL3'
  virtual port -> SC, falling back to the Midi Through road for v2
parent fa548f0f
...@@ -70,6 +70,7 @@ LAUNCHERS = [ ...@@ -70,6 +70,7 @@ LAUNCHERS = [
{"key": "ardour", "name": "Ardour", "blurb": "DAW: the Tidal Live session (records the stems)", {"key": "ardour", "name": "Ardour", "blurb": "DAW: the Tidal Live session (records the stems)",
"candidates": ["ardour9", "ardour8", "ardour7", "ardour6", "ardour"], "candidates": ["ardour9", "ardour8", "ardour7", "ardour6", "ardour"],
"args": ([str(ARDOUR_SESSION)] if ARDOUR_SESSION.exists() else []), "args": ([str(ARDOUR_SESSION)] if ARDOUR_SESSION.exists() else []),
"wrap": ["pw-jack"],
"exe": r"[Aa]rdour[-\d.]*$"}, "exe": r"[Aa]rdour[-\d.]*$"},
{"key": "qjackctl", "name": "QjackCtl", "blurb": "JACK / audio graph control", {"key": "qjackctl", "name": "QjackCtl", "blurb": "JACK / audio graph control",
"candidates": ["qjackctl"], "args": [], "exe": r"qjackctl$"}, "candidates": ["qjackctl"], "args": [], "exe": r"qjackctl$"},
...@@ -391,7 +392,11 @@ def is_running(spec, lines=None): ...@@ -391,7 +392,11 @@ def is_running(spec, lines=None):
def _argv(spec): def _argv(spec):
inner = [binary(spec), *spec.get("args", [])] # `wrap` (e.g. Ardour's pw-jack) sits OUTSIDE the binary: ldconfig resolves
# libjack.so.0 to jackd2's, so a bare `ardour` starts its own jackd, grabs a
# card and splits the graph from scsynth's — the two-servers trap gig-up's
# launch note documents. Same mechanism, same reason, one line.
inner = [*spec.get("wrap", []), binary(spec), *spec.get("args", [])]
return _term_argv(inner) if spec.get("terminal") else inner return _term_argv(inner) if spec.get("terminal") else inner
......
...@@ -423,7 +423,7 @@ soft "transition ghosts" \ ...@@ -423,7 +423,7 @@ soft "transition ghosts" \
# venue, a warn here means the set has no hands. # venue, a warn here means the set has no hands.
soft "LCXL present" \ soft "LCXL present" \
"replug the LCXL (USB OUT endpoint stalls — LEDs dark but input alive), then tools/lcxl-init.py" \ "replug the LCXL (USB OUT endpoint stalls — LEDs dark but input alive), then tools/lcxl-init.py" \
bash -c 'aseqdump -l 2>/dev/null | grep -qi "Launch Control XL"' bash -c 'aseqdump -l 2>/dev/null | grep -qiE "Launch Control XL|LCXL3"'
# --- 7b. the live chain, opt-in (--live) ------------------------------------- # --- 7b. the live chain, opt-in (--live) -------------------------------------
# Everything above proves the SET is correct. None of it proves a RIG EXISTS. # Everything above proves the SET is correct. None of it proves a RIG EXISTS.
...@@ -488,9 +488,19 @@ if (( LIVE )); then ...@@ -488,9 +488,19 @@ if (( LIVE )); then
# The surface being WIRED is not the surface being LIT. Separate daemon, separate # The surface being WIRED is not the surface being LIT. Separate daemon, separate
# failure, and until tonight nothing checked it: this is the exact gap that let # failure, and until tonight nothing checked it: this is the exact gap that let
# PLN arrive at "i dont see midi feedback visuel anymore" the day before a gig. # PLN arrive at "i dont see midi feedback visuel anymore" the day before a gig.
# Generation-aware, same rule as the root launcher's leds(): an LCXL3 is
# painted by lcxl3-driver (and Conflicts= the v2 watcher), the original LCXL
# by lcxl-leds-watch. Name-match both generations — the v3 board enumerates
# as "LCXL3 1", and a v2-only grep made this check pass by accident.
soft "LCXL LEDs" \ soft "LCXL LEDs" \
"systemctl --user start lcxl-leds-watch — board is plugged in but nothing is painting it" \ "start the painter: LCXL3 → systemctl --user start lcxl3-driver; original LCXL → systemctl --user start lcxl-leds-watch" \
bash -c '! aseqdump -l 2>/dev/null | grep -qi "Launch Control XL" || systemctl --user is-active --quiet lcxl-leds-watch.service' bash -c 'if aseqdump -l 2>/dev/null | grep -qi "LCXL3"; then
systemctl --user is-active --quiet lcxl3-driver.service
elif aseqdump -l 2>/dev/null | grep -qi "Launch Control XL"; then
systemctl --user is-active --quiet lcxl-leds-watch.service
else
exit 0
fi'
# Asks whether THIS instance warmed, by keying to the unit's own start time — # Asks whether THIS instance warmed, by keying to the unit's own start time —
# not "was there ever a PRELOAD line", which a previous boot would satisfy forever. # not "was there ever a PRELOAD line", which a previous boot would satisfy forever.
......
...@@ -22,16 +22,17 @@ else: ...@@ -22,16 +22,17 @@ else:
except (OSError, subprocess.SubprocessError): except (OSError, subprocess.SubprocessError):
sys.exit(2) sys.exit(2)
# client id -> name, and (client:port) -> set of destination clients # client id -> name, port name by (client:port), and (client:port) -> destinations
names, edges, client, port = {}, {}, None, None names, pnames, edges, client, port = {}, {}, {}, None, None
for line in out.splitlines(): for line in out.splitlines():
m = re.match(r"client (\d+): '([^']*)'", line) m = re.match(r"client (\d+): '([^']*)'", line)
if m: if m:
client, names[int(m.group(1))] = int(m.group(1)), m.group(2) client, names[int(m.group(1))] = int(m.group(1)), m.group(2)
continue continue
m = re.match(r"\s+(\d+) '", line) m = re.match(r"\s+(\d+) '([^']*)'", line)
if m and client is not None: if m and client is not None:
port = (client, int(m.group(1))) port = (client, int(m.group(1)))
pnames[port] = m.group(2)
continue continue
m = re.match(r"\s+Connecting To: (.*)", line) m = re.match(r"\s+Connecting To: (.*)", line)
if m and port is not None: if m and port is not None:
...@@ -41,7 +42,7 @@ for line in out.splitlines(): ...@@ -41,7 +42,7 @@ for line in out.splitlines():
def clients(pred): def clients(pred):
return {c for c, n in names.items() if pred(n)} return {c for c, n in names.items() if pred(n)}
lcxl = clients(lambda n: "Launch Control XL" in n) lcxl = clients(lambda n: "Launch Control XL" in n or "LCXL3" in n)
sc = clients(lambda n: n == "SuperCollider") sc = clients(lambda n: n == "SuperCollider")
thru = clients(lambda n: "Midi Through" in n) thru = clients(lambda n: "Midi Through" in n)
if not lcxl: if not lcxl:
...@@ -49,6 +50,20 @@ if not lcxl: ...@@ -49,6 +50,20 @@ if not lcxl:
if not sc: if not sc:
print("SuperCollider is not on the MIDI bus at all"); sys.exit(1) print("SuperCollider is not on the MIDI bus at all"); sys.exit(1)
# The v3 board routes through the LCXL3 driver: the board reaches SC via the
# driver's "ParVagues LCXL3" virtual port, not via Midi Through — and chasing
# the PipeWire relay mesh instead would make this check unable to fail again
# (everything reaches everything through 143). So: the driver's virtual port
# existing AND holding an edge into SC IS the v3 path; Midi Through stays the
# v2 board's road. NB the virtual port is a PORT name ("ParVagues LCXL3" under
# client 'RtMidiOut Client'), so it is matched per-port, not per-client.
drv = {p for p, n in pnames.items() if "ParVagues LCXL3" in n}
if drv:
reach = {d for p in drv for d in edges.get(p, set())}
if reach & sc:
print("LCXL3 -> lcxl3-driver -> SC"); sys.exit(0)
print("LCXL3 driver is up but its virtual port does NOT reach SC"); sys.exit(1)
# one hop, then two hops via Midi Through # one hop, then two hops via Midi Through
reach = {d for (c, _), ds in edges.items() if c in lcxl for d in ds} reach = {d for (c, _), ds in edges.items() if c in lcxl for d in ds}
if reach & sc: if reach & sc:
......
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