USB ID 1b1c:0c04 is shared by several legacy Corsair Link devices, including
at least:
- Corsair H80i, H100i, H110i GT (AIO coolers)
- Corsair Link Cooling Node
- Corsair Commander Mini (fan + temperature + RGB controller)
They all present as a single "Integrated USB Bridge" HID endpoint that fronts the actual Corsair Link device over an internal serial daisy chain.
The USB descriptor is identical across all observed variants:
| Field | Value |
|---|---|
idVendor |
0x1B1C |
idProduct |
0x0C04 |
manufacturer_string |
"Corsair Memory, Inc." |
product_string |
"Integrated USB Bridge" |
serial_number |
(empty) |
bcdDevice |
0x0200 |
Source: hid.enumerate(0x1b1c, 0x0c04) against a Commander Mini.
The fields above are also what every other variant is expected to expose, so
liquidctl cannot dispatch on USB metadata alone.
Commands are variable-length packages packed into a single 64-byte HID OUT report:
report = [length] [pkg]+ [pad to 64 bytes]
pkg = [seq] [op] [cmd] [params...]
seq is a per-instance sequence number cycled 2..32. op selects a
read/write width:
| op | meaning |
|---|---|
| 0x06 | WRITE_ONE_BYTE |
| 0x07 | READ_ONE_BYTE |
| 0x08 | WRITE_TWO_BYTES |
| 0x09 | READ_TWO_BYTES |
| 0x0A | WRITE_THREE_BYTES |
| 0x0B | READ_THREE_BYTES |
The HID IN report packs responses back-to-back starting at byte 0 (no length prefix). Per-package response sizes:
| op | response payload |
|---|---|
| WRITE_ONE | [seq, 0x06] — 2 bytes, no echo |
| READ_ONE | [seq, 0x07, data] — 3 bytes |
| READ_TWO | [seq, 0x09, lo, hi] — 4 bytes |
| READ_THREE | [seq, 0x0B, b0, b1, b2] — 5 bytes |
A (SELECT, READ_TWO) pair therefore takes 7 request bytes and 6 response
bytes. The 63-byte report payload (after the length byte) caps a single
batch at ~9 such pairs; reads beyond that must be split across multiple HID
exchanges.
Evidence: probe_coolit.py against a Commander Mini. Sample combined
response for TEMP_SELECT 0 + TEMP_READ + FAN_SELECT 0 + FAN_READ_RPM:
17 06 18 09 f7 26 19 06 1a 09 4d 03
[seq,06] [seq,06] -- WRITE_ONE acks
[seq,09,lo,hi] [seq,09,lo,hi] -- READ_TWO payloads
data 0x26f7=38.97 C data 0x034d=845 rpm
Names below are from OpenCorsairLink's include/protocol/coolit.h
(enum COOLIT_Commands).
| cmd | name | width(s) | notes |
|---|---|---|---|
| 0x00 | DeviceID | READ_ONE | variant byte; see table above |
| 0x01 | FirmwareID | READ_TWO | [minor.patch_byte, major_byte] (e.g. 0x06 0x11 = 1.1.6) |
| 0x02 | ProductName | READ_ONE/TWO | always 4 ASCII bytes (e.g. "CLCC"); READ_ONE/TWO put data at bytes 2-5, READ_THREE prepends a stray 0x00 |
| 0x03 | Status | (unknown) | name from header; not exercised |
| 0x04 | LED_SelectCurrent | WRITE_ONE | selects active LED channel |
| 0x05 | LED_Count | READ_ONE | LED-channel count (1 on Commander Mini and H110i) |
| 0x06 | LED_Mode | READ/WRITE_ONE | Static=0x00, TwoColorCycle=0x40, FourColorCycle=0x80, TemperatureColor=0xC0 |
| 0x07 | LED_CurrentColor | READ_THREE | RGB triplet of currently displayed color |
| 0x08 | LED_TemperatureColor | (unknown) | name from header |
| 0x09 | LED_TemperatureMode | (unknown) | name from header |
| 0x0A | LED_TemperatureModeColors | WRITE_THREE | curve of (temp, R, G, B) points |
| 0x0B | LED_CycleColors | WRITE_THREE | sets the 4 cycle-mode colors; payload [0x0C, R0,G0,B0, R1,G1,B1, R2,G2,B2, R3,G3,B3] (leading 0x0C constant; static = all 4 triplets the same) |
| 0x0C | TEMP_SelectActiveSensor | WRITE_ONE | selects active temperature sensor |
| 0x0D | TEMP_CountSensors | READ_ONE | discoverable temp_count — Commander Mini: 4, H110i: 1 |
| 0x0E | TEMP_Read | READ_TWO | [frac_byte, integer_C] |
| 0x0F | TEMP_Limit | (unknown) | name from header |
| 0x10 | FAN_Select | WRITE_ONE | selects active fan/pump slot |
| 0x11 | FAN_Count | READ_ONE | discoverable fan-slot count — Commander Mini: 6, H110i: 3 (2 fans + pump) |
| 0x12 | FAN_Mode | READ/WRITE_ONE | enum on writes (FixedPWM/FixedRPM/Default/Quiet/Balanced/Performance/Custom); on reads, 0x03 = empty slot, 0x8f = fan present (default/auto); H110i pump reads 0x85 |
| 0x13 | FAN_FixedPWM | WRITE_ONE | duty 0–255 |
| 0x14 | FAN_FixedRPM | WRITE_TWO | u16le rpm |
| 0x15 | FAN_ReportExtTemp | (unknown) | name from header |
| 0x16 | FAN_ReadRPM | READ_TWO | u16le RPM (0x0000 = empty slot) |
| 0x17 | FAN_MaxRecordedRPM | READ_TWO | per-slot historic max |
| 0x18 | FAN_UnderSpeedThreshold | (unknown) | name from header |
| 0x19 | FAN_RPMTable | WRITE_THREE | custom curve: 5 (rpm-lo, rpm-hi) pairs after a leading 0x0A |
| 0x1A | FAN_TempTable | WRITE_THREE | custom curve: 5 (0x00, temp_C) pairs after a leading 0x0A |
Self-discovery commands are the load-bearing finding here.
TEMP_CountSensors (0x0D) and FAN_Count (0x11) mean the driver does
not need to hardcode temp_count and fan_count per variant — the
device tells us. The variant table only has to carry what the device
can't report: human-readable description and pump details.
Fan-mode enum (cmd 0x12 writes), from
OpenCorsairLink/include/protocol/coolit.h enum COOLIT_Fan_Modes:
FixedPWM=0x02, FixedRPM=0x04, Default=0x06, Quiet=0x08,
Balanced=0x0A, Performance=0x0C, Custom=0x0E. The current
coolit.py _FanMode enum only covers FixedPWM/FixedRPM/Custom.
LED-mode enum (cmd 0x06 writes), from enum COOLIT_Led_Modes:
Static=0x00, TwoColorCycle=0x40, FourColorCycle=0x80,
TemperatureColor=0xC0. To set a static color: write LED_SelectCurrent
to pick the channel, write LED_Mode=0x00, then LED_CycleColors with all
4 triplets equal to the desired RGB. (Driver does not yet implement
set_color.)
If true, the driver can read 0x00 at connect() time and dispatch to the
right (has_pump, fan_count, temp_count, ...) config without requiring the
user to pick with --match.
| device | DEVICE_TYPE byte | source |
|---|---|---|
| H80 | 0x37 |
OpenCorsairLink device.c |
| Cooling Node | 0x38 |
OpenCorsairLink device.c |
| Lighting Node | 0x39 |
OpenCorsairLink device.c |
| H100 | 0x3A |
OpenCorsairLink device.c |
| H80i | 0x3B |
OpenCorsairLink device.c |
| H100i | 0x3C |
OpenCorsairLink device.c |
| Commander Mini | 0x3D |
direct probe (PRODUCT_NAME="CLCC") |
| H100i GT | 0x40 |
OpenCorsairLink device.c |
| H110i GT | 0x41 |
OpenCorsairLink device.c |
| H110i (also labeled "H110i GT" by the original liquidctl driver) | 0x42 |
parsed pcap + OpenCorsairLink device.c |
Two rows are validated against real hardware: 0x3D (Commander Mini) by
direct probe, and 0x42 (H110i / H110i GT) via the OpenCorsairLink pcap in
collected-device-data/. The other eight come from OpenCorsairLink's
device.c table on the testing branch. The driver loads them as a
best-effort default; users on those variants will see auto-detection
succeed but if a per-variant detail (fan_count, pump_index) is wrong they
should report.
Naming note for 0x42: OpenCorsairLink calls 0x42 "H110i" (no GT) with
pump at fan slot 2; "H110i GT" is 0x41 with pump at slot 3. The historical
liquidctl driver was labeled "Corsair H110i GT" and hardcoded pump_index = 2 — so the driver was actually targeting OpenCorsairLink's "H110i". The
contributor whose pcap lives in collected-device-data/Corsair H110i GT/
likely had an H110i and named the directory wrong. We label 0x42 as
"Corsair H110i / H110i GT" so substring --match works for either name
and existing user scripts don't break.
Firmware versions also differ, secondary signal:
| device | firmware (cmd 0x01) |
|---|---|
| Commander Mini | 1.1.6 (06 11) |
| H110i GT | 2.0.0 (00 20) |
Evidence sources to mine:
collected-device-data/Corsair H110i GT/*.pcapng— Wireshark captures of OpenCorsairLink configuring fan curves on a real H110i GT. Parsed;parse_h110i_pcap.pyin the repo root shows OpenCorsairLink reads cmd0x00repeatedly and gets0x42every time. PRODUCT_NAME is not probed.- OpenCorsairLink source — likely has a hardcoded table of device-type bytes; could be cross-referenced for the H80i / H100i / Cooling Node rows.
| device | has_pump | fan_count | temp_count |
|---|---|---|---|
| H110i GT | True | 2 | 1 |
| Commander Mini | False | 6 | 4 |
| H80i / H100i | True | 2 (?) | 1 (?) |
| Cooling Node | False | ? (4?) | ? (4?) |
Question marks indicate values not yet confirmed against real hardware or captures.
A fan slot with nothing connected returns 0x0000 for both RPM bytes
(verified on Commander Mini: slots 2, 3 empty read 0; slots 0, 1, 4, 5
populated). The driver reports these as 0 rpm rows (does not hide
them) so users can see which slot index is empty.
A second, more direct signal: FAN_MODE (0x12) reads as 0x03 for an
empty slot vs 0x8f for a populated one (default/auto mode). This
distinguishes "fan stopped" (mode 0x8f, rpm 0) from "no fan" (mode 0x03,
rpm 0), which the rpm-only heuristic cannot. The driver does not yet use
this signal.
FAN_MAX_RPM (0x17) reads 0x0000 for empty slots and the fan's
nominal max RPM for populated ones (Commander Mini sample: slots 0/1
report ~1725, slots 4/5 report ~1315 — a mix of fan models).
When TEMP_READ is issued with no thermistor on the selected header, the
Commander Mini returns a value with the integer byte's high bit set.
Observed: integer byte 0xb4 on one unplugged port (which would otherwise
decode as ~180 °C). The driver currently treats any read where
integer_byte & 0x80 is true as "disconnected" and omits that row.
Open questions:
- Is the sentinel always
0xb4xx, or any value above 127 °C? - Does it match across variants (e.g. AIO devices)?
A clean way to verify would be to compare a port's reading immediately before and after physically disconnecting a probe; awkward to do on a running system.
Protocol index 0 corresponds to the highest-numbered silkscreen port (T4), and index 3 to the lowest (T1). That is:
TEMP_SELECT N -> driver labels "Temperature {temp_count - N}"
-> matches physical port {temp_count - N}
User-confirmed by plugging a known probe into the silkscreen-labeled port
"2" and observing the reading appear at TEMP_SELECT 2, which the driver
now labels "Temperature 2" after the reversal.
This reversal is presumed Commander-Mini-specific; the AIO variants only have one temperature probe (the cold-plate liquid sensor) so the question of label ordering does not arise for them.
- Lighting / RGB control — Commander Mini has 1 LED channel and the AIOs have RGB rings, but the driver currently exposes no color control.
- Whether the device-type byte changes after firmware updates.
- Behavior of pump-mode commands on non-AIO variants — gated off by
has_pump=Falsein the driver, so untested. - Whether multiple Corsair Link devices can sit behind one bridge (the "Integrated USB Bridge" name hints at this); current driver assumes one.