fix(tray): say when the dongle has stopped answering - #4
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
VID_1532&PID_101Band every HID interfacePresent: True,Status: OK.Speakers (BlackShark V3 Pro PS - Game)and- Chat, bothOK.headsetctl listenumerated all four collections and picked the right control candidate (usage page0xff14).headsetctl param get 0x20→ no response. Same for0x21. Zero unrelated events.0x20is answered by the dongle out of its own state rather than proxied over the wirelesslink, 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.
connectedisOption<bool>,Nonemeans "notread yet", and the panel renders it as
SEARCHING— the same thing shown in the firstsecond 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::exchangereturned anunanswered request as
ProtocolMismatch, the error meaning the device replied and thereply 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.rsswallowed it at
debuglevel 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 byany successful exchange, and by the dongle going absent (absent is not silent).
NOT RESPONDING; the banner reads Dongle not responding. Unplug it andplug it back in. The banner outranks the Synapse warning — advice about something
overriding your settings is noise when nothing can read or write them.
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, andcargo test --workspace --target x86_64-pc-windows-gnuall clean.apply_device_snapshotcarrying the flag, and two layout tests — that a silent dongle never reads as
SEARCHING,and never as
CONNECTEDoff a stale reading.dongle-not-respondingfixture was added andchecked 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.
param get 0x20→01 01.🤖 Generated with Claude Code