Skip to content

Corsair Virtuoso XT/SE: add sidetone, lights and inactive time via the Bragi protocol - #570

Open
gdevenyi wants to merge 1 commit into
Sapd:masterfrom
gdevenyi:corsair-virtuoso-xt-bragi
Open

Corsair Virtuoso XT/SE: add sidetone, lights and inactive time via the Bragi protocol#570
gdevenyi wants to merge 1 commit into
Sapd:masterfrom
gdevenyi:corsair-virtuoso-xt-bragi

Conversation

@gdevenyi

@gdevenyi gdevenyi commented Sep 5, 2026

Copy link
Copy Markdown

Changes made

The Virtuoso XT/SE turns out to speak Corsair's Bragi property protocol — the same one corsair_void_v2w.hpp already uses — just framed on HID report 0x02 of the vendor collection (Usage-Page 0xff42) 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:

Capability How
CAP_SIDETONE property 0x46 (enable) + 0x47 (volume, 0–1000 in steps of 10)
CAP_INACTIVE_TIME property 0x0d (enable) + 0x0e (timeout in ms)
CAP_LIGHTS lighting handle 0x00, resource 0x01, via open/write/close

Packet framing[0] report ID 0x02, [1] target, [2] command (0x01 SET / 0x02 GET), [3] property, [5..] little-endian value. Replies come back on report 0x01 with the status in [3] (0x00 ok, 0x05 no such property, 0x09 write refused).

Software mode. 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 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 0 do 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 1 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 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 (target 0x08). 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 from BRAGI_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, while 0x0f tracked the level and flipped 0x10 from 2 to 1 the moment the cable went in. Properties 0x11 and 0x12 decoding to exactly 0x1b1c and 0x0a62 confirm the property numbering matches ckb-next's Bragi table.

Testing

Tested on a Virtuoso XT in both wireless (0x0a64) and wired (0x0a62) modes:

  • battery level and charging state, cross-checked against the charge cable
  • sidetone across its range and off — verified by reading 0x46/0x47 back
  • inactive time set, disabled and restored — verified by reading 0x0d/0x0e back
  • lights toggled both ways through the CLI and confirmed visually
  • hardware mode confirmed restored to 1 after each settings write
  • headset powered off / not paired reports offline rather than a bogus reading

The 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 and 0x08 target 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_LIGHTS is min_value 0, max_value 1). Exposing it would mean a new capability across CAPABILITIES_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 (property 0xa3) also looks feasible but is unverified.

Checklist

  • I adjusted the README (if needed)
  • For new features in HeadsetControl: I discussed it beforehand in Issues or Discussions and adhered to the wiki

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

Copilot AI lite review requested due to automatic review settings September 5, 2026 14:18
@gdevenyi
gdevenyi force-pushed the corsair-virtuoso-xt-bragi branch from 42c3e6a to 5542b39 Compare September 5, 2026 14:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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, and CAP_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.

Comment thread lib/devices/corsair_virtuoso_xt.hpp
Comment thread lib/devices/corsair_virtuoso_xt.hpp
@gdevenyi
gdevenyi force-pushed the corsair-virtuoso-xt-bragi branch 2 times, most recently from 5f23a5c to 01796de Compare September 5, 2026 14:27
@gdevenyi

gdevenyi commented Sep 5, 2026

Copy link
Copy Markdown
Author

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 -l 1 — until there's a shape agreed there.

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
@gdevenyi
gdevenyi force-pushed the corsair-virtuoso-xt-bragi branch from 01796de to 01ef3fd Compare September 5, 2026 14:48
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.

2 participants