Commander ST fw 2.x (tested on Corsair iCUE H170i ELITE CAPELLIX AIO) — single-shot apply workaround
A standalone Python script for users hitting either of these symptoms on a Corsair Commander Core / Commander ST running firmware 2.x:
liquidctl set <ch> speed T P T P …fails withIndexError: index out of range—_read_datahard-codes three HID read chunks (~181 bytes) and the device's full curve table for 7 channels at 7 points each exceeds that.liquidctl status(or anyset) times out after ~5-25 invocations, only recovers withusbreset— the device's firmware silently stops responding to HID after a burst of operations, while the USB stack and kernel hidraw layer remain healthy (dmesgis silent during the failure;lsusb -vstill works).
Tracking issue: liquidctl/liquidctl#753.
A workaround, not a fix. It bypasses the upstream set_* CLI flow entirely and writes the device's three hardware-profile tables (SPEED_MODE, FIXED_PERCENT, CURVE_PERCENT) directly in one wake context, ~12 HID ops total — well under the device's firmware-lockup threshold. Because it doesn't pre-read any of those tables, it also dodges the _read_data 181-byte truncation.
Net effect: a single boot-time invocation applies your full fan/pump config and exits. The Commander ST then drives the fans autonomously from its onboard water-temp sensor; no daemon polls the device afterwards.
- Pokes private API. It imports
_CMD_*,_send_command, etc. fromliquidctl.driver.commander_core. Upstream doesn't guarantee that surface — a future refactor could break this script. - Commander Core/ST-only. Hard-codes 7 channels (1 pump + 6 fan ports). Other Commander Core variants might work but are untested.
- Read-write only. It never reads back what's on the device. If anything else (iCUE on Windows, other tooling) has written settings the script then overwrites them — including channels you didn't list, which default to
FIXED 0%. - Not a fix for upstream. If the
_read_datachunking and the firmware lockup both get addressed upstream, this script becomes redundant.
apply.py— the scriptapply.json.example— example config for an iCUE H170i ELITE CAPELLIX (Commander ST internal)
# adjust paths in apply.json to your setup; copy to /etc/ or wherever
sudo /usr/bin/python3 apply.py /path/to/apply.jsonOr wire as a systemd oneshot at boot — see the liquidctl-curves.service template at the end of this README.
{
"channels": {
"pump": {"fixed": 80},
"fan4": {"curve": [[25, 30], [35, 50], [45, 75], [55, 100]]},
"fan5": {"curve": [[25, 30], [35, 50], [45, 75], [55, 100]]},
"fan6": {"curve": [[25, 30], [35, 50], [45, 75], [55, 100]]}
}
}Each channel takes either {"fixed": int_0_100} or {"curve": [[temp_c, duty_pct], ...]} (2–7 points). Omitted channels default to FIXED 0%. Channel names are pump, fan1..fan6.
[Unit]
Description=Apply Corsair Commander ST fan and pump curves
After=multi-user.target
ConditionPathExists=/dev/hidraw0
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/usr/bin/usbreset 1b1c:0c32
ExecStartPre=/bin/sleep 3
ExecStart=/usr/bin/python3 /usr/local/bin/apply.py /etc/liquidctl-apply.json
[Install]
WantedBy=multi-user.targetAdjust the 1b1c:0c32 USB ID to your device (lsusb -d 1b1c:). The usbreset step recovers the device if its firmware locked up before this boot — without it, the script could time out on a stale lockup carried over from the previous session.