corsair: add VIRTUOSO SE LED control via V2W protocol - #568
Conversation
f8918be to
f205083
Compare
|
Fixed clang-format violations flagged by CI. The formatting issues were:
Also caught a few pre-existing formatting inconsistencies in the file (brace initialization style in other methods) that clang-format standardized. |
|
I corrected the formatting issue that failed the action. |
f205083 to
3be3432
Compare
|
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 ( |
|
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? |
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.
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.
3be3432 to
4704a40
Compare
|
Thanks for the review. Here is what was addressed and what we found: Changes in this update
About 0x0a3d (wired SE) and V2WWe 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. LimitationsThe 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. |
|
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. |
|
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. |
|
just a heads up but https://github.com/jurkovic-nikola/OpenLinkHub/ can control the RGB on it (is how I do it). I can confirm it does get stuck at 59% as well, I saw the percentage go down from a known full charge to 59% and thought it was just corsair's weird battery reporting after a few hours of usage, I can keep digging on it when I get some free time next week to spin my windows VM back up. |
|
The issue here is that the headset wired uses a completely different protocol
This is also how I control mine at the moment also. It holds and refreshes the connection to the control endpoint every 20 seconds. My PR implements the ability to change this stuff correctly, but its up to Sapd if he wants to implement the keep alive which is out of scope currently and adds complexity to this project. It might not be worth it unless other headsets have this same problem and need a daemon also. This is a nice headset but it isn't new and is still a pretty expensive option that isn't exactly wide spread. |
|
Turns out both PRs are the same protocol. #570 calls it "Bragi" and yours is the V2W class, but the bytes are identical - same framing, same command IDs (SET 0x01, GET 0x02, CLOSE 0x05, WRITE 0x06, OPEN 0x0d), and your So I'm taking #570 and closing this - it covers both SE product IDs in one class instead of splitting the same headset across two with different names and different capabilities depending on cable vs dongle. Your 59% report is what made me look at the battery properly. The request we ship is: Report ID and target byte, then nothing - no command, no property. It was never a battery query. And the byte we report as a percentage is 0x3b = 59, which is why you and the reporter in #567 both got exactly 59% on different headsets. Constant in an echo frame. That's in 4.1.0, so every Virtuoso user is seeing a made-up number. #570 replaces it with a GET of property 0x0f plus charge state from 0x10 - what the V2W class already does correctly. The other thing worth keeping from here is that the SE receiver takes lighting on endpoint 0x08. #570 probes for its target and will probably pick 0x09 on the SE, which nobody has tested. If it doesn't work, 0x08 is the first thing to try. Can you test #570 on your SE?You have the only SE here - gdevenyi has an XT wireless and wired, no SE. So #570's SE support is untested and I don't want to advertise capabilities nobody has tried. On the receiver (0x0a3e) and on the cable (0x0a3d):
Then the one I asked about in August, which I don't think either of us has timed: run gdevenyi measured on the XT that it drops back to hardware mode on its own after ~180s of silence, and the last frame survives that - lights stay off. Opposite of what you saw. My guess is OpenLinkHub sends an explicit hardware-mode command when it shuts down, and he found that an explicit switch is what makes the LEDs repaint from the on-board effect (he shipped that briefly and If the lights don't hold on the SE, I'd ship #570 with battery and sidetone for the SE and leave CAP_LIGHTS on the XT only. |



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 command0x0d, RGB via command0x06) that was documented through reverse engineering efforts by gravityfargo and the VirtuosoControl project.Changes
lib/devices/corsair_void_v2w.hpp:0x0a3e(VIRTUOSO SE wireless receiver) toSUPPORTED_PRODUCT_IDSCAP_LIGHTSto capabilitiessetLights()method that initializes the LED endpoint then sets all zones to white (on) or zeroed (off)initLEDs()andsetZoneRGB()private helpers for V2W LED commandsTesting
Verified on a real Corsair VIRTUOSO SE (PID
0x1b1c:0x0a3e):headsetcontrol -l 1/-l 0