---
log: 017
title: "The surface lights up again — in our colours, and it stays"
date: 2026-07-28
task: "#67 (+#49/#51/#11)"
tags: [tooling, rig, controller, opal2026]
shareable: true
---

## Cap (what & why)

The LaunchControl XL had gone dark. Not miscoloured — **dark**, with exactly one knob
(A1) green from a hand-sent test SysEx. PLN asked for it back three times, and the
ask decoded into three separate deliverables: *any* feedback at boot, **our** colour
coding rather than factory yellow, and colours that react to touch **and persist
after you let go**. J-7 to OPAL. A controller you can't read is a controller you
can't play.

## Manœuvre (how)

Built `tools/lcxl-leds.py` — python3 stdlib only, standalone, no editor required.

- `--map [TRACK.tidal]` paints by **role**, derived from the `^NN` bindings the file
  actually contains. Unbound controls go **dark** — that is the whole cognitive win:
  the board becomes a map of the file in front of you. With no track it paints the
  channel **convention**, so a boot can never end on a dark surface.
- `--watch` is the touch-reactive daemon. Persistence isn't a feature bolted on: the
  daemon owns a model of every control, and colour is a *pure function* of that
  model, so any repaint reproduces the same board. The LEDs are **write-only, there
  is no readback**, so owning the state is the only way.
- `--test` walks every index (a dead LED is only visible in motion), `--all`/`--off`,
  and `--dry-run` prints hex and sends nothing.
- Never sends a CC. Ever. SysEx out only; input is read-only via `aseqdump` as a
  child, which does not steal MIDI from SuperDirt.

Wired into `tools/tidal-remote.py` (now a 5-step boot: reboot → wait → seed → **paint**
→ eval) and into `gig-up.sh` (paints as soon as the rig owns MIDI, before Ardour and
Pulsar exist; `GIG_LEDS=watch` also starts the daemon).

## Prise (findings / artifacts)

- `tools/lcxl-leds.py`, ~700 lines, zero dependencies.
- Role parsing on three real tracks: `vague_de_crime` 15 controls (rhythm 9, fx 6),
  `gimme_acid` 21 (rhythm/bass/fx), `perfect` 25 (rhythm/bass/lead/fx).
- A 40-LED full repaint is **one** SysEx write of 89 bytes.
- `--watch` seam proven with a fake `aseqdump` on PATH: 9/9 synthetic events
  decoded → coloured → sent, plus respawn-with-backoff when the child exited.

## Sel (the shareable learning)

Three traps, each one a general lesson:

1. **The tool that works on a cold rig can fail on the live one.** `amidi -p hw:2,0,0`
   lit knob A1 by hand — but only because nothing held the raw device. With
   SuperCollider up, the ALSA *sequencer* layer owns the rawmidi substream and amidi
   dies with *"Device or resource busy"*. The fix was a different transport
   (`aseqsend` to the sequencer port), and the lesson is that a hardware test on an
   idle rig proves nothing about the rig you actually perform on.
2. **Stripping comments can change the grammar.** A blank line is Tidal's block
   separator, so splitting on blank lines to attribute `^NN` to a `dN` is right — but
   stripping full-line `--` comments *first* manufactures blank lines that cut a
   block in half. It didn't crash; it quietly dumped 9/9 bindings into the "fx"
   fallback. A parser miss masqueraded as a boring result.
3. **Verify the seam, not the function.** The first `--watch` regexes assumed
   space-separated numbers. `aseqdump` actually prints
   `Control change  %2d, controller %d, value %d`. The daemon would have run clean,
   logged nothing and painted nothing — the perfect silent failure. Pulling the
   format strings out of the binary beat guessing.

And one design note: the periodic re-assert runs on its **own thread**. Inside the
read loop it would only fire when an event arrived — i.e. never during the silences
when a device hiccup would actually go unnoticed.

## Hameçon (hook)

"My MIDI controller went dark a week before the gig. Fixing it taught me that the
test which proves your hardware works can be the exact test that lies to you."

## Sillage (what it unlocks)

A scriptable painter that works with Pulsar closed retires the HUD as the *only*
LED path (#57 can now be fixed calmly instead of urgently), and gives the per-track
visual language something to build on: value ramps, armed states, transition cues.
