commander_pro: add support for changing fan modes during init (#474)
Closes: #472.
This commit is contained in:
parent
90ef98f721
commit
c03b445d0a
|
@ -38,6 +38,12 @@ Corsair Lighting Node Pro
|
|||
```
|
||||
|
||||
|
||||
Passing `--fan-mode='<fan_num>:<mode>[,...]'` can be used to change the
|
||||
fan mode from `dc` to `pwm` to `off` if the connected fan type is changed.
|
||||
The `--fan-mode` option is persistent across restarts and can only be used
|
||||
when not using the hwmon driver.
|
||||
|
||||
|
||||
## Retrieving the fan speeds, temperatures and voltages
|
||||
|
||||
The Lighting Node Pro and Lighting Node Core do not have a status message.
|
||||
|
|
|
@ -105,6 +105,7 @@ _liquidctl_main() {
|
|||
--start-led
|
||||
--maximum-leds
|
||||
--temperature-sensor
|
||||
--fan-mode
|
||||
"
|
||||
|
||||
# generate options list and remove any flag that has already been given
|
||||
|
|
|
@ -163,6 +163,9 @@ The number of LED's the effect should apply to.
|
|||
.
|
||||
.SS Other options
|
||||
.TP
|
||||
.BI \-\-fan\-mode= channel:mode[,...]
|
||||
Set the fan modes.
|
||||
.TP
|
||||
.B \-\-single\-12v\-ocp
|
||||
Enable single rail +12V OCP.
|
||||
.TP
|
||||
|
@ -251,6 +254,10 @@ Lighting channels: not yet supported.
|
|||
.SS Corsair Obsidian 1000D
|
||||
Cooling channels (only Commander Pro and Obsidian 1000D): \fIsync\fR, \fIfan[1\-6]\fR.
|
||||
.PP
|
||||
Where the fan connection types can be set with
|
||||
.BI \-\-fan\-mode= channel:mode[,...] ,
|
||||
where the allowed modes are: \fIoff\fR, \fIdc\fR, \fIpwm\fR.
|
||||
.PP
|
||||
Lighting channels: (only Lighting Node Core:) \fIled\fR;
|
||||
(only Commander Pro and Lighting Node Pro:) \fIsync\fR, \fIled[1\-2]\fR.
|
||||
.TS
|
||||
|
|
|
@ -12,41 +12,42 @@ Usage:
|
|||
liquidctl --version
|
||||
|
||||
Device selection options (see: list -v):
|
||||
-m, --match <substring> Filter devices by description substring
|
||||
-n, --pick <number> Pick among many results for a given filter
|
||||
--vendor <id> Filter devices by hexadecimal vendor ID
|
||||
--product <id> Filter devices by hexadecimal product ID
|
||||
--release <number> Filter devices by hexadecimal release number
|
||||
--serial <number> Filter devices by serial number
|
||||
--bus <bus> Filter devices by bus
|
||||
--address <address> Filter devices by address in bus
|
||||
--usb-port <port> Filter devices by USB port in bus
|
||||
-m, --match <substring> Filter devices by description substring
|
||||
-n, --pick <number> Pick among many results for a given filter
|
||||
--vendor <id> Filter devices by hexadecimal vendor ID
|
||||
--product <id> Filter devices by hexadecimal product ID
|
||||
--release <number> Filter devices by hexadecimal release number
|
||||
--serial <number> Filter devices by serial number
|
||||
--bus <bus> Filter devices by bus
|
||||
--address <address> Filter devices by address in bus
|
||||
--usb-port <port> Filter devices by USB port in bus
|
||||
|
||||
Animation options (devices/modes can support zero or more):
|
||||
--speed <value> Abstract animation speed (device/mode specific)
|
||||
--time-per-color <value> Time to wait on each color (seconds)
|
||||
--time-off <value> Time to wait with the LED turned off (seconds)
|
||||
--alert-threshold <number> Threshold temperature for a visual alert (°C)
|
||||
--alert-color <color> Color used by the visual high temperature alert
|
||||
--direction <string> If the pattern should move forward or backward.
|
||||
--start-led <number> The first led to start the effect at
|
||||
--maximum-leds <number> The number of LED's the effect should apply to
|
||||
--speed <value> Abstract animation speed (device/mode specific)
|
||||
--time-per-color <value> Time to wait on each color (seconds)
|
||||
--time-off <value> Time to wait with the LED turned off (seconds)
|
||||
--alert-threshold <number> Threshold temperature for a visual alert (°C)
|
||||
--alert-color <color> Color used by the visual high temperature alert
|
||||
--direction <string> If the pattern should move forward or backward.
|
||||
--start-led <number> The first led to start the effect at
|
||||
--maximum-leds <number> The number of LED's the effect should apply to
|
||||
|
||||
Other device options:
|
||||
--single-12v-ocp Enable single rail +12V OCP
|
||||
--pump-mode <mode> Set the pump mode (certain Corsair coolers)
|
||||
--temperature-sensor <number> The temperature sensor number for the Commander Pro
|
||||
--legacy-690lc Use Asetek 690LC in legacy mode (old Krakens)
|
||||
--non-volatile Store on non-volatile controller memory
|
||||
--direct-access Directly access the device despite kernel drivers
|
||||
--unsafe <features> Comma-separated bleeding-edge features to enable
|
||||
--single-12v-ocp Enable single rail +12V OCP
|
||||
--pump-mode <mode> Set the pump mode (certain Corsair coolers)
|
||||
--fan-mode <channel>:<mode>[,...] Set the mode for each fan (certain Corsair devices)
|
||||
--temperature-sensor <number> The temperature sensor number for the Commander Pro
|
||||
--legacy-690lc Use Asetek 690LC in legacy mode (old Krakens)
|
||||
--non-volatile Store on non-volatile controller memory
|
||||
--direct-access Directly access the device despite kernel drivers
|
||||
--unsafe <features> Comma-separated bleeding-edge features to enable
|
||||
|
||||
Other interface options:
|
||||
-v, --verbose Output additional information
|
||||
-g, --debug Show debug information on stderr
|
||||
--json JSON output (list/initialization/status)
|
||||
--version Display the version number
|
||||
--help Show this message
|
||||
-v, --verbose Output additional information
|
||||
-g, --debug Show debug information on stderr
|
||||
--json JSON output (list/initialization/status)
|
||||
--version Display the version number
|
||||
--help Show this message
|
||||
|
||||
Deprecated:
|
||||
-d, --device <index> Select device by listing index
|
||||
|
@ -83,7 +84,7 @@ from docopt import docopt
|
|||
from liquidctl import __version__
|
||||
from liquidctl.driver import *
|
||||
from liquidctl.error import NotSupportedByDevice, NotSupportedByDriver, UnsafeFeaturesNotEnabled
|
||||
from liquidctl.util import color_from_str
|
||||
from liquidctl.util import color_from_str, fan_mode_parser
|
||||
|
||||
|
||||
# conversion from CLI arg to internal option; as options as forwarded to bused
|
||||
|
@ -116,6 +117,7 @@ _PARSE_ARG = {
|
|||
'--legacy-690lc': bool,
|
||||
'--non-volatile': bool,
|
||||
'--direct-access': bool,
|
||||
'--fan-mode': lambda x: fan_mode_parser(x),
|
||||
'--unsafe': lambda x: x.lower().split(','),
|
||||
'--verbose': bool,
|
||||
'--debug': bool,
|
||||
|
|
|
@ -41,6 +41,7 @@ _CMD_GET_FAN_MODES = 0x20
|
|||
_CMD_GET_FAN_RPM = 0x21
|
||||
_CMD_SET_FAN_DUTY = 0x23
|
||||
_CMD_SET_FAN_PROFILE = 0x25
|
||||
_CMD_SET_FAN_MODE = 0x28
|
||||
|
||||
_CMD_RESET_LED_CHANNEL = 0x37
|
||||
_CMD_BEGIN_LED_EFFECT = 0x34
|
||||
|
@ -83,6 +84,11 @@ _MODES = {
|
|||
'rainbow2': 0x0a,
|
||||
}
|
||||
|
||||
_FAN_MODES = {
|
||||
'off': _FAN_MODE_DISCONNECTED,
|
||||
'dc': _FAN_MODE_DC,
|
||||
'pwm': _FAN_MODE_PWM,
|
||||
}
|
||||
|
||||
def _prepare_profile(original, critcalTempature):
|
||||
clamped = ((temp, clamp(duty, 0, _MAX_FAN_RPM)) for temp, duty in original)
|
||||
|
@ -157,7 +163,7 @@ class CommanderPro(UsbHidDriver):
|
|||
self._data = RuntimeStorage(key_prefixes=[ids, loc])
|
||||
return ret
|
||||
|
||||
def _initialize_directly(self, **kwargs):
|
||||
def _initialize_directly(self, fan_modes: dict, **kwargs):
|
||||
res = self._send_command(_CMD_GET_FIRMWARE)
|
||||
fw_version = (res[1], res[2], res[3])
|
||||
|
||||
|
@ -179,6 +185,13 @@ class CommanderPro(UsbHidDriver):
|
|||
]
|
||||
|
||||
if self._fan_count > 0:
|
||||
for i, value in fan_modes.items():
|
||||
fan_num = int(i) - 1
|
||||
if value not in ['dc', 'pwm', 'off']:
|
||||
raise ValueError(f"invalid fan mode: '{value}'")
|
||||
|
||||
self._send_command(_CMD_SET_FAN_MODE, [0x02, fan_num, _FAN_MODES.get(value)])
|
||||
|
||||
res = self._send_command(_CMD_GET_FAN_MODES)
|
||||
fanModes = res[1:self._fan_count+1]
|
||||
self._data.store('fan_modes', fanModes)
|
||||
|
@ -230,7 +243,7 @@ class CommanderPro(UsbHidDriver):
|
|||
|
||||
return status
|
||||
|
||||
def initialize(self, direct_access=False, **kwargs):
|
||||
def initialize(self, direct_access=False, fan_modes={}, **kwargs):
|
||||
"""Initialize the device and the driver.
|
||||
|
||||
This method should be called every time the systems boots, resumes from
|
||||
|
@ -245,12 +258,15 @@ class CommanderPro(UsbHidDriver):
|
|||
if self._hwmon and not direct_access:
|
||||
_LOGGER.info('bound to %s kernel driver, assuming it is already initialized',
|
||||
self._hwmon.driver)
|
||||
|
||||
if fan_modes:
|
||||
_LOGGER.warning('kernel driver does not support the `fan mode` options, ignoring')
|
||||
return self._get_static_info_from_hwmon()
|
||||
else:
|
||||
if self._hwmon:
|
||||
_LOGGER.warning('forcing re-initialization despite %s kernel driver',
|
||||
self._hwmon.driver)
|
||||
return self._initialize_directly()
|
||||
return self._initialize_directly(fan_modes)
|
||||
|
||||
def _get_status_directly(self):
|
||||
temp_probes = self._data.load('temp_sensors_connected', default=[0]*self._temp_probs)
|
||||
|
|
|
@ -8,6 +8,8 @@ import colorsys
|
|||
import logging
|
||||
from ast import literal_eval
|
||||
from enum import Enum, EnumMeta, unique
|
||||
from typing import Optional
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
import crcmod.predefined
|
||||
|
@ -404,6 +406,117 @@ def map_direction(direction, forward=None, backward=None):
|
|||
raise ValueError(f'invalid direction: {direction!r}')
|
||||
|
||||
|
||||
def fan_mode_parser(value: Optional[str], max_fans: Optional[int] = None) -> dict:
|
||||
"""Convert the --fan-mode=<key>:<value>[,...] options into a {key: value, ....} dictionary.
|
||||
The default is an empty dictionary.
|
||||
|
||||
Unstable.
|
||||
|
||||
>>> fan_mode_parser(None, 5)
|
||||
{}
|
||||
|
||||
>>> fan_mode_parser('', 5)
|
||||
{}
|
||||
|
||||
>>> fan_mode_parser('1:dc', 5)
|
||||
{'1': 'dc'}
|
||||
|
||||
>>> fan_mode_parser('2:auto', 5)
|
||||
{'2': 'auto'}
|
||||
|
||||
>>> fan_mode_parser('3:off', 5)
|
||||
{'3': 'off'}
|
||||
|
||||
>>> fan_mode_parser('1:DC', 5)
|
||||
{'1': 'dc'}
|
||||
|
||||
>>> fan_mode_parser('2:AUTO', 5)
|
||||
{'2': 'auto'}
|
||||
|
||||
>>> fan_mode_parser('3:OFF', 5)
|
||||
{'3': 'off'}
|
||||
|
||||
>>> fan_mode_parser('5:OFF', 5)
|
||||
{'5': 'off'}
|
||||
|
||||
>>> fan_mode_parser('5:OFF')
|
||||
{'5': 'off'}
|
||||
|
||||
>>> fan_mode_parser('1:dc,2:auto,3:off,4:pwm', 5)
|
||||
{'1': 'dc', '2': 'auto', '3': 'off', '4': 'pwm'}
|
||||
|
||||
>>> fan_mode_parser('1:dc, 2:auto, 3:off', 5)
|
||||
{'1': 'dc', '2': 'auto', '3': 'off'}
|
||||
|
||||
>>> fan_mode_parser('4:dc, 3:auto, 2:off', 5)
|
||||
{'2': 'off', '3': 'auto', '4': 'dc'}
|
||||
|
||||
>>> fan_mode_parser('1 :dc, 2: auto, 3 : off, 4 : auto ', 5)
|
||||
{'1': 'dc', '2': 'auto', '3': 'off', '4': 'auto'}
|
||||
|
||||
>>> fan_mode_parser('1:dc:dc', 5)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: invalid format, should be '<fan num>:<mode>'
|
||||
|
||||
>>> fan_mode_parser('-1:dc', 5)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: invalid fan number: '-1'
|
||||
|
||||
>>> fan_mode_parser('0:pwm', 5)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: invalid fan number: '0'
|
||||
|
||||
>>> fan_mode_parser('5:dc', 4)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: invalid fan number: '5'
|
||||
|
||||
>>> fan_mode_parser('a:dc', 5)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: invalid fan number: 'a'
|
||||
|
||||
|
||||
>>> fan_mode_parser('1:PMW', 5)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: invalid fan mode: 'PMW'
|
||||
|
||||
"""
|
||||
|
||||
if not value:
|
||||
return {}
|
||||
|
||||
parts = value.split(',')
|
||||
|
||||
opts = {}
|
||||
for p in parts:
|
||||
p2 = p.split(':')
|
||||
if len(p2) != 2:
|
||||
raise ValueError("invalid format, should be '<fan num>:<mode>'")
|
||||
|
||||
[key, val] = [i.strip() for i in p2]
|
||||
|
||||
try:
|
||||
key_val = int(key, 10)
|
||||
except ValueError:
|
||||
raise ValueError(f"invalid fan number: '{key}'")
|
||||
|
||||
if key_val <= 0 or (max_fans is not None and key_val > max_fans):
|
||||
raise ValueError(f"invalid fan number: '{key}'")
|
||||
|
||||
if val.lower() not in ['off', 'auto', 'dc', 'pwm']:
|
||||
raise ValueError(f"invalid fan mode: '{val}'")
|
||||
|
||||
opts.update({key: val.lower()})
|
||||
|
||||
|
||||
return dict(sorted(opts.items(), key=lambda item: item[0]))
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def mkCrcFun(crc_name):
|
||||
"""Efficiently construct a predefined CRC function.
|
||||
|
|
|
@ -164,6 +164,32 @@ def test_connect_lighting(lightingNodeProDeviceUnconnected):
|
|||
assert lightingNodeProDeviceUnconnected._data is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('exp,fan_modes', [([(3, 0x01)], {'4': 'dc'}), ([(3, 0x00)], {'4': 'off'}), ([(4, 0x02)], {'5': 'pwm'}), ([(0, 0x01),(2, 0x02),(1, 0x01)], {'1': 'dc', '3':'pwm', '2': 'dc'})])
|
||||
def test_initialize_commander_pro_fan_modes(commanderProDevice, exp, fan_modes, tmp_path):
|
||||
|
||||
responses = [
|
||||
'000009d4000000000000000000000000', # firmware
|
||||
'00000500000000000000000000000000', # bootloader
|
||||
'00010100010000000000000000000000', # temp probes
|
||||
'00010102000000000000000000000000', # fan set (throw away)
|
||||
'00010102000000000000000000000000', # fan set (throw away)
|
||||
'00010102000000000000000000000000', # fan set (throw away)
|
||||
'00010102000000000000000000000000' # fan probes
|
||||
]
|
||||
for d in responses:
|
||||
commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d)))
|
||||
|
||||
commanderProDevice.initialize(direct_access=True, fan_modes=fan_modes)
|
||||
|
||||
sent = commanderProDevice.device.sent
|
||||
assert len(sent) == 4+len(exp)
|
||||
|
||||
for i in range(len(exp)):
|
||||
assert sent[3+i].data[0] == 0x28
|
||||
assert sent[3+i].data[2] == exp[i][0]
|
||||
assert sent[3+i].data[3] == exp[i][1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('has_hwmon,direct_access', [(False, False), (True, True), (True, False)])
|
||||
def test_initialize_commander_pro(commanderProDevice, has_hwmon, direct_access, tmp_path):
|
||||
if has_hwmon and not direct_access:
|
||||
|
@ -234,7 +260,7 @@ def test_initialize_commander_pro(commanderProDevice, has_hwmon, direct_access,
|
|||
def test_initialize_lighting_node(lightingNodeProDevice):
|
||||
responses = [
|
||||
'000009d4000000000000000000000000', # firmware
|
||||
'00000500000000000000000000000000' # bootloader
|
||||
'00000500000000000000000000000000' # bootloader
|
||||
]
|
||||
for d in responses:
|
||||
lightingNodeProDevice.device.preload_read(Report(0, bytes.fromhex(d)))
|
||||
|
|
Loading…
Reference in New Issue