Add libusb transport for Cloud III S Wireless#42
Add libusb transport for Cloud III S Wireless#42vxel wants to merge 5 commits intoLennardKittner:cloud_III_sfrom
Conversation
LennardKittner
left a comment
There was a problem hiding this comment.
Thanks for the PR. I like the new HidTransport abstraction.
| } | ||
| }; | ||
| let want_vid = VENDOR_IDS[0]; | ||
| let want_pid = PRODUCT_IDS[0]; |
There was a problem hiding this comment.
The code should not assume that there is only a single product ID. Many headsets have multiple.
| /// | ||
| /// Linux-only: the firmware quirk has only been observed on Linux's hidraw | ||
| /// stack, and the kernel-driver-detach pattern is meaningful only there. | ||
| #[cfg(target_os = "linux")] |
There was a problem hiding this comment.
This probably shouldn't be Linux-only, so it also works on Windows and macOS.
| if let Some(events) = test_device.wait_for_updates(Duration::from_secs(1)) { | ||
| for event in events { | ||
| debug_println!("got response {event:?}"); | ||
| for _event in events { |
There was a problem hiding this comment.
The "_" is intentional to avoid warnings when compiling in release mode.
| } | ||
| } | ||
|
|
||
| fn rusb_to_hid(err: rusb::Error) -> HidError { |
There was a problem hiding this comment.
Is this necessary? Why not add a new error type to DeviceError?
| .collect(); | ||
| // Custom-opened devices come first so the connection probe tries them | ||
| // before falling back to any hidapi candidate. | ||
| for state in custom_states.into_iter().rev() { |
There was a problem hiding this comment.
Why not append states to custom_states?
| if now >= deadline { | ||
| return Ok(0); | ||
| } | ||
| let (g, _) = cvar.wait_timeout(q, deadline - now).unwrap(); |
There was a problem hiding this comment.
Do I understand correctly that we are waiting here while still holding the lock?
If so, the reader thread will also get blocked and cannot listen for new events while the main thread waits.
|
@vxel I'm trying to get this to work but it's unfortunately not out of the box working. My junior assistant developer has reported
So, @vxel Which 'profile' are you using? IEC958? Analog? Smoke signal? I'll just keep it running and do some voodoo rituals at some point to make it co-operate perhaps. But right now the dongle is at its heightened spiritual tantrum state I think. SIKE It's connected now. But no responses from the headset. Notably, after running this branch, if I kill the hyper_headset process, I can no longer control the volume with the slider. There is a SIGINT exit trap missing to restore the thinggamabob. CLAUDE FINDS THE PROBLEM - again.Claude finds out for the fifth time in 30 minutes that the dongle is the problem● Actually it tells us something important: the PR's central premise looks shaky for your dongle.
PR #42's claim: "Linux hidraw drops RF-forwarded responses on this dongle." — that's the whole reason for the libusb rewrite.
But your observation contradicts it:
- Volume buttons (RF-forwarded HID reports) worked fine on kernel hidraw before libusb intervened. Always worked. Never broken.
- They only broke after libusb left intf 3 unbound.
So the kernel HID path isn't dropping arbitrary RF-forwarded reports. The PR author may have been wrong about why their queries weren't working — and "fixing" it with libusb worked for them but for an unknown
reason.
What's actually going on with your dongle:
- Dongle is alive: button events flow (one-way, headset→dongle→host) on both kernel HID and libusb paths
- Mute writes get acknowledged (mic actually mutes)
- But query responses never come — same on libusb as on hidraw
That points away from the OS layer and toward something else:
- Dongle→headset RF path may be wedged: queries reach the dongle but never get RF-forwarded to the headset (so no response is generated). Button events work because they're one-way headset→dongle.
- Or the headset's BT/RF state is wedged: it pairs enough to relay buttons but won't reply to query packets.
Either way, it's not Linux-side. Switching transports (hidraw ↔ libusb) won't change anything for query reads on your dongle. Same story we've been seeing all session — firmware-state, host can't fix it.
Want me to still patch the kernel-driver-rebind on exit? It's a real bug in the PR, but unrelated to your query problem.
✻ Cooked for 1m 35s
❯ yes . but for the record, you're saying that the dongle is bugged, right?
● Yes — that's the most consistent explanation with what we've observed:
- Both transports (hidraw and libusb) deliver every byte the dongle sends
- The dongle sends one-way data fine (button events on both paths)
- It accepts and acts on writes (mute LED toggles)
- It just doesn't reply to query packets
Nothing in the host stack can manufacture a reply that the dongle doesn't send. Yes, dongle (or headset) firmware state is the most likely culprit. Caveat: we haven't ruled out a USB capture / Wireshark-side
investigation that could prove writes actually leave the host and reach the dongle — but every other signal we have points firmware-side.
Now patching the kernel-rebind bug. Oh man Claude, for god's sake, I've told you a hundred times the dongle is bugged!1 Footnotes |
but, did we learn something new? Or is it the dongle, after all?❯ so, in conclusion tho. you've been ambivalent on the possible causes, but, did we learn something new?
❯
❯ not new:
❯ - the firmware is the problem
❯ - the dongle and the headset are in a divorce
❯ - ...etc
● Yes, a few genuinely new things:
1. The PR's central premise doesn't hold for your dongle.
PR #42 says hidraw drops RF-forwarded responses, justifying the libusb rewrite. But on your hardware:
- Volume buttons (RF-forwarded HID reports) work fine via kernel hidraw
- Even via libusb, query responses still don't come
So hidraw isn't the problem — at least not the whole problem. The query-deafness happens at a layer libusb doesn't reach. The PR's libusb transport is solving for a different failure mode than yours.
2. The "wedge" is selective, not total.
Before this session it was vaguely "writes work, reads don't." We now have a sharper picture: [^note1] - Host→dongle writes: ack and act (mute LED toggles)
- Headset→dongle one-way RF: works (button events flow on both transports)
- Host→dongle→headset→dongle→host round-trip: silently fails (queries)
That points at the dongle→headset direction or the response-forwarding step specifically. Not generic firmware death.
3. PR #42 mute protocol vs yours is genuinely different bytes — 0x0c-cmd-0x01 (64-byte) vs your 0x05 (20-byte). Both work for muting. Useful to know if your branch ever wants to converge with PR #42's protocol.
4. Two latent bugs in PR #42, now fixed in your PR:
- Connect couldn't survive query-deafness (gates on a probe response that never comes)
- Ctrl+C left the dongle's HID interface unbound, breaking volume keys system-wide
5. Practical takeaway: even when query reads fail, the tray/CLI is now usable for writes-only (mute, EQ presets, auto-shutdown). That's a usability floor we didn't have before.[^note2] What we didn't learn: an actual fix for the query-deafness. Still a firmware/RF-state problem on the device side.
✻ Churned for 25sFootnotes
|

Cloud III S Wireless: libusb transport, generic refactor, mute fix
Problem
Restores reliable headset state queries (battery, charge, side tone, voice prompt, color, auto-shutdown, mute) on Cloud III S Wireless under Linux, and lifts the libusb-based workaround into a clean transport abstraction so other devices can opt into it without polluting
mod.rs.Disclaimer : made with the help of AI (Claude Code)
Diagnosis
/dev/hidrawNhas been opened, query responses that travel over RF (battery, charge, side tone, …) silently never reach user space. NGenuity uses raw USB on Windows; I replicate that on Linux via libusb, hence completely bypassing hidraw.write_hid_reportoverride sent SET_REPORT type Feature, which the firmware ignores on Report ID 0x0c. NGenuity always uses type Output. The override is removed; the defaulthid_device.write()produces the correct request.05 02/05 00) which did not seem to work. Replaced with the 0x0c-protocol cmd0x01(0c 02 03 00 00 01 <0|1>), confirmed by USB capture of NGenuity.Changes
HidTransport { Hidapi(...), Libusb(...) }andLibusbTransportinmod.rs. The libusb variant detaches the kernel HID driver, claims the interface, and drives rawSET_REPORTcontrol transfers + INT IN reads through a background reader thread.DeviceEntrygained acustom_open: Option<CustomOpener>hook so devices can register a non-hidapi opener. The hidapi enumeration generically skips VID/PID pairs owned by a custom opener.cloud_iii_s_wireless.rs, gated#[cfg(target_os = "linux")].0x01. The notification path (0d 02 03 00 03 <val>) was already handled.Notes