# Reverse Engineering the USB Driver for the  Corsair Virtuoso SE Headset

**12/17/22:** My goal is to have the ability to control the LEDs of the headset via the program [ckb-next](https://github.com/ckb-next/ckb-next) and sidetone with [HeadsetControl](https://github.com/Sapd/HeadsetControl)

This device uses the Slipstream Wireless Dongle, and I won't be. I'm figuring this out as I go, and adding a wireless dongle (that neither program has worked on yet) will add a layer of complexity to something I already don't know how to do. For now, USB only.

**1/2/23**: As you can see in the "USB Description Overview w/ lsusb" section, I've managed to map out my best estimation of what each nibble does. I am working solely on ckb-next integration for LED control at this stage. The unfortunate issue I face is that I was hoping I could build off of the bragi protocol already implemented in ckb-next, but it looks like it isn't compatible.

**1/3/23**: When I first started out, I focused on the packets sent to control functionality. With the new info that it isn't bragi compatible, I have to decipher the initiation packets sent when iCUE first recognizes the headset. Yay me.

# References:

[USB.org - HID 1.1 Specsheet](https://www.usb.org/document-library/device-class-definition-hid-111)

How to setup Wireshark for the purpose of USB packet capture on [liquidctl wiki](https://github.com/liquidctl/liquidctl/blob/main/docs/developer/capturing-usb-traffic.md)

[beyondlogic: USB in a NutShell](https://www.beyondlogic.org/usbnutshell/usb1.shtml)

[liquidctl Wiki Techniques for analyzing USB protocols](https://github.com/liquidctl/liquidctl/blob/main/docs/developer/techniques-for-analyzing-usb-protocols.md)

[A Reddit post](https://www.reddit.com/r/MechanicalKeyboards/comments/2ij2um/corsair_k70_rgb_usb_protocol_reverse_engineering/) outlining the reverse engineering of a Corsair Keyboard

[ckb-next wiki](https://github.com/ckb-next/ckb-next/wiki/Corsair-Protocol#07-05---lighting-control) on the Corsair Protocol

[liquidctl wiki](https://github.com/liquidctl/liquidctl/blob/main/docs/developer/protocol/commander_core.md) on the Corsair Commander Core

### FFR

`Device ID: 0x0a3d`

dev\_address determined from the "GET DESCRIPTOR Response DEVICE" Packet, which is like the fourth one sent in in Wireshark, depending on how many USB devices are passed to the VM. For me, #1 was the VirtualBox mouse driver.

Wireshark Filter:

`((((usb.device_address == 2)) && (usb.data_len == 64)) ) && (usb.irp_info.direction == 0x0)`

Bash output in OSS-Code looks pretty when put in a JSON file.

# USB Description Overview w/ lsusb

`lsusb -v -d 1b1c:0a3d` *verbose output for the specific device*

```markdown
Bus 001 Device 008: ID 1b1c:0a3d Corsair CORSAIR VIRTUOSO SE USB
```

**1b1c**:0a3d = Vendor ID, unique to manufacturer (usually)

1b1c:**0a3d** = Product ID, unique to device

Ref: USB HID Appendix

### Device Descriptor

USB device required by OS to explain descriptor. Not all fields are required.

`idVendor 0x1b1c Corsair`

`iProduct 2 CORSAIR VIRTUOSO SE USB`

### Configuration Descriptor

`MaxPower 150mA`

### Interface Descriptor

`bInterfaceProtocol 0` "none," not a keyboard or mouse

The headset has 4 interfaces starting at 0. Interface 3 is the HID device to control the LEDs and other settings.

### Endpoint Descriptor

The host computer initiates everything. Interrupts in USB are seen by the host at a certain poll rate and know, based on this descriptor, what that interval is, and what packet size is expected. In this case, 64 bytes.

`bEndpointAddress 0x81 EP 1 IN` Endpoint 1 **IN**, Most significant bit sent 8

*The host is telling the device to send it data.*

`bEndpointAddress 0x01 EP 1 OUT` Endpoint 1 **OUT**, Most significant bit sent 0

*The host is giving the device stuff to do.*

# Frame Analysis: Initialization

## Raw Frame

```markdown
0000   [1b 00] [e0 a6 c5 1d 82 91 ff ff] [00 00 00 00] [09 00]
0010   [00] [01 00] [02 00] [01] [01] [40 00 00 00] {[02] [08] [(02 11 00
0020   00 00 00 00 00 00 00 00 00 00 00) 00 00 00 00 00
0030   00 00 00 00 00 00 00 00 00 00 00] [00 00 00 00 00
0040   00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0050   00 00 00 00 00 00 00 00 00 00 00]}
```

91 bytes on the wire, this specific frame is the first interrupt\_out sent to the headset.

I broke the bytes up in square brackets by function, according to Wireshark. Curly brackets indicate the HID data. I'll break down each square bracket by field name indicated in Wireshark.

1. usbpcap\_header\_len
    
2. irp\_id
    
3. usbd\_status
    
4. function
    
5. irp\_info
    
6. bus id
    
7. device address
    
8. endpoint
    
9. transfer\_type
    
10. packet data length
    
11. report id
    
12. vendor data
    
13. padding (only first 15 bytes have been observed to be used)
    
14. not specified
    

Only items 11, 12, and 13 are relevant HID data.

## Initialization Frames

There are 33 of these.... Hopefully they'll help decipher the Interrupt\_out mappings.

# HID Data Decoding

## INTERRUPT\_OUT Mapping

```markdown
Pkt  Purpose            Out    IN
[0]  Report ID          02     01
[1]  Vendor Data        08     00
[2]  Commands                  Echo
[3]  Feature Select
[4]  Data (1)
[5]  Data (2)
[6]  Unknown (2)
[7]  Unknown (3)
[8]  Red
[9]  Mic (1)? 
[10] Mic (2)
[11] Green 
[12] Charge LED
[13] Mic (3)
[14] Blue 
```

### \[2\] Commands

Possibly indicates what feature of the headset is being modified

```markdown
*** Seen in Initialization ***
01
02
0d Sleep Enabled Status
09
08
05

*** Seen while expirementing with settings ***
06 RGB
0e Sleep wait time

*** Seen Ocasionally with no user input ***
12 
```

### \[3\] Feature Select

Indicates the feature being written to/read from

```markdown
00 LED
02 Brightness
0d Sleep toggle 
0e Sleep time
```

### \[4\] \[5\] Data Bytes

I'm pretty sure these two bytes are the values passed by whatever setting is altered. I have only seen them change when changing the sleep time and brightness.

**Case of the Battery:** I started a capture when I knew the headset wasn't fully charged. The reply packets indicate that Data (2) was the only byte that changed slowly over a few minutes. "XX 03" seems to indicate that "XX" (data 1) is the battery level, and "03" (data 2) maybe indicates that data 1 is outputting battery info. *TODO: VERIFY SETTING INDICATOR DURING CHARGE*

```markdown
09 00     Mic LED color
XX 03     Battery Level
```

`XX` = byte changes

### \[9\]\[10\]\[13\]Microphone

```bash
Mic (2)    ff
Mic (3)    00
Microphone is Muted and "Disabled LED When Mic is Active" does nothing.
Mic LED is RED

Mic (2)    00
Mic (3)    ff
Microphone is not Muted and "Disabled LED When Mic is Active" is disabled.
Mic LED is Green

Mic (2)    00
Mic (3)    00
Microphone is not Muted and "Disabled LED When Mic is Active" is enabled.
Mic LED is off

Mic (2)    ff
Mic (3)    ff
unknown
Mic LED is yellow


I think Mic (1) indicates that the Green RGB contol byte is being used to control the charge LED rather than the main logo LEDs. "e1"
```

Data (1) is `09`. Also, the button on the microphone controls the mute/unmute bytes.

Microphone LED is also full RGB which can be observed when switching the headset to wireless mode and may be independently controllable, but iCUE won't let users change this.

## Features without dedicated bytes

### Sleep Toggle and Wait Time

Sleep on, 2 packets. Sleep off, 1 packet. Change sleep time, 2 packets.

When sleep is turned on, the ("when to start" setting in iCUE) sleep time must be re-assigned every time. The two Data Bytes change when the sleep time value has been changed. I think the Setting Indicator byte flips to `0d` to toggle the actual sleep function since all other values are zeros at this point, and it is present for the first sleep-on packet and the only sleep-off packet. The byte goes to `0e` only after the first sleep-on packet to reassign the sleep time to the newly re-enabled sleep function.

Order of operations

1. **Turn sleep on in iCUE**
    
    Packet 1 = Feature Select `0d` , Data Bytes `ff`
    
    Packet 2 = Feature Select `0e` , Data Bytes specify the when to start time
    
2. **Turn sleep off in iCUE**
    
    Packet 1 = Feature Select `0d` , Data Bytes `00`
    

### Charge indicator LED

When the headset is connected VIA USB and fully charged, this led flashes. The speed at which interrupt-out packets are sent corresponds to the speed of the LED pulsing. <s>When it's solid green, it sticks to 80</s>. This LED is also full RGB which can be observed when switching the headset to wireless mode and may be independently controllable, but iCUE won't let users change this.

This byte is the dedicated brightness of the charge led.

You can send datum to this byte to change the intensity, but it is overwritten by, assumably, the firmware.

### Software Brightness control packets

Six packets are sent to the device when using the dedicated brightness slider.

### Logo-LED

No contention here. Pretty obvious which bytes control the primary LED just by toggling them.

### Software only

There are no packets sent from the host for the following functions: Sidetone and its volume slider, 7.1/Stereo, lighting link presets, EQ presets, Voice Prompts, Battery indicator in the status bar.
