Commit 96f13223 by PLN (Algolia)

feat(rig): the LCXL3 driver joins the supervised rig — one inventory, one painter

The night's lesson, converged: the driver only ran when a session remembered to
start it, then the session's background task got OOM-reaped and the surface sat
untranslated with every check green.

- tools/lcxl3-driver.service: repo-owned unit like its siblings; retry-forever
  hotplug posture; Conflicts=lcxl-leds-watch so exactly one painter owns the
  board; follow-mode (no --track) so paint tracks the loaded file.
- rig_units.py: lcxl3-driver + perf-tray rows (THE list is complete again),
  new 'manual' boot policy so --ensure can never start the v2 painter into a
  Conflicts= kill; --list --boot filter for shell consumers.
- parvagues-gear: moved into the repo; derives its sets from rig_units --list
  instead of being the fourth hardcoded copy; autostart-on now defers to
  --ensure (it used to enable parvagues-sc at login against policy).
- gig-up.sh leds(): detects the plugged surface generation and hands the LCXL3
  to the driver unit; v2 path invoked via python3 (exec-bit-proof).
parent 3fdf5b97
[Unit]
Description=LCXL3 translation driver (v3 DAW protocol -> v2 corpus CCs + paint + OLED)
Documentation=file:///home/pln/Work/Sound/Tidal/tools/lcxl3-driver.py
# Born 2026-09-05, the night the LCXL3 sat untranslated while every check was
# green: the driver only ran when a session remembered to start it, and the
# session that started it got reaped. Faders -> Ardour gains live on this path;
# without it the surface is a very nice-looking brick.
#
# The surface is hot-pluggable and the USB-C hub drops it. Same posture as
# lcxl-leds-watch: retry forever, bind within RestartSec of a replug.
# StartLimitIntervalSec=0 must live HERE in [Unit] — in [Service] systemd
# silently ignores it and the unit falls into `failed` after 5 restarts with
# the surface deaf for the rest of the session.
StartLimitIntervalSec=0
# ONE painter owns the board. The v2 watcher (lcxl-leds-watch) speaks the old
# device's SysEx and misreads the LCXL3's silence as a USB OUT stall. With
# Conflicts=, starting either painter stops the other — plugging a generation
# of surface picks its painter, and two writers can never fight over the LEDs.
Conflicts=lcxl-leds-watch.service
[Service]
Type=simple
WorkingDirectory=/home/pln/Work/Sound/Tidal
# Python block-buffers stdout under systemd; without this the journal shows
# nothing and the driver's diagnostics die unflushed in a 4 KB buffer.
Environment=PYTHONUNBUFFERED=1
# No --track: the driver follows ~/.cache/parvagues/current-track (<1 s tick),
# so paint and OLED labels track what is actually loaded instead of a name
# frozen at unit start. --relative was settled with PLN 2026-08-29 (the driver
# integrates encoder deltas and owns the values — pot-pickup cannot exist).
ExecStart=/usr/bin/python3 /home/pln/Work/Sound/Tidal/tools/lcxl3-driver.py --relative --paint
Restart=always
RestartSec=5
# Deliberately NOT niced down, unlike the LED watcher: this unit is the
# surface's nervous system (every fader move to Ardour crosses it), and its
# work is a few byte translations per event — starving it adds latency to
# the hands, not safety to the audio.
[Install]
WantedBy=default.target
#!/usr/bin/env bash
# parvagues-gear — pause/resume the whole ParVagues rig as one unit.
#
# Why: the gear set autostarts at login and runs forever — sclang+scsynth burn
# ~2.6W idle, and midi-autoconnect / tidal-ardour-autoroute poll every 2s.
# On battery (esp. this worn pack) that's real minutes. There was no "off".
#
# THE UNIT LIST IS NOT AUTHORED HERE. tools/rig_units.py owns it (the
# "list lived in three places" lesson); this script derives its sets from
# `rig_units.py --list`. Adding a unit to the rig = one row there, and gear
# pauses/resumes it with no edit here. "manual"-policy units (the v2 LED
# watcher) are paused if running but never resumed — resuming one would
# Conflicts=-kill the LCXL3 driver.
#
# ORDER MATTERS: parvagues-sc-watchdog is Restart=always and exists to restart
# SC when scsynth dies. Stop the watchdog FIRST or it resurrects SC; on resume,
# start SC FIRST so the watchdog doesn't race it.
set -euo pipefail
DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
WATCHDOG=parvagues-sc-watchdog
SC=parvagues-sc
REST=() # startable units minus SC + watchdog (order handled above)
MANUAL=() # never resumed; paused only if running
while IFS= read -r u; do
[[ $u == "$SC" || $u == "$WATCHDOG" ]] && continue
REST+=("$u")
done < <(python3 "$DIR/tools/rig_units.py" --list --boot auto)
while IFS= read -r u; do MANUAL+=("$u"); done \
< <(python3 "$DIR/tools/rig_units.py" --list --boot manual)
ALL=("$WATCHDOG" "$SC" "${REST[@]}" "${MANUAL[@]}")
pwr() {
local c v
c=$(cat /sys/class/power_supply/BAT0/current_now 2>/dev/null) || return 0
v=$(cat /sys/class/power_supply/BAT0/voltage_now)
awk -v c="$c" -v v="$v" 'BEGIN{printf "%.1fW", c/1e6*v/1e6}'
echo " · battery $(cat /sys/class/power_supply/BAT0/capacity)%"
}
case "${1:-status}" in
pause)
systemctl --user stop "$WATCHDOG" # before SC, or it restarts it
systemctl --user stop "$SC" "${REST[@]}" "${MANUAL[@]}"
echo "gear paused · $(pwr)"
;;
resume)
systemctl --user start "$SC" # before watchdog, avoid the race
systemctl --user start "$WATCHDOG" "${REST[@]}"
echo "gear resumed · $(pwr)"
;;
status)
for u in "${ALL[@]}"; do
printf ' %-28s %s\n' "$u" "$(systemctl --user is-active "$u")"
done
echo " $(pwr)"
;;
autostart-off) systemctl --user disable "${ALL[@]}" ;;
# enable-per-policy lives in ONE place; this also starts what should run.
# (The old inline `enable ALL` was enabling parvagues-sc at login, against
# its own on-demand policy — a login must not start SuperDirt.)
autostart-on) python3 "$DIR/tools/rig_units.py" --ensure --apply ;;
*) echo "usage: parvagues-gear {pause|resume|status|autostart-off|autostart-on}" >&2; exit 2 ;;
esac
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