---
log: 035
title: "There were never two screens. There were thirty-four."
date: 2026-08-29
task: "n/a — LCXL3 OLED display-ownership battle"
tags: [tooling, lcxl3, surface, protocol, oled]
shareable: true
---

## Cap (what & why)

The new Launch Control XL 3's OLED kept showing the firmware's bare value —
`0` for a relative encoder, a raw 0-127 for a fader — instead of the driver's own
overlay naming the control in corpus language. PLN, after three rounds of fixes:
"still shows values." On the faders, "no info at all." A 128x64 panel that says
`0` mid-set is worse than a dark one; it looks like a working display lying to you.

## Manœuvre (how)

Three rounds had all been variations on one move: re-summon our overlay on the
global temporary target (36h) faster than the firmware's timeout could drop it.
Round 3 even found a real self-defeating bug — the keep-alive stamped its clock
on every push, so "stale" never became true — fixed it, and lost anyway.

Fourth round, we read the display chapter of the programmer's reference instead
of tuning the loop. First list in the chapter:

> • 05h (5) - 24h (36): Temporary display for Analogue controls (same as CC
>   indices, 05h (5) - 0Ch (12): Faders, 0Dh (13) - 24h (36): Encoders)
> • 35h (53): Permanent/Stationary display
> • 36h (54): Overlay/Temporary display

Thirty-four targets, not two. Every fader and every encoder owns a display of
its own, addressed by its own CC index — and that is the one the firmware
auto-triggers on touch, per config bit 6, "default: Set". A fader sweep fires
target 05h thirty times a second; we were re-summoning 36h once a second.
Unwinnable by construction.

The tell had been sitting there the whole time: buttons were fine. Buttons live
at 25h-34h, outside the analogue range, own no per-control target, and were never
contested at all. A symptom that only some controls have is a symptom about
*which* controls, not about timing.

So: stop fighting, start labelling. Arrangement 4 — the firmware's default — is
"Parameter Name and Numeric Parameter Value", one host field: Name. Empty,
because nobody had written it. Write it and the display we were suppressing
becomes the one we wanted, tracking the value at wire speed and timing its own
dismissal for free.

## Prise (findings / artifacts)

- `tools/lcxl3.py`: per-control target constants, the config bit-field
  (`CFG_AUTO_CHANGE` = bit 6 is the native display's on-switch),
  `Surface.label_control()` / `native_overlay()`, and `lcxl3.py label|native` for
  single-control eye tests.
- `tools/lcxl3-driver.py`: labels all 32 analogue controls on their own targets at
  startup, re-asserts every 20 s, stands down from the overlay for them.
  `--show-oled` dry-runs the label table offline, opening no port.
- The 3 DJF knobs keep our overlay — "HPF 3.4kHz !!" is prose and arrangement 4's
  value is a number — by clearing bits 5+6 on exactly those three targets.
  Restored on exit.
- 32 labels pushed to the live device from a second process while the driver kept
  translating, uninterrupted (PID up 47 min across the push).
- Three bugs the labels exposed by being the first thing to render at full width:
  `set_text` had been silently eating the four reassigned control codes its own
  docstring advertised (the heart frames arrived as spaces); `parse_context` said
  "a trailing comment IS the best label" and stripped `--` only at the *start* of
  the body, where a trailing comment never is; and with no comment it fell back to
  raw combinator soup. Across `live/`: **158 syntax-noisy labels out of 22368
  became 3**. C8 of piment_bresilien went from `d8 # n "23")) --` to PLN's own
  `d8 Raise COMEON!`.

## Sel (the shareable learning)

Three rounds of increasingly clever fixes to a race we could not win, because we
had never asked *which display* the firmware was drawing on. The keep-alive got
better each round and the screen never did — and a fix that improves the
mechanism without moving the symptom is evidence you are fixing the wrong
mechanism.

The partial symptom was the free clue we ignored. "The values win" was treated as
a fact about timing; "the values win *on knobs and faders but not buttons*" is a
fact about addressing, and it was there from the first report.

And the resolution wasn't to defeat the firmware's display. It was to notice the
firmware was offering us a field we had left blank. The best fix took work *away*
from the driver: no keep-alive, no diffing, no 0.9 s re-summon — the hardware
already tracked the value and timed the dismissal, it just didn't know the
control's name.

## Hameçon (hook)

"I spent an evening losing a fight for a screen. Then I read the manual and found
out there were thirty-four of them, and the one I wanted had an empty name field."
