Skip to content

fix(tray): say when the dongle has stopped answering - #4

Merged
cunningorb merged 1 commit into
mainfrom
fix/surface-unresponsive-dongle
Sep 18, 2026
Merged

cunningorb merged 1 commit into
mainfrom
fix/surface-unresponsive-dongle

Conversation

@cunningorb

Copy link
Copy Markdown
Owner

What prompted this

The tray reported that it could not find the headset and was not auto-switching, with the
dongle plugged directly into the laptop and the headset on. Diagnosis on the live machine:

  • USB enumeration entirely healthy — VID_1532&PID_101B and every HID interface Present: True, Status: OK.
  • Both audio endpoints live: Speakers (BlackShark V3 Pro PS - Game) and - Chat, both OK.
  • headsetctl list enumerated all four collections and picked the right control candidate (usage page 0xff14).
  • headsetctl param get 0x20 → no response. Same for 0x21. Zero unrelated events.
  • Exactly one tray instance running, no Synapse contending.

0x20 is answered by the dongle out of its own state rather than proxied over the wireless
link, and returns a value even with the headset powered off. Silence there is the dongle
having gone quiet, not the headset being away. The remedy is a replug.

The defect

The tray could not say any of that. connected is Option<bool>, None means "not
read yet", and the panel renders it as SEARCHING — the same thing shown in the first
second after launch. A wedged dongle and a cold start looked identical, indefinitely, and
nothing on screen or in the tooltip named the remedy.

The evidence was discarded one layer down. ControlSession::exchange returned an
unanswered request as ProtocolMismatch, the error meaning the device replied and the
reply was wrong
. Two conditions with different causes and different remedies shared one
variant, distinguishable only by matching on the text of a message — so worker.rs
swallowed it at debug level and carried on.

It also spun. A failed refresh never reset the refresh timer, so the full read sequence
re-ran roughly every two seconds, each attempt blocking for a complete exchange timeout,
for as long as the condition lasted.

The change

  • DeviceError::NoResponse { param, waited, events_seen } — silence is now its own error.
    Rendered wording is byte-for-byte unchanged, so CLI output does not move.
  • HeadsetState::dongle_silent, set after three consecutive silent refreshes. Cleared by
    any successful exchange, and by the dongle going absent (absent is not silent).
  • Header reads NOT RESPONDING; the banner reads Dongle not responding. Unplug it and
    plug it back in.
    The banner outranks the Synapse warning — advice about something
    overriding your settings is noise when nothing can read or write them.
  • Retries back off: 2s after an ordinary failure, 15s once judged unresponsive.

What this does not do

It does not recover a wedged dongle. Nothing in user space can; only a replug does. It
stops the tray from hiding which of the two things went wrong, which is what made this cost
a manual CLI session every time.

Verification

  • cargo fmt --all --check, cargo clippy --workspace --all-targets --target x86_64-pc-windows-gnu -- -D warnings, and cargo test --workspace --target x86_64-pc-windows-gnu all clean.
  • New tests: the error variant (not just its message), the tooltip, apply_device_snapshot
    carrying the flag, and two layout tests — that a silent dongle never reads as SEARCHING,
    and never as CONNECTED off a stale reading.
  • Rendered all 18 panel fixtures. A new dongle-not-responding fixture was added and
    checked visually; the first attempt overflowed the status caption into the device name,
    which is why the remedy moved to the banner. The Synapse banner was re-checked for regression.
  • The healthy path was re-verified against the real dongle once it was replugged:
    param get 0x20 → 01 01.

🤖 Generated with Claude Code

The dongle can wedge: it stays enumerated, every interface reports
healthy, the control collection still resolves and scores correctly, and
it answers nothing. The tray had no way to describe that. `connected` was
`Option<bool>` where `None` meant only "not read yet", which the panel
drew as SEARCHING -- the same thing it shows in the first second after
launch -- so a wedged dongle and a cold start were indistinguishable, and
nothing on screen named the one thing that clears it.

The evidence was discarded a layer down. An unanswered request came back
as `ProtocolMismatch`, the error meaning the device replied and the reply
was wrong, so recognising silence meant matching on the text of a
message. Silence is now `DeviceError::NoResponse`, carrying the
parameter, the wait, and the unrelated events seen. The rendered wording
is unchanged.

That distinction is what makes the report trustworthy: the first read of
a refresh is `0x20`, which the dongle answers out of its own state rather
than proxying over the wireless link, and which answers even with the
headset powered off. Silence there is the dongle itself having gone
quiet, not the headset being away.

Three consecutive silent refreshes now set `dongle_silent`, the header
reads NOT RESPONDING, and the banner says to unplug it and plug it back
in. The banner outranks the Synapse warning, which is noise when nothing
can read or write the settings at all.

Also stops the worker hammering a device that will not answer. A failed
refresh left the refresh timer untouched, so the whole read sequence
re-ran every couple of seconds, each attempt blocking for a full exchange
timeout, indefinitely, reported only at a log level nothing listens to.
Retries back off, dropping to one attempt every fifteen seconds once the
dongle is judged unresponsive.

This does not recover a wedged dongle -- only a replug does. It stops the
tray from hiding which of the two things went wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cunningorb
cunningorb merged commit f42c254 into main Sep 18, 2026
1 check passed
@cunningorb
cunningorb deleted the fix/surface-unresponsive-dongle branch September 18, 2026 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant