Commit 7bda1ccd by PLN (Algolia)

gig-up: the gate that could not fail, and the two units nobody started

Follow-up to 72937cc7. Having found that the Bridge panel could not see the LED
watcher, I went looking for what else was watching the surface. Nothing was —
and one of the things that claimed to be could not fail.

THE FALSE-GREEN GATE. The pre-gig check for "LCXL -> SC" was:

    aconnect -l | grep -A3 "Launch Control XL" | grep -q "128:"

`grep -A3` matches each of the THREE lines containing "Launch Control XL" and
prints three lines after each; the last window runs off the end of the LCXL
block and into the next client header, which is literally
`client 128: 'SuperCollider'`. So the string "128:" was in the haystack whether
or not a single wire existed. This gate has been passing since it was written,
for a reason unrelated to what it measures. It would have said green with the
desk connected to nothing.

Proved rather than argued: recorded the live graph, stripped only the
Midi Through -> SC edge from it, and ran both gates against the doctored file.
Old gate PASSED. New gate reported "LCXL -> Midi Through, but Through does NOT
reach SC" and exited 1.

tools/lcxl-path.py replaces it by walking the graph. It has to: the real path
here is TWO hops (LCXL 20:0 -> Midi Through 14:0 -> SC in2), so a direct-only
test would be wrong in the other direction. It takes an optional recorded-graph
file argument precisely so the gate can be tested for FAILURE without
unplugging hardware at a venue — the reason the old one was never caught is
that nobody could cheaply watch it fail.

TWO UNITS NOBODY STARTED. lcxl-leds-watch and midi-autoconnect were both
`disabled`, so neither came up at login; they ran only when started by hand.
Both now join the auto-start loop.

A NEW GATE. "The surface is wired" and "the surface is lit" are different
daemons and different failures, and only the first was ever checked. The new
"LCXL LEDs" soft check reads: if the board is on the bus, the watcher must be
active. Deliberately conditional on the board — at a kitchen table an unplugged
desk is normal and must not warn, but at the venue this is the difference
between playing blind and not.

Verified both new gates in BOTH directions: green as the rig stands, and red
with the watcher stopped / the Through edge cut. A gate observed only passing
is indistinguishable from the one this commit deletes.
parent 72937cc7
...@@ -218,7 +218,11 @@ if (( CONVERGE )); then ...@@ -218,7 +218,11 @@ if (( CONVERGE )); then
for _ in $(seq 1 50); do sleep 2; pgrep -x scsynth >/dev/null 2>&1 && break; done for _ in $(seq 1 50); do sleep 2; pgrep -x scsynth >/dev/null 2>&1 && break; done
fi fi
for u in parvagues-sc-watchdog tidal-ardour-autoroute; do # lcxl-leds-watch and midi-autoconnect joined this list on 2026-08-21: both were
# `disabled`, so neither started at login, and the LED watcher had been STOPPED
# with the board unplugged — `Restart=always` does not undo a stop. The surface
# was dark for a whole session and nothing here or on the Bridge panel noticed.
for u in parvagues-sc-watchdog tidal-ardour-autoroute lcxl-leds-watch midi-autoconnect; do
if ! systemctl --user is-active --quiet "$u.service"; then if ! systemctl --user is-active --quiet "$u.service"; then
systemctl --user start "$u.service" >>"$LOG" 2>&1 && { echo " ${G}+${Z} started $u"; did=1; } systemctl --user start "$u.service" >>"$LOG" 2>&1 && { echo " ${G}+${Z} started $u"; did=1; }
fi fi
...@@ -459,9 +463,23 @@ if (( LIVE )); then ...@@ -459,9 +463,23 @@ if (( LIVE )); then
"open the set in Pulsar and boot Tidal (nothing is sending patterns yet)" \ "open the set in Pulsar and boot Tidal (nothing is sending patterns yet)" \
bash -c 'ss -lunp 2>/dev/null | grep -q ":6010"' bash -c 'ss -lunp 2>/dev/null | grep -q ":6010"'
# This was `aconnect -l | grep -A3 "Launch Control XL" | grep -q "128:"`, which
# PASSED UNCONDITIONALLY: -A3 runs off the end of the LCXL block into the
# `client 128: 'SuperCollider'` header, so "128:" was always in the window even
# with the desk wired to nothing. Verified 2026-08-21 against a doctored graph —
# old gate green, new gate red. tools/lcxl-path.py walks the graph instead, and
# takes a recorded-graph argument so it can be tested for FAILURE without
# unplugging anything at the venue.
soft "LCXL -> SC" \ soft "LCXL -> SC" \
"replug the LCXL, then tools/lcxl-init.py — knobs will move nothing until this is up" \ "replug the LCXL, then tools/lcxl-init.py — knobs will move nothing until this is up" \
bash -c 'aconnect -l 2>/dev/null | grep -A3 "Launch Control XL" | grep -q "128:"' python3 tools/lcxl-path.py
# 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
# PLN arrive at "i dont see midi feedback visuel anymore" the day before a gig.
soft "LCXL LEDs" \
"systemctl --user start lcxl-leds-watch — board is plugged in but nothing is painting it" \
bash -c '! aseqdump -l 2>/dev/null | grep -qi "Launch Control XL" || systemctl --user is-active --quiet lcxl-leds-watch.service'
# 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.
......
#!/usr/bin/env python3
"""Does the LCXL actually reach SuperCollider? Exit 0 if yes.
Replaces `aconnect -l | grep -A3 "Launch Control XL" | grep -q "128:"`, which
passed unconditionally: -A3 runs off the end of the LCXL block into the
`client 128: 'SuperCollider'` HEADER, so the string "128:" was always present
whether or not a single wire existed. A gate that cannot fail is not a gate.
The real path on this box is two hops -- LCXL 20:0 -> Midi Through 14:0 -> SC
in2 -- so a direct-only test would also be wrong. Follow the graph instead.
"""
import pathlib, re, subprocess, sys
# An optional file argument feeds a recorded graph in, so the gate itself can be
# tested for FAILURE without unplugging anything the night before a gig.
if len(sys.argv) > 1:
out = pathlib.Path(sys.argv[1]).read_text()
else:
try:
out = subprocess.run(["aconnect", "-l"], capture_output=True, text=True,
timeout=5).stdout
except (OSError, subprocess.SubprocessError):
sys.exit(2)
# client id -> name, and (client:port) -> set of destination clients
names, edges, client, port = {}, {}, None, None
for line in out.splitlines():
m = re.match(r"client (\d+): '([^']*)'", line)
if m:
client, names[int(m.group(1))] = int(m.group(1)), m.group(2)
continue
m = re.match(r"\s+(\d+) '", line)
if m and client is not None:
port = (client, int(m.group(1)))
continue
m = re.match(r"\s+Connecting To: (.*)", line)
if m and port is not None:
dst = {int(x) for x in re.findall(r"(\d+):\d+", m.group(1))}
edges.setdefault(port, set()).update(dst)
def clients(pred):
return {c for c, n in names.items() if pred(n)}
lcxl = clients(lambda n: "Launch Control XL" in n)
sc = clients(lambda n: n == "SuperCollider")
thru = clients(lambda n: "Midi Through" in n)
if not lcxl:
print("no LCXL on the bus"); sys.exit(1)
if not sc:
print("SuperCollider is not on the MIDI bus at all"); sys.exit(1)
# one hop, then two hops via Midi Through
reach = {d for (c, _), ds in edges.items() if c in lcxl for d in ds}
if reach & sc:
print("LCXL -> SC direct"); sys.exit(0)
if reach & thru:
onward = {d for (c, _), ds in edges.items() if c in thru for d in ds}
if onward & sc:
print("LCXL -> Midi Through -> SC"); sys.exit(0)
print("LCXL -> Midi Through, but Through does NOT reach SC"); sys.exit(1)
print(f"LCXL reaches {sorted(reach)} — neither SC nor Midi Through"); sys.exit(1)
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