Commit c625c25c by PLN (Algolia)

surface state: do not publish a fader nobody has reported, and never autofix a…

surface state: do not publish a fader nobody has reported, and never autofix a card someone switched off

Two holes the same shape -- a tool stating something it has not observed.

The driver seeds row D and A1-A4 to 0 because those are Ardour's faders and it
has no readback for them. Publishing that seed would have midiviz draw eight
faders resting at zero on a mix whose faders are up: the picture lying about
rest, in the row the layer was built to be honest about. state_write now applies
the same value_known discrimination paint_cell already used, so an unobserved
Ardour control is omitted and the cell stays a dot until Ardour echoes a
position or PLN moves it.

check-audio's 'pinned off' finding is now report-only. It is unreachable today
(the laptop codec offers nothing better than off), but the calm-evening
WirePlumber restart that brings its HiFi verb back also makes this branch fire --
and a --fix acting on it would switch the codec on while skipping the step that
confirms the DMIC guard matched a real node, reopening the capture path whose
probe hung the DSP in July. fixable=False is that procedure, in code.

Also: the morning list said Thursday. The first real press of the launcher icon
is not a gig-day experiment.
parent 9f8eab2e
...@@ -320,7 +320,7 @@ A zero with no marker holds the window, names the symptom and exits 2. Tests on ...@@ -320,7 +320,7 @@ A zero with no marker holds the window, names the symptom and exits 2. Tests on
both ends of that coupling, so rewording gig-up's last line fails loudly instead both ends of that coupling, so rewording gig-up's last line fails loudly instead
of silently holding every launch. of silently holding every launch.
### Thursday morning, in this order ### Wednesday morning (23rd), in this order — NOT gig day
1. **`systemctl --user start lcxl3-driver`** — the one thing left undone 1. **`systemctl --user start lcxl3-driver`** — the one thing left undone
tonight; the sandbox refused to start a unit. The board IS on the bus tonight; the sandbox refused to start a unit. The board IS on the bus
...@@ -331,6 +331,11 @@ of silently holding every launch. ...@@ -331,6 +331,11 @@ of silently holding every launch.
- look at C1/C2/C3 in midimon: the three DJF strips, detent at 64 - look at C1/C2/C3 in midimon: the three DJF strips, detent at 64
`gig-up.sh` already restarts this unit when the board is present, so a real `gig-up.sh` already restarts this unit when the board is present, so a real
launch covers it — but a real launch has not happened yet. launch covers it — but a real launch has not happened yet.
A row-D strip in midimon is **Ardour's** reported fader, not the board's: the
driver has no readback for those, so it publishes nothing for a fader nobody
has reported and the cell stays a dot until Ardour echoes one or you move it.
A jump on first touch means pot pickup was out of sync, not that the strip
was wrong.
2. **Press the launcher icon once** and watch what it does now. A window that 2. **Press the launcher icon once** and watch what it does now. A window that
holds is now information, not a failure. holds is now information, not a failure.
3. **`tools/check-audio.py`** — reads the box, changes nothing. Four warns 3. **`tools/check-audio.py`** — reads the box, changes nothing. Four warns
......
...@@ -1977,6 +1977,13 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0, ...@@ -1977,6 +1977,13 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
# publishes 127 until the next press. Nothing here has to remember # publishes 127 until the next press. Nothing here has to remember
# anything, which is why it cannot drift out of step with the board. # anything, which is why it cannot drift out of step with the board.
# #
# WHAT A ROW-D STRIP MEANS. Those faders are Ardour's, and the driver has
# no readback for them — so a strip in row D is Ardour's own reported
# position, arriving on the feedback port, and NO strip means nobody has
# told us yet. The driver omits an Ardour control it has never observed
# rather than publishing its seed of 0, so this layer can never draw
# eight faders resting at zero on a mix whose faders are up.
#
# Drawn in the cells the controls physically occupy (C1/C2/C3) rather # Drawn in the cells the controls physically occupy (C1/C2/C3) rather
# than in a strip of its own, for two reasons. The layout PLN just # than in a strip of its own, for two reasons. The layout PLN just
# signed off on does not move; and this module's founding idea is that # signed off on does not move; and this module's founding idea is that
......
...@@ -45,10 +45,20 @@ Driver = next(v for v in vars(DRV).values() ...@@ -45,10 +45,20 @@ Driver = next(v for v in vars(DRV).values()
if isinstance(v, type) and hasattr(v, "state_write")) if isinstance(v, type) and hasattr(v, "state_write"))
def _publisher(values: dict[int, int], track: str = "TEST"): def _publisher(values: dict[int, int], track: str = "TEST", seen=None):
"""The minimum a Driver needs to be to publish. Keeps the test honest about """The minimum a Driver needs to be to publish. Keeps the test honest about
what state_write actually reads, instead of constructing a whole rig.""" what state_write actually reads, instead of constructing a whole rig.
return types.SimpleNamespace(values=dict(values), track_name=track,
`seen` is the driver's `ardour_seen` — the Ardour-owned CCs whose position
has actually been reported to us. It defaults to "all of them", so a test
whose subject is the WIRE (the seam, the rate cap, the atomic replace) does
not quietly lose its fader to the observation rule; pass an explicit set
when the observation rule itself is what is under test.
"""
vals = dict(values)
return types.SimpleNamespace(
values=vals, track_name=track, grid=grid,
ardour_seen=set(vals) if seen is None else set(seen),
_state_seq=0, _state_sent=None, _state_at=0.0) _state_seq=0, _state_sent=None, _state_at=0.0)
...@@ -507,3 +517,28 @@ def test_the_pip_is_drawn_from_the_flag_the_rebind_tick_sets(tmp_path): ...@@ -507,3 +517,28 @@ def test_the_pip_is_drawn_from_the_flag_the_rebind_tick_sets(tmp_path):
w.repaint() w.repaint()
on = w.grab().toImage().pixelColor(x, y).name() on = w.grab().toImage().pixelColor(x, y).name()
assert off != on, "the untranslated-port pip drew nothing" assert off != on, "the untranslated-port pip drew nothing"
def test_the_publisher_omits_a_fader_it_has_never_observed(tmp_path, monkeypatch):
"""Row D belongs to ARDOUR, and the driver has no readback for it.
`self.values` seeds those CCs to 0, so publishing the raw dict would have a
viewer draw eight faders resting at zero on a mix whose faders are wherever
they are — a confident nought in the one row this layer exists to be honest
about. The driver already makes this distinction for its LEDs
(`value_known` in paint_cell); the publisher now makes the same one.
"""
fader, knob = grid.ROW_CCS["D"][0], grid.ROW_CCS["C"][0]
monkeypatch.setattr(DRV, "STATE_FILE", tmp_path / "surface-state.json")
pub = _publisher({fader: 0, knob: 64}, track="FB", seen=())
Driver.state_write(pub, now=1.0, force=True)
got = json.loads((tmp_path / "surface-state.json").read_text())["values"]
assert str(fader) not in got, "published a fader position nobody reported"
assert got[str(knob)] == 64, "dropped a value the driver does own"
# Once Ardour echoes it (or PLN moves it), it is an observation and ships.
pub.ardour_seen = {fader}
pub.values[fader] = 100
Driver.state_write(pub, now=2.0, force=True)
got = json.loads((tmp_path / "surface-state.json").read_text())["values"]
assert got[str(fader)] == 100, "an observed fader must be published"
...@@ -195,15 +195,29 @@ def findings() -> list[dict]: ...@@ -195,15 +195,29 @@ def findings() -> list[dict]:
"or something set the profile after boot.", "or something set the profile after boot.",
fix="pactl set-card-profile %s %s" % (name, pin), fixable=False)) fix="pactl set-card-profile %s %s" % (name, pin), fixable=False))
elif pin == "off" and want not in (None, "off"): elif pin == "off" and want not in (None, "off"):
# Deliberate-looking: somebody switched a working card off. Say what # REPORTED, NEVER ACTED ON, and `fixable=False` is what makes that
# it costs, never undo it silently — on this rig that pin is how the # true in code rather than only in this sentence.
# glitchy codec was kept out of the graph after it hung the stack. #
# Somebody switched a working card off, and on this rig that pin is
# how the laptop codec was kept out of the graph after its DSP hung
# WirePlumber in uninterruptible D-state and took scsynth down
# mid-session. Switching it back on is a change of RISK POSTURE, not
# a repair: it reopens the capture PCM that hung, and the guard
# written against that in July turns out never to have been in force
# (wrong config format for this WirePlumber — see the dead-config
# finding below). There is an ordered procedure for it, and step 4
# of that procedure is confirming the guard matched a real node.
# An --fix that skipped straight to step 5 would be the tool walking
# past its own documentation.
out.append(dict( out.append(dict(
level=WARN, what=f"{name}: pinned off", card=name, level=WARN, what=f"{name}: pinned off", card=name,
detail=f"a usable profile exists ({want!r}) but the card is " detail=f"a usable profile exists ({want!r}) but the card is "
"pinned off. If that was deliberate, leave it.", "pinned off. If that was deliberate, leave it — this "
fix="tools/check-audio.py --fix # clears the pin", fixable=True, "tool will not undo it.",
target=want)) fix=f"deliberate step, not an autofix: read "
f"docs/2026-09-22-audio-profile-stuck.md, then "
f"pactl set-card-profile {name} {want}",
fixable=False))
# 2. ALSA has roles this card's PipeWire profile list does not offer # 2. ALSA has roles this card's PipeWire profile list does not offer
verbs = ucm_verbs(c) verbs = ucm_verbs(c)
......
...@@ -1705,7 +1705,18 @@ class Driver: ...@@ -1705,7 +1705,18 @@ class Driver:
if not force and now - self._state_at < 0.1: if not force and now - self._state_at < 0.1:
return return
self._state_at = now self._state_at = now
snap = {str(k): int(v) for k, v in sorted(self.values.items())} # `value_known`, the same discrimination paint_cell() already makes.
#
# Row D and A1-A4 are ARDOUR'S faders: the driver has no readback for
# them, so `self.values` holds a seed of 0 until either Ardour echoes a
# position or PLN moves the control. Publishing that seed would have a
# viewer draw eight faders sitting at zero on a mix whose faders are
# wherever they are — the picture lying about rest, in the one row it
# was built to be honest about. So an unobserved Ardour control is
# OMITTED, and a consumer that finds no value for a cell correctly
# shows nothing rather than a confident nought.
snap = {str(k): int(v) for k, v in sorted(self.values.items())
if k not in self.grid.ARDOUR_CCS or k in self.ardour_seen}
if snap == self._state_sent and not force: if snap == self._state_sent and not force:
return return
self._state_seq += 1 self._state_seq += 1
......
...@@ -238,3 +238,31 @@ def test_the_gate_asks_it_without_the_fixing_flag(): ...@@ -238,3 +238,31 @@ def test_the_gate_asks_it_without_the_fixing_flag():
assert "--fix" not in probe.argv assert "--fix" not in probe.argv
assert probe.kind == cg.ADVISE assert probe.kind == cg.ADVISE
assert "--fix" in probe.fix, "the fix hint should name the repair" assert "--fix" in probe.fix, "the fix hint should name the repair"
def test_a_card_pinned_off_is_reported_but_never_acted_on(monkeypatch):
"""The finding that would otherwise fire the day the fix becomes possible.
Today the laptop codec offers only `off` and `pro-audio`, so `best_profile`
returns `off` and this branch is unreachable. After a calm-evening
WirePlumber restart brings its `HiFi` verb back, it becomes reachable — and
a `--fix` that acted on it would switch the card on WITHOUT the step that
confirms the DMIC guard matched a real node, reopening the capture PCM whose
probe hung the DSP in July. `fixable=False` is that guard, in code.
"""
card = _card(name="sof", active="off", profiles={
"off": {"priority": 0, "available": True, "sinks": 0},
"HiFi": {"priority": 100, "available": True, "sinks": 5},
"pro-audio": {"priority": 1, "available": True, "sinks": 5},
})
monkeypatch.setattr(CA, "cards", lambda: [card])
monkeypatch.setattr(CA, "pins", lambda: {"sof": "off"})
monkeypatch.setattr(CA, "ucm_verbs", lambda _c: [])
monkeypatch.setattr(CA, "dead_fragments", lambda _v: [])
monkeypatch.setattr(CA, "wp_version", lambda: (0, 4))
hits = [f for f in CA.findings() if f["what"].endswith("pinned off")]
assert len(hits) == 1, "a working card pinned off went unreported"
assert hits[0]["fixable"] is False, "--fix would switch the glitchy codec on"
assert "pactl set-card-profile" in hits[0]["fix"], "no manual path offered"
assert "audio-profile-stuck" in hits[0]["fix"], "the procedure is not cited"
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