Aksh (@its_aksh_) on X

X (formerly Twitter) ·

5 min read Original article ↗

Installed Linux for a project.

Ryzen 9 9950X hit ~90°C.

Fans sat at ~700 RPM.

BIOS curves were configured correctly.

Nothing was technically broken.

The issue was structural: the fans were USB-controlled and not connected to PWM headers.

Which means the BIOS was never in the control path to begin with.

1) System Info

CPU: Ryzen 9 9950X

OS: Ubuntu 22.04.5 LTS

Fans: Lian Li UNI FAN SL Wireless (USB-only via wireless TX/RX dongle)

0416:8040 (SLV3TX)

0416:8041 (SLV3RX)

Controller: Corsair iCUE Commander Core

1b1c:0c32

Wiring: Zero PWM leads to the motherboard (intentional)

Boot: Dual boot (Linux + Windows)

Linux could read temperatures fine. Nothing was issuing fan commands to the USB controllers. That gap--sensors exist, control doesn’t--is the whole problem.

2) Lian Li SL Wireless (USB-Only, No PWM Lead)

2.1 Verifying the Control Path

First: make sure Linux can see the dongle.

Quick context:

USB devices show up as VID:PID (Vendor ID : Product ID).

Seeing those IDs means the dongle exists on the USB bus.

It doesn’t prove you can control fans yet--but it proves where control must happen: userspace → USB.

2.2 What Didn’t Work

BIOS fan curves

“Motherboard sync”

Uni-sync (empty config)

Why: those approaches assume either (a) PWM wiring, or (b) a different controller behavior. In USB-only mode, the BIOS can observe temperatures but cannot actuate the fans.

2.3 What uwscli Is

uwscli (uws) is an open-source CLI that talks directly to the Lian Li SL Wireless controller over USB. No vendor GUI required. It sends the actual control commands.

What it does, practically:

Enumerates receivers

Sets fan PWM values

Gives you control entirely in Linux

Here’s the proof command:

If RPM jumps immediately, that means you have authority over the controller.

Everything after this is just curve design.

2.4 Install + Permissions

Create a venv and install:

Add udev rules so you don’t have to run it as root:

Reload and replug:

2.5 Calibration + Curve (PWM ≠ RPM)

PWM is the command. RPM is what you get. They correlate, but not perfectly, so I measured.

Measured (approx):

PWM → RPM

40 → ~340

80 → ~675

137 → ~1110

160 → ~1275

240 → ~1880

My Windows curve was defined in RPM targets. I mapped it to PWM (0–255) using calibration.

Windows curve (RPM targets):

34°C → 500 RPM

55°C → 699 RPM

65°C → 950 RPM

78°C → 1250 RPM

90°C → 2000 RPM (max)

Mapped to PWM (via measurements):

34°C → PWM 59

55°C → PWM 83

65°C → PWM 116

78°C → PWM 156

90°C → PWM 255 (max)

2.6 The Control Loop

Loop logic:

Read CPU temperature from /sys/class/hwmon (AMD k10temp)

Interpolate target PWM from curve points (piecewise-linear)

Send it to the controller via uws

Stability / noise control choices:

Ramp up: immediate (safety first)

Ramp down: delayed by 5 seconds (prevents yo-yo noise)

Poll interval: ~1 second

Ignore tiny PWM deltas: < 3 (reduces micro-chatter + USB spam)

The actual command being issued inside the loop is basically:

2.7 systemd Unit (What It Is + Minimal Example)

A systemd unit is a Linux service definition. It allows your script to run automatically at boot and restart if it crashes.

Example:

Load test:

Portable equivalent (all logical cores):

If temps rise and the fans follow, it’s done.

3) Corsair Commander Core (USB Control, But Quirky)

3.1 What liquidctl Is

liquidctl is a CLI that communicates with USB cooling devices: AIO pumps, fan controllers, RGB controllers, etc.

Here, it reliably:

Reads pump RPM, fan RPMs, water temp

Sets pump duty (%)

Sets fan duty (%)

It did not reliably handle device-stored curve mode on my unit.

liquidctl reported mine as: “Corsair Commander ST (broken)”

Which is not reassuring, but it was also not wrong.

3.2 What Failed (Device-Side Curves)

Trying to use hardware-stored curves failed with:

IndexError('index out of range')

Since fixed duty control works reliably, a software-driven CPU temperature loop became the stable solution.

Working commands:

3.3 Pump + Fan Strategy (Same as Lian Li)

Pump mapping I used (Windows → Linux mental model):

Quiet: 75%

Balanced: 85%

Extreme: 100%

On Linux, I chose 75%:

Fan curve (duty %), Lian Li-like shape:

30°C → 23%

40°C → 33%

50°C → 45%

60°C → 61%

70°C → 100%

Minimum clamp:

FAN_MIN = 33%

Meaning 23% becomes 33% in practice, because very low duty can behave weirdly on some setups.

Stability parameters I used:

POLL_S = 2.5s

HYST_C = 1.0°C

MIN_CHANGE = 5%

WINDOW = 7 (median smoothing)

Definitions:

Polling interval: how often you check CPU temp

Hysteresis: ignores tiny temp fluctuations so you don’t flap around thresholds

Min change: avoids spamming USB updates for tiny duty changes

Median window: smooths noisy sensor reads before making decisions

RGB can flash when the controller receives updates. Reducing update frequency and adding hysteresis helps, but it may not fully eliminate it.

Measured RPMs at fixed duty:

Duty (%) → RPM

33% → ~885 RPM

45% → ~1175 RPM

61% → ~1520 RPM

3.4 Concurrency Warning

If your systemd service is running and you manually run liquidctl, you may see:

AssertionError('response does not match command')

That’s because two processes are talking to the same USB controller at once. It’s not a “Linux problem”; it’s a “stop pestering the same hardware” problem.

Optional mitigation: serialize access with flock:

Or just don’t run manual liquidctl while the daemon is active.

4) Dual Boot Safety

Some tools can write settings to non-volatile controller memory.

I did not use --non-volatile.

Meaning:

Linux changes are runtime-only

Boot into Windows → iCUE / L-Connect reasserts your Windows profiles

Worst case: the last Linux duty persists briefly until Windows software loads, then it gets overwritten

This is how you avoid accidentally “fixing Linux” and breaking your Windows setup.

5) Stability Notes

Minimum clamp: floor for fan speed so you don’t hit unstable low-duty zones

Polling interval: how often you read temperature

Hysteresis: ignore small fluctuations around thresholds

Delta threshold / min change: avoid spamming USB commands for tiny adjustments

Boot note: These curves apply only after Linux userspace starts. The systemd services launch automatically at boot, then keep enforcing the curve while the machine is running.

Fin.