Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 01 - ndizazzo - initial hardware mode captures

Provided-by: @ndizazzo
Provided-at: 2026-05-13

Initial exploratory USBPcap captures used to compare startup, hardware-mode enable/disable, live lighting preview, and fan/cooling preset traffic.

Files:

- `commander-duo-01-startup.pcap`: from `/tmp/opencode/duo-usbpcap/startup.pcap`
- `commander-duo-02-hardware-enable.pcap`: from `/tmp/opencode/duo-usbpcap/hardware-enable.pcap`
- `commander-duo-03-disable-hardware-mode.pcap`: from `/tmp/opencode/duo-usbpcap/disable-hardware-mode.pcap`
- `commander-duo-04-hardware-fixed-red.pcap`: from `/tmp/opencode/duo-usbpcap/hardware-red.pcap`
- `commander-duo-05-hardware-lighting-preview.pcap`: from `/tmp/opencode/duo-usbpcap/hardware-lighting.pcap`
- `commander-duo-06-hardware-lighting-speed-preview.pcap`: from `/tmp/opencode/duo-usbpcap/hardware-lighting-set-speed.pcap`
- `commander-duo-07-hardware-lighting-direction-preview.pcap`: from `/tmp/opencode/duo-usbpcap/hardware-lighting-set-direction.pcap`
- `commander-duo-08-fan-quiet.pcap`: from `/tmp/opencode/duo-usbpcap/fan-quiet.pcap`
- `commander-duo-09-fan-balanced.pcap`: from `/tmp/opencode/duo-usbpcap/fan-balanced.pcap`
- `commander-duo-10-fan-extreme.pcap`: from `/tmp/opencode/duo-usbpcap/fan-extreme.pcap`
- `commander-duo-11-fan-zero-rpm.pcap`: from `/tmp/opencode/duo-usbpcap/fan-zero.pcap`
- `commander-duo-12-hardware-set-cooling-preset.pcap`: from `/tmp/opencode/duo-usbpcap/harware-set-cooling-preset.pcap`
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 02 - ndizazzo - device memory preview and apply captures

Provided-by: @ndizazzo
Provided-at: 2026-05-13

Follow-up captures for Device Memory preview, fixed/off lighting, global red, LED-count changes, rainbow/speed/direction previews, palette preview, save/apply, and after-replug behavior.

Files:

- `commander-duo-13-device-memory-baseline-open.pcap`: from `/tmp/opencode/duo-new-captures/duo-00-baseline-device-memory-open.pcap`
- `commander-duo-14-device-memory-fixed-aa55cc-preview.pcap`: from `/tmp/opencode/duo-new-captures/duo-02-device-memory-fixed-aa55cc.pcap`
- `commander-duo-15-device-memory-off-preview.pcap`: from `/tmp/opencode/duo-new-captures/duo-03-device-memory-off.pcap`
- `commander-duo-16-device-memory-global-red-preview.pcap`: from `/tmp/opencode/duo-new-captures/duo-04-device-memory-port1-red-port2-red.pcap`
- `commander-duo-17-device-memory-led-count-change.pcap`: from `/tmp/opencode/duo-new-captures/duo-07-device-memory-led-count-change.pcap`
- `commander-duo-18-device-memory-rainbow-preview.pcap`: from `/tmp/opencode/duo-new-captures/duo-08-device-memory-effect-rainbow.pcap`
- `commander-duo-19-device-memory-rainbow-speed-preview.pcap`: from `/tmp/opencode/duo-new-captures/duo-09-device-memory-effect-speed-change.pcap`
- `commander-duo-20-device-memory-rainbow-direction-preview.pcap`: from `/tmp/opencode/duo-new-captures/duo-10-device-memory-effect-direction-change.pcap`
- `commander-duo-21-device-memory-two-color-palette-preview.pcap`: from `/tmp/opencode/duo-new-captures/duo-11device-memory-two-color-palette.pcap`
- `commander-duo-22-device-memory-save-apply-only.pcap`: from `/tmp/opencode/duo-new-captures/duo-12-device-memory-save-apply-only.pcap`
- `commander-duo-23-device-memory-after-replug-readback.pcap`: from `/tmp/opencode/duo-new-captures/duo-13-after-replog-readback.pcap`
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 03 - ndizazzo - focused device memory commits and readback

Provided-by: @ndizazzo
Provided-at: 2026-05-13

Focused captures that confirm persistent rainbow commit, fixed-color 112233 commit byte order, and Device Memory readback payloads.

Files:

- `commander-duo-24-device-memory-rainbow-commit.pcap`: from `/tmp/opencode/duo-redo-captures/redo-01-rainbow.pcap`
- `commander-duo-25-device-memory-fixed-112233-commit.pcap`: from `/tmp/opencode/duo-redo-captures/redo-02-color-commit.pcap`
- `commander-duo-26-device-memory-fixed-112233-readback.pcap`: from `/tmp/opencode/duo-redo-captures/redo-03-readback.pcap`
Binary file not shown.
Binary file not shown.
Binary file not shown.
21 changes: 21 additions & 0 deletions Corsair Commander DUO/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Corsair Commander DUO

Provided-by: @ndizazzo
Provided-at: 2026-05-13

USBPcap captures used to reverse engineer liquidctl support for the Corsair Commander DUO.

Highlights:

- Standard 5 V ARGB software lighting uses live RGB writes on endpoint `0x22`.
- Device Memory fixed/off lighting commits use endpoint `65 6d` with data type `7e 20`.
- Focused readback confirms static Device Memory color order `ff bb gg rr`.
- Device Memory rainbow commits use endpoint `65 6d` with data type `02 a4`.
- Captured `61/62/63 6d` writes appear related to cooling/profile data and were not used as lighting writes.

The Python scripts in `analyzer - ndizazzo/` summarize HID payloads from these USBPcap files.

Capture groups:
- `01 - ndizazzo - initial hardware mode captures/`: Initial exploratory USBPcap captures used to compare startup, hardware-mode enable/disable, live lighting preview, and fan/cooling preset traffic.
- `02 - ndizazzo - device memory preview and apply captures/`: Follow-up captures for Device Memory preview, fixed/off lighting, global red, LED-count changes, rainbow/speed/direction previews, palette preview, save/apply, and after-replug behavior.
- `03 - ndizazzo - focused device memory commits and readback/`: Focused captures that confirm persistent rainbow commit, fixed-color 112233 commit byte order, and Device Memory readback payloads.
11 changes: 11 additions & 0 deletions Corsair Commander DUO/analyzer - ndizazzo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# analyzer - ndizazzo

Provided-by: @ndizazzo
Provided-at: 2026-05-13

Helper scripts used during Commander DUO protocol analysis.

- `duo_hid_summary.py`: summarizes HID host writes, open endpoints, live RGB writes, and endpoint payloads.
- `duo_extract_hid_csv.py`: extracts HID packets to CSV for manual inspection.

Both scripts require `tshark` in PATH and read HID payloads from `usbhid.data`.
68 changes: 68 additions & 0 deletions Corsair Commander DUO/analyzer - ndizazzo/duo_extract_hid_csv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Extract focused Commander DUO HID packet rows from USBPcap captures."""

from __future__ import annotations

import argparse
import csv
import subprocess
from pathlib import Path


def _hex_to_bytes(value: str) -> bytes:
return bytes.fromhex(value.replace(":", ""))


def _payload_body(payload: bytes) -> bytes:
if len(payload) >= 2 and payload[0] == 0x00 and payload[1] == 0x08:
return payload[1:]
return payload


def extract(path: Path, output: Path, endpoint_filter: set[str] | None) -> None:
cmd = [
"tshark",
"-r",
str(path),
"-T",
"fields",
"-e",
"frame.number",
"-e",
"frame.time_relative",
"-e",
"usb.endpoint_address",
"-e",
"usbhid.data",
]
proc = subprocess.run(cmd, check=True, text=True, capture_output=True)
with output.open("w", newline="") as fh:
writer = csv.writer(fh)
writer.writerow(["frame", "time_relative", "endpoint", "direction", "command", "payload_hex"])
for line in proc.stdout.splitlines():
frame, time_rel, endpoint, payload = (line.split("\t") + [""] * 4)[:4]
if not payload or (endpoint_filter and endpoint not in endpoint_filter):
continue
data = _payload_body(_hex_to_bytes(payload))
if not data or data[0] != 0x08:
continue
direction = "host" if endpoint == "0x04" else "device"
command = data[1:3].hex(" ") if len(data) >= 3 else ""
writer.writerow([frame, time_rel, endpoint, direction, command, data.hex(" ")])


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("pcap", type=Path)
parser.add_argument("output", type=Path)
parser.add_argument(
"--endpoint",
action="append",
help="Endpoint to include, e.g. 0x04 or 0x84. Repeatable. Defaults to all HID endpoints.",
)
args = parser.parse_args()
extract(args.pcap, args.output, set(args.endpoint) if args.endpoint else None)


if __name__ == "__main__":
main()
112 changes: 112 additions & 0 deletions Corsair Commander DUO/analyzer - ndizazzo/duo_hid_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Summarize Commander DUO USBPcap HID transactions.

Requires tshark in PATH. The reducer intentionally uses usbhid.data because
USBPcap captures for this device place HID payloads there, not usb.capdata.
"""

from __future__ import annotations

import argparse
import subprocess
from collections import Counter
from pathlib import Path


def _hex_to_bytes(value: str) -> bytes:
return bytes.fromhex(value.replace(":", ""))


def _tshark_rows(path: Path) -> list[tuple[int, float, str, bytes]]:
cmd = [
"tshark",
"-r",
str(path),
"-T",
"fields",
"-e",
"frame.number",
"-e",
"frame.time_relative",
"-e",
"usb.endpoint_address",
"-e",
"usbhid.data",
]
proc = subprocess.run(cmd, check=True, text=True, capture_output=True)
rows = []
for line in proc.stdout.splitlines():
number, time_rel, endpoint, payload = (line.split("\t") + [""] * 4)[:4]
if not payload:
continue
rows.append((int(number), float(time_rel or 0), endpoint, _hex_to_bytes(payload)))
return rows


def _payload_body(payload: bytes) -> bytes:
# Older captures include HID report id 00 before 08; newer captures start at 08.
if len(payload) >= 2 and payload[0] == 0x00 and payload[1] == 0x08:
return payload[1:]
return payload


def _command_data(data: bytes) -> bytes:
if len(data) < 3:
return bytes()
command = data[1:3]
body = data[3:]
if command == bytes([0x0D, 0x01]):
return body.rstrip(bytes([0x00]))
if command == bytes([0x06, 0x01]) and len(data) >= 9:
length = int.from_bytes(data[3:5], "little")
return data[3 : 7 + length]
if command in (bytes([0x06, 0x00]), bytes([0x07, 0x00])) and len(data) >= 5:
length = int.from_bytes(data[3:5], "little")
return data[3 : 7 + length]
return body.rstrip(bytes([0x00]))


def summarize(path: Path) -> None:
rows = _tshark_rows(path)
host = [(n, t, _payload_body(p)) for n, t, ep, p in rows if ep == "0x04"]
dev = [(n, t, _payload_body(p)) for n, t, ep, p in rows if ep == "0x84"]
opens = []
endpoint_writes = []
color_writes = []
for number, time_rel, data in host:
if len(data) < 4 or data[0] != 0x08:
continue
command = data[1:3]
if command == bytes([0x0D, 0x01]):
opens.append(_command_data(data).hex(" "))
elif command == bytes([0x06, 0x01]) and len(data) >= 9:
length = int.from_bytes(data[3:5], "little")
dtype = data[7:9]
payload = data[9 : 7 + length]
endpoint_writes.append((number, time_rel, length, dtype, payload))
elif command in (bytes([0x06, 0x00]), bytes([0x07, 0x00])):
color_writes.append((number, time_rel, command, data[3:]))

print(f"{path}")
print(f" host writes: {len(host)}")
print(f" device responses: {len(dev)}")
print(f" open endpoints: {Counter(opens).most_common(12)}")
print(f" live color writes: {len(color_writes)}")
for number, time_rel, length, dtype, payload in endpoint_writes:
print(
" endpoint write "
f"frame={number} time={time_rel:.6f} len={length} "
f"dtype={dtype.hex(' ')} payload={payload.hex(' ')}"
)


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("pcap", nargs="+", type=Path)
args = parser.parse_args()
for pcap in args.pcap:
summarize(pcap)


if __name__ == "__main__":
main()
Loading