Corsair Virtuoso XT/SE: add sidetone, lights and inactive time via the Bragi protocol - #570
Corsair Virtuoso XT/SE: add sidetone, lights and inactive time via the Bragi protocol#570gdevenyi wants to merge 1 commit into
Conversation
42c3e6a to
5542b39
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The updated device header introduces at least one build-breaking include issue and a behavior in setLights(false) that can leave the device stuck in software mode with undesirable side effects.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR reworks the Corsair Virtuoso XT/SE device implementation to use Corsair’s Bragi property protocol on the vendor HID report, expanding support beyond battery reporting to additional device capabilities.
Changes:
- Reimplemented Virtuoso XT/SE communication using Bragi property GET/SET framing and reply filtering.
- Added support for
CAP_SIDETONE,CAP_INACTIVE_TIME, andCAP_LIGHTS, including software-mode bracketing for persistent settings writes. - Updated README capability matrix and Corsair sidetone note to reflect XT/SE behavior.
File summaries
| File | Description |
|---|---|
| README.md | Updates device capability table and Corsair sidetone guidance for Virtuoso XT/SE. |
| lib/devices/corsair_virtuoso_xt.hpp | Implements Bragi protocol framing, target resolution, and adds sidetone/lights/inactive-time capabilities (plus battery read rework). |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
5f23a5c to
01796de
Compare
|
Opened #572 in Ideas to discuss RGB colour support, since the Virtuoso XT's lighting hardware does full RGB (I cycled the zones red/green/blue while reverse-engineering it) but no capability in the project can express a colour. This PR stays as-is — static white for |
The Virtuoso XT/SE speaks Corsair's Bragi protocol, the same one corsair_void_v2w.hpp uses, but framed on HID report 0x02 of the vendor collection instead of an unnumbered report. Reworking the device around that protocol replaces the single undocumented battery command with the documented property table and adds three capabilities: * CAP_SIDETONE - property 0x46 (enable) and 0x47 (volume, 0-1000) * CAP_INACTIVE_TIME - property 0x0d (enable) and 0x0e (timeout in ms) * CAP_LIGHTS - lighting handle 0x00, resource 0x01 Battery now reads BRAGI_BATTERY_LEVEL (0x0f, tenths of a percent) and reports charging from BRAGI_BATTERY_STATUS (0x10). The previous command returned a byte that stayed frozen at one value across a full charge cycle, while 0x0f tracked the level and matched the charge state; properties 0x11 and 0x12 decoding to the exact vendor and product IDs confirm the property numbering. Writes are refused with status 0x09 unless the headset is switched into software mode (property 0x03 = 2). Settings written that way persist, so those writes are bracketed by a scope guard that hands the headset straight back to hardware mode rather than parking it in software mode. Lighting is the exception and deliberately stays in software mode. It is a block of data pushed through the open/write/close handle sequence, and hardware mode drives the LEDs from the effect the headset runs itself, which paints straight over the frame we wrote - restoring it makes "lights off" do nothing at all. The headset drops back to hardware mode by itself within a few minutes of the host going quiet, and the frame it was last given survives that. Because the capability is only on/off, "on" paints every zone static white rather than restoring the effect that was running before; the protocol offers no way to read that effect back and replay it. A wireless receiver relays commands to the headset paired with it (target 0x09) while a wired headset answers for itself (target 0x08). The registry shares one instance across product IDs and only records the ID it last matched on, so the target is confirmed against the device rather than trusted - otherwise a receiver and a wired headset plugged in together break each other. Tested on a Virtuoso XT in both wireless (0x0a64) and wired (0x0a62) modes: battery level and charging state, sidetone across its range and off, inactive time set/disabled/restored - each verified by reading the property back - and lights toggled both ways through the CLI and confirmed visually. The SE product IDs are untested and ride along on the same framing. Note that iCUE's equalizer and spatial audio are host-side DSP applied to the PC audio stream, not device settings, so they are out of scope here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BHCVMmAAeV5WGP7oYZsk3T
01796de to
01ef3fd
Compare
Changes made
The Virtuoso XT/SE turns out to speak Corsair's Bragi property protocol — the same one
corsair_void_v2w.hppalready uses — just framed on HID report0x02of the vendor collection (Usage-Page0xff42) instead of an unnumbered report. Reworking the device around that protocol replaces the single undocumented battery command with the documented property table and adds three capabilities:CAP_SIDETONE0x46(enable) +0x47(volume, 0–1000 in steps of 10)CAP_INACTIVE_TIME0x0d(enable) +0x0e(timeout in ms)CAP_LIGHTS0x00, resource0x01, via open/write/closePacket framing —
[0]report ID0x02,[1]target,[2]command (0x01SET /0x02GET),[3]property,[5..]little-endian value. Replies come back on report0x01with the status in[3](0x00ok,0x05no such property,0x09write refused).Software mode. Writes are refused with status
0x09unless the headset is switched into software mode (property0x03= 2). Settings written that way persist, so those writes are bracketed by an RAII guard that restores hardware mode and leaves the on-board buttons working.Lighting is the exception and deliberately stays in software mode. Hardware mode drives the LEDs from the effect the headset runs itself, which paints straight over the frame we wrote — restoring it makes
-l 0do nothing at all (I shipped that bug briefly and caught it on hardware). The headset returns to hardware mode by itself once the host goes quiet, and the frame it was last given survives that. Since the capability is only on/off,-l 1paints every zone static white rather than restoring the effect that was running before — the protocol offers no way to read that effect back and replay it. A power cycle restores the headset's own lighting.Wireless vs wired. A receiver relays commands to the headset paired with it (target
0x09); a wired headset answers for itself (target0x08). Asking the wrong one is silent rather than an error. The registry shares one instance across product IDs and only records the ID it last matched on, so the product ID is used as a hint and then confirmed against the device — without that, a receiver and a wired headset plugged in at the same time break each other. (Worth noting: plugging the USB-C cable into a Virtuoso XT doesn't just charge it, the headset re-enumerates as the wired product ID.)Battery is now read differently, which is the one behaviour change to scrutinise. It reads
BRAGI_BATTERY_LEVEL(0x0f, tenths of a percent) and reports charging fromBRAGI_BATTERY_STATUS(0x10). The previous 2-byte command returned a byte that sat frozen at the same value for an hour and across a full charge cycle, while0x0ftracked the level and flipped0x10from 2 to 1 the moment the cable went in. Properties0x11and0x12decoding to exactly0x1b1cand0x0a62confirm the property numbering matches ckb-next's Bragi table.Testing
Tested on a Virtuoso XT in both wireless (
0x0a64) and wired (0x0a62) modes:0x46/0x47back0x0d/0x0ebackThe SE product IDs (
0x0a3e,0x0a3d) are untested — I don't have the hardware. They ride along on the same framing, which matches a published SE capture showing the same interface, report IDs and0x08target byte.Out of scope
iCUE's equalizer and spatial audio are host-side DSP applied to the PC audio stream rather than device settings, so they aren't reachable from a HID tool.
RGB colour works on this hardware — I cycled the zones red/green/blue while reverse-engineering it — but HeadsetControl has no capability that can express a colour (
CAP_LIGHTSismin_value 0, max_value 1). Exposing it would mean a new capability acrossCAPABILITIES_XLIST, the descriptor table, the handler registry and both public APIs, which is a project-wide feature rather than a device change. Happy to open a Discussion if there's appetite for it. Mic volume (property0xa3) also looks feasible but is unverified.Checklist
These are existing capabilities on a device that already had an entry, so there's no new feature to discuss — happy to open a Discussion first if you'd prefer.
🤖 Generated with Claude Code
https://claude.ai/code/session_01BHCVMmAAeV5WGP7oYZsk3T