Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Commander Core: Add fixed speed support #405

Merged
merged 5 commits into from
Jan 18, 2022
Merged
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
28 changes: 27 additions & 1 deletion docs/corsair-commander-core-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,21 @@ The device should be initialized every time it is powered on.
```
# liquidctl initialize
Corsair Commander Core (experimental)
├── Firmware version 1.6.135
├── Firmware version 2.6.201
├── AIO LED count 29
├── RGB port 1 LED count 8
├── RGB port 2 LED count 8
├── RGB port 3 LED count N/A
├── RGB port 4 LED count N/A
├── RGB port 5 LED count N/A
├── RGB port 6 LED count N/A
├── AIO port connected Yes
├── Fan port 1 connected Yes
├── Fan port 2 connected Yes
├── Fan port 3 connected No
├── Fan port 4 connected No
├── Fan port 5 connected No
├── Fan port 6 connected No
├── Water temperature sensor Yes
└── Temperature sensor 1 No
```
Expand All @@ -39,3 +46,22 @@ Corsair Commander Core (experimental)
├── Fan speed 6 0 rpm
└── Water temperature 35.8 °C
```

## Programming the pump and fan speeds

Currently, the pump and each fan can be set to a fixed duty cycle.

```
# liquidctl set fan1 speed 70
^^^^ ^^
channel duty
```

Valid channel values are `pump`, `fanN`, where 1 <= N <= 6 is the fan number, and
`fans`, to simultaneously configure all fans.

In iCUE the pump can be set to different modes that correspond to a fixed percent that can be used in liquidctl.
Quiet is 75%, Balanced is 85% and Extreme is 100%.

Note: The pump and some fans have a limit to how slow they can go and will not stop when set to zero.
This is a hardware limitation that cannot be changed.
66 changes: 62 additions & 4 deletions docs/developer/protocol/commander_core.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,9 @@ Command:
| 0x01 | 0x06 |
| 0x02 | Channel |
| 0x03, 0x04 | Data length |
| 0x05, 0x06 | 00:00 Unknown |
| 0x07-... | Data |
| 0x05, 0x06 | 00:00 Unknown (Before data length starts) |
| 0x07, 0x08 | Data Type (Included in data length) |
| 0x09-... | Data |

### `0x08` - Read

Expand Down Expand Up @@ -159,8 +160,26 @@ Data Type: `0x06 0x00`
| 0x05, 0x06 | Speed of Fan 2 |
| 0x07, 0x08 | Speed of Fan 3 |
| 0x09, 0x0a | Speed of Fan 4 |
| 0x0b, 0x1c | Speed of Fan 5 |
| 0x0d, 0x1e | Speed of Fan 6 |
| 0x0b, 0x0c | Speed of Fan 5 |
| 0x0d, 0x0e | Speed of Fan 6 |

### `0x1a` - Connected Speed Devices

Data Type: `0x09 0x00`

Connection State: 0x07 if connected or 0x01 if not connected

| Byte index | Description |
| ---------- | ----------- |
| 0x00 | Number of Ports |
| 0x01 | AIO/EXT Connection State|
| 0x02 | Fan 1 Connection State |
| 0x03 | Fan 2 Connection State |
| 0x04 | Fan 3 Connection State |
| 0x05 | Fan 4 Connection State |
| 0x06 | Fan 5 Connection State |
| 0x07 | Fan 6 Connection State |


### `0x20` - Connected LEDs

Expand Down Expand Up @@ -202,3 +221,42 @@ Data Type: `0x10 0x00`
| 0x02, 0x03 | Temperature in Celsius (needs to be divided by 10) |
| 0x04 | 0x00 if connected or 0x01 if not connected |
| 0x05, 0x06 | Temperature in Celsius (needs to be divided by 10) |

### `0x60 0x6d` - Hardware Speed Device Mode

Data Type: `0x03 0x00`

| Byte index | Description |
| ---------- | ----------- |
| 0x00 | Number of Ports |
| 0x01 | AIO/EXT Speed Mode |
| 0x02 | Fan 1 Speed Mode |
| 0x03 | Fan 2 Speed Mode |
| 0x04 | Fan 3 Speed Mode |
| 0x05 | Fan 4 Speed Mode |
| 0x06 | Fan 5 Speed Mode |
| 0x07 | Fan 6 Speed Mode |

Speed Modes:

| Mode | Description |
| ---- | ----------- |
| 0x00 | Fixed percentage |
| 0x02 | Fan percentage fan curve |

Note: This list is not complete and currently only contains what has been confirmed so far

### `0x61 0x6d` - Hardware Fixed Speed (Percentage)

Data Type: `0x04 0x00`

| Byte index | Description |
| ---------- | ----------- |
| 0x00 | Number of Ports |
| 0x01, 0x02 | Speed as percentage for AIO/EXT port |
| 0x03, 0x04 | Speed as percentage for Fan 1 |
| 0x05, 0x06 | Speed as percentage for Fan 2 |
| 0x07, 0x08 | Speed as percentage for Fan 3 |
| 0x09, 0x0a | Speed as percentage for Fan 4 |
| 0x0b, 0x0c | Speed as percentage for Fan 5 |
| 0x0d, 0x0e | Speed as percentage for Fan 6 |
2 changes: 2 additions & 0 deletions liquidctl.8
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ Internal data used by some drivers.
.
.SH DEVICE SPECIFICS
.
.SS Corsair Commander Core
Cooling channels: \fIpump\fR, \fIfans\fR, \fIfan[1\-6]\fR.
.SS Corsair Commander Pro
.SS Corsair Lighting Node Pro
.SS Corsair Lighting Node Core
Expand Down
85 changes: 73 additions & 12 deletions liquidctl/driver/commander_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from liquidctl.driver.usb import UsbHidDriver
from liquidctl.error import ExpectationNotMet, NotSupportedByDriver
from liquidctl.util import u16le_from
from liquidctl.util import clamp, u16le_from

_LOGGER = logging.getLogger(__name__)

Expand All @@ -23,20 +23,29 @@

_INTERFACE_NUMBER = 0

_FAN_COUNT = 6

_CMD_WAKE = (0x01, 0x03, 0x00, 0x02)
_CMD_SLEEP = (0x01, 0x03, 0x00, 0x01)
_CMD_GET_FIRMWARE = (0x02, 0x13)
_CMD_RESET = (0x05, 0x01, 0x00)
_CMD_SET_MODE = (0x0d, 0x00)
_CMD_GET = (0x08, 0x00)
_CMD_READ = (0x08, 0x00)
_CMD_WRITE = (0x06, 0x00)

_MODE_LED_COUNT = (0x20,)
_MODE_GET_SPEEDS = (0x17,)
_MODE_GET_TEMPS = (0x21,)
_MODE_CONNECTED_SPEEDS = (0x1a,)
_MODE_HW_SPEED_MODE = (0x60, 0x6d)
_MODE_HW_FIXED_PERCENT = (0x61, 0x6d)

_DATA_TYPE_SPEEDS = (0x06, 0x00)
_DATA_TYPE_LED_COUNT = (0x0f, 0x00)
_DATA_TYPE_TEMPS = (0x10, 0x00)
_DATA_TYPE_CONNECTED_SPEEDS = (0x09, 0x00)
_DATA_TYPE_HW_SPEED_MODE = (0x03, 0x00)
_DATA_TYPE_HW_FIXED_PERCENT = (0x04, 0x00)


class CommanderCore(UsbHidDriver):
Expand All @@ -58,15 +67,21 @@ def initialize(self, **kwargs):

# Get LEDs per fan
res = self._read_data(_MODE_LED_COUNT, _DATA_TYPE_LED_COUNT)

num_devices = res[0]
led_data = res[1:1 + num_devices * 4]
for i in range(0, num_devices):
connected = u16le_from(led_data, offset=i*4) == 2
num_leds = u16le_from(led_data, offset=i*4+2)
connected = u16le_from(led_data, offset=i * 4) == 2
num_leds = u16le_from(led_data, offset=i * 4 + 2)
label = 'AIO LED count' if i == 0 else f'RGB port {i} LED count'
status += [(label, num_leds if connected else None, '')]

# Get what fans are connected
res = self._read_data(_MODE_CONNECTED_SPEEDS, _DATA_TYPE_CONNECTED_SPEEDS)
num_devices = res[0]
for i in range(0, num_devices):
label = 'AIO port connected' if i == 0 else f'Fan port {i} connected'
status += [(label, res[i + 1] == 0x07, '')]

# Get what temp sensors are connected
for i, temp in enumerate(self._get_temps()):
connected = temp is not None
Expand Down Expand Up @@ -99,7 +114,27 @@ def set_speed_profile(self, channel, profile, **kwargs):
raise NotSupportedByDriver

def set_fixed_speed(self, channel, duty, **kwargs):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on displaying the fan/pump mode and target fixed speed (if set) that are set here in the status message? Maybe potentially under --verbose? If so is there a device that does this already or format ideas?
This can be another PR, I just had the idea.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be nice.

For the pump/fan mode, take a look at the Commander Pro and Smart Device (V1): those specific devices only report PWM or DC, but you could extend that idea for whether their running in fixed PWM or on-board profile modes. IIRC, they use enums with a suitable __str__() implementation for the pretty/human output.

For the target duty cycle (if set), take a look at the Corsair Platinum coolers.

raise NotSupportedByDriver
channels = CommanderCore._parse_channels(channel)

with self._wake_device_context():
# Set hardware speed mode
res = self._read_data(_MODE_HW_SPEED_MODE, _DATA_TYPE_HW_SPEED_MODE)
device_count = res[0]

data = bytearray(res[0:device_count + 1])
for chan in channels:
data[chan + 1] = 0x00 # Set the device's hardware mode to fixed percent
self._write_data(_MODE_HW_SPEED_MODE, _DATA_TYPE_HW_SPEED_MODE, data)

# Set speed
res = self._read_data(_MODE_HW_FIXED_PERCENT, _DATA_TYPE_HW_FIXED_PERCENT)
device_count = res[0]
data = bytearray(res[0:device_count * 2 + 1])
duty_le = int.to_bytes(clamp(duty, 0, 100), length=2, byteorder="little", signed=False)
for chan in channels:
i = chan * 2 + 1
data[i: i + 2] = duty_le # Update the device speed
self._write_data(_MODE_HW_FIXED_PERCENT, _DATA_TYPE_HW_FIXED_PERCENT, data)

@classmethod
def probe(cls, handle, **kwargs):
Expand All @@ -116,9 +151,9 @@ def _get_speeds(self):
res = self._read_data(_MODE_GET_SPEEDS, _DATA_TYPE_SPEEDS)

num_speeds = res[0]
speeds_data = res[1:1 + num_speeds*2]
speeds_data = res[1:1 + num_speeds * 2]
for i in range(0, num_speeds):
speeds.append(u16le_from(speeds_data, offset=i*2))
speeds.append(u16le_from(speeds_data, offset=i * 2))

return speeds

Expand All @@ -128,11 +163,11 @@ def _get_temps(self):
res = self._read_data(_MODE_GET_TEMPS, _DATA_TYPE_TEMPS)

num_temps = res[0]
temp_data = res[1:1 + num_temps*3]
temp_data = res[1:1 + num_temps * 3]
for i in range(0, num_temps):
connected = temp_data[i*3] == 0x00
connected = temp_data[i * 3] == 0x00
if connected:
temps.append(u16le_from(temp_data, offset=i*3+1)/10)
temps.append(u16le_from(temp_data, offset=i * 3 + 1) / 10)
else:
temps.append(None)

Expand All @@ -141,7 +176,7 @@ def _get_temps(self):
def _read_data(self, mode, data_type):
self._send_command(_CMD_RESET)
self._send_command(_CMD_SET_MODE, mode)
raw_data = self._send_command(_CMD_GET)
raw_data = self._send_command(_CMD_READ)

if tuple(raw_data[3:5]) != data_type:
raise ExpectationNotMet('device returned incorrect data type')
Expand Down Expand Up @@ -178,3 +213,29 @@ def _wake_device_context(self):
yield
finally:
self._send_command(_CMD_SLEEP)

def _write_data(self, mode, data_type, data):
self._read_data(mode, data_type) # Will ensure we are writing the correct data type to avoid breakage

self._send_command(_CMD_RESET)
self._send_command(_CMD_SET_MODE, mode)

buf = bytearray(len(data) + len(data_type) + 4)
buf[0: 2] = int.to_bytes(len(data) + 2, length=2, byteorder="little", signed=False)
buf[4: 4 + len(data_type)] = data_type
buf[4 + len(data_type):] = data

self._send_command(_CMD_WRITE, buf)

@staticmethod
def _parse_channels(channel):
if channel == 'pump':
return [0]
elif channel == "fans":
return range(1, _FAN_COUNT + 1)
elif channel.startswith("fan") and channel[3:].isnumeric() and 0 < int(channel[3:]) <= _FAN_COUNT:
return [int(channel[3:])]
else:
fan_names = ['fan' + str(i) for i in range(1, _FAN_COUNT + 1)]
fan_names_part = '", "'.join(fan_names)
raise ValueError(f'unknown channel, should be one of: "pump", "{fan_names_part}" or "fans"')
Loading