msi: support Coreliquid K360 and two similar variants (#564)
This commit is contained in:
parent
c1cb21e2e2
commit
912dd0ad61
|
@ -5,4 +5,6 @@
|
|||
/dist/
|
||||
__pycache__/
|
||||
liquidctl/_version.py
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
.coverage
|
||||
.vscode/
|
||||
|
|
|
@ -128,6 +128,7 @@ Device notes are sorted alphabetically, major (upper case) notes before minor
|
|||
| AIO liquid cooler | ~~[Corsair iCUE Elite Capellix H100i, H115i, H150i](docs/corsair-commander-core-guide.md)~~ | <sup>_Bp_</sup> | 1.11.1 |
|
||||
| AIO liquid cooler | [Corsair iCUE Elite RGB H100i, H150i](docs/corsair-platinum-pro-xt-guide.md) | | 1.13.0 |
|
||||
| AIO liquid cooler | [EVGA CLC 120 (CL12), 240, 280, 360](docs/asetek-690lc-guide.md) | <sup>_Z_</sup> | 1.9.1 |
|
||||
| AIO liquid cooler | [MSI MPG Coreliquid K360](docs/msi-mpg-coreliquid-guide.md) | <sup>_penx_</sup> | git |
|
||||
| AIO liquid cooler | [NZXT Kraken M22](docs/kraken-x2-m2-guide.md) | | 1.10.0 |
|
||||
| AIO liquid cooler | [NZXT Kraken X40, X60](docs/asetek-690lc-guide.md) | <sup>_LZ_</sup> | 1.9.1 |
|
||||
| AIO liquid cooler | [NZXT Kraken X31, X41, X61](docs/asetek-690lc-guide.md) | <sup>_LZ_</sup> | 1.9.1 |
|
||||
|
|
|
@ -0,0 +1,312 @@
|
|||
# MSI coreliquid AIO protocol
|
||||
|
||||
### Compatible devices
|
||||
|
||||
| Device Name | USB ID | LED channels | Fan channels |
|
||||
| ----------- | ------ | ------------ | ------------ |
|
||||
| MPG Coreliquid K360 | `ODB0:B130` | 1 | 5 |
|
||||
|
||||
### Command formats
|
||||
|
||||
Most of the communication with the K360 uses 64 byte HID reports. Lighting effect control uses a 185 byte feature report.
|
||||
Incoming and outgoing reports generally share the same size.
|
||||
Write commands start with a prefix of `0xD0`, and multi-byte numbers are little-endian, unless stated otherwise.
|
||||
|
||||
| Feature report number | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x52 | Get or set board data, notably lighting control |
|
||||
| 0xD0 | "get all hardware monitor data" (currently unused) |
|
||||
|
||||
It can be noted that the feature report for the board data seems to be designed to include information about all the leds connected to the motherboard.
|
||||
The driver only needs to set a small subset of the data in order to control the cpu cooler.
|
||||
|
||||
## Get General Information
|
||||
|
||||
### Get APROM Firmware version - `0xB0`
|
||||
|
||||
Request:
|
||||
|
||||
| Byte index | Value |
|
||||
| ---------- | ----------- |
|
||||
| 0x00 | 0x01 |
|
||||
| 0x01 | 0xB0 |
|
||||
| Fill | 0xCC |
|
||||
|
||||
Response:
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x02 | X |
|
||||
|
||||
Firmware version is `(X >> 4).(X & 0x0F)`
|
||||
|
||||
### Get LDROM Firmware version - `0xB6`
|
||||
|
||||
Request:
|
||||
|
||||
| Byte index | Value |
|
||||
| ---------- | ----------- |
|
||||
| 0x00 | 0x01 |
|
||||
| 0x01 | 0xB6 |
|
||||
| Fill | 0xCC |
|
||||
|
||||
Response:
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x02 | X |
|
||||
|
||||
Firmware version is `(X >> 4).(X & 0x0F)`
|
||||
|
||||
### Get screen Firmware version - `0xF1`
|
||||
|
||||
Response:
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x02 | Version number |
|
||||
|
||||
### Get device model index - `0xB1`
|
||||
|
||||
Request:
|
||||
|
||||
| Byte index | Value |
|
||||
| ---------- | ----------- |
|
||||
| Fill | 0xCC |
|
||||
|
||||
Response:
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x02 | Version number |
|
||||
|
||||
## Sending system information for fan control
|
||||
|
||||
Fan profiles are **NOT** controlled by any internal device temperature measurement.
|
||||
Instead, the device expects periodic reports of the CPU temperature, which it uses to interpolate
|
||||
fan speeds and to show on the screen.
|
||||
|
||||
### Set CPU status - `0x85`
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x01 | 0x85 |
|
||||
| 0x02-0x03 | cpu frequency (int, MHz) |
|
||||
| 0x01 | cpu temperature (int, C) |
|
||||
|
||||
|
||||
### Set GPU status - `0x86`
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x01 | 0x86 |
|
||||
| 0x02-0x03 | gpu memory frequency (int, MHz) |
|
||||
| 0x01 | gpu usage (int, %) |
|
||||
|
||||
|
||||
## Lighting effects
|
||||
|
||||
### Get all board data (lighting) - Feature report `0x52`
|
||||
|
||||
Response:
|
||||
|
||||
| Byte index | Description |
|
||||
|--------------------------|---------------------------------------------------------|
|
||||
| 0x1F | **Lighting mode** |
|
||||
| 0x20-0x22 | **RGB values for color1 in JRainbow1 area** |
|
||||
| 0x23 | **Bits 0-1: Speed (LOW, MEDIUM, HIGH)** |
|
||||
| 0x24 | **Bits 2-6: Brightness level (0-10)** |
|
||||
| 0x24-0x26 | **RGB values for color2 in JRainbow1 area** |
|
||||
| 0x27 | **Bit 7: Color selection (0: Rainbow, 1: User-defined)**|
|
||||
| 0x29 | Number of LEDs in JRainbow1 area |
|
||||
| 0x34 | Number of LEDs in JRainbow2 area |
|
||||
| 0x3D | Bit 0: Stripe (0) or Fan (1) selection |
|
||||
| 0x3D | Bits 1-3: Fan type (SP, HD, LL) |
|
||||
| 0x3E | Bits 2-7: Corsair device quantity (0-63) |
|
||||
| 0x3F | Number of LEDs in JCorsair area |
|
||||
| 0x48 | Bit 0: LL120 outer individual mode (0 or 1) |
|
||||
| 0x4E | Bit 7: Combined JRGB (True or False) |
|
||||
| 0x52 | Bit 0: Onboard sync (True or False) |
|
||||
| 0x52 | Bit 1: Combine JRainbow1 |
|
||||
| 0x52 | Bit 2: Combined JRainbow2 |
|
||||
| 0x52 | Bit 3: Combined JCorsair |
|
||||
| 0x52 | Bit 4: Combined JPipe1 |
|
||||
| 0x52 | Bit 5: Combined JPipe2 |
|
||||
| 0xB8 | **Save to device (0 or 1)** |
|
||||
|
||||
Ligthing effects are sent to the device by sending the feature report `0x52` with the desired data in the above format.
|
||||
|
||||
| Byte Value | Lighting Effect Name |
|
||||
|------------|---------------------------|
|
||||
| 0 | DISABLE |
|
||||
| 1 | NO_ANIMATION |
|
||||
| 2 | BREATHING |
|
||||
| 3 | FLASHING |
|
||||
| 4 | DOUBLE_FLASHING |
|
||||
| 5 | LIGHTNING |
|
||||
| 6 | MSI_MARQUEE |
|
||||
| 7 | METEOR |
|
||||
| 8 | WATER_DROP |
|
||||
| 9 | MSI_RAINBOW |
|
||||
| 10 | POP |
|
||||
| 11 | JAZZ |
|
||||
| 12 | PLAY |
|
||||
| 13 | MOVIE |
|
||||
| 14 | MARQUEE |
|
||||
| 15 | COLOR_RING |
|
||||
| 16 | PLANETARY |
|
||||
| 17 | DOUBLE_METEOR |
|
||||
| 18 | ENERGY |
|
||||
| 19 | BLINK |
|
||||
| 20 | CLOCK |
|
||||
| 21 | COLOR_PULSE |
|
||||
| 22 | COLOR_SHIFT |
|
||||
| 23 | COLOR_WAVE |
|
||||
| 24 | VISOR |
|
||||
| 25 | RAINBOW |
|
||||
| 26 | RAINBOW_WAVE |
|
||||
| 27 | VISOR |
|
||||
| 28 | JRAINBOW |
|
||||
| 29 | RAINBOW_FLASHING |
|
||||
| 30 | RAINBOW_DOUBLE_FLASHING |
|
||||
| 31 | RANDOM |
|
||||
| 32 | FAN_CONTROL |
|
||||
| 33 | DISABLE2 |
|
||||
| 34 | COLOR_RING_FLASHING |
|
||||
| 35 | COLOR_RING_DOUBLE_FLASHING|
|
||||
| 36 | STACK |
|
||||
| 37 | CORSAIR_IQUE |
|
||||
| 38 | FIRE |
|
||||
| 39 | LAVA |
|
||||
| 40 | END |
|
||||
|
||||
|
||||
## Fan control
|
||||
|
||||
### Fan temperature config - `0x33` (Get) or `0x41` (Set)
|
||||
|
||||
Format:
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x00 | 0xD0 |
|
||||
| 0x01 | 0x32/0x41 (response/write)|
|
||||
| 0x02-0x09 | Radiator fan 1 config |
|
||||
| 0x0A-0x11 | Radiator fan 2 config |
|
||||
| 0x12-0x19 | Radiator fan 3 config |
|
||||
| 0x01A-0x21 | Pump speed config |
|
||||
| 0x22-0x29 | Waterblock fan config |
|
||||
|
||||
A fan temperature config consists of 8 bit integer values:
|
||||
- Mode index
|
||||
- 7 temperature points in Celsius.
|
||||
|
||||
### Fan speed config - `0x32` (Get) or `0x40` (Set)
|
||||
|
||||
Format:
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x00 | 0xD0 |
|
||||
| 0x01 | 0x32/0x40 (response/write) |
|
||||
| 0x02-0x09 | Radiator fan 1 config |
|
||||
| 0x0A-0x11 | Radiator fan 2 config |
|
||||
| 0x12-0x19 | Radiator fan 3 config |
|
||||
| 0x01A-0x21 | Pump speed config |
|
||||
| 0x22-0x29 | Waterblock fan config |
|
||||
|
||||
A fan config consists of 8 bit integer values:
|
||||
- Mode index
|
||||
- 7 duty cycle percentage values.
|
||||
|
||||
### Get current fan status - `0x31`
|
||||
|
||||
Response:
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x00 | 0xD0 |
|
||||
| 0x01 | 0x31 |
|
||||
| 0x02-0x03 | Radiator fan 1 rpm |
|
||||
| 0x04-0x05 | Radiator fan 2 rpm |
|
||||
| 0x06-0x07 | Radiator fan 3 rpm |
|
||||
| 0x08-0x09 | Pump speed rpm |
|
||||
| 0x0A-0x0B | Waterblock fan rpm |
|
||||
| 0x16-0x17 | Radiator fan 1 duty % |
|
||||
| 0x18-0x19 | Radiator fan 2 duty % |
|
||||
| 0x1A-0x1B | Radiator fan 3 duty % |
|
||||
| 0x1C-0x1D | Pump speed duty % |
|
||||
| 0x1E-0x01F | Waterblock fan duty % |
|
||||
|
||||
## Display control
|
||||
|
||||
### Show Hardware Monitor - `0x71`
|
||||
|
||||
The device is capable of displaying a maximum of 3 different parameters, which will cycle on the display.
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x01 | 0x71 |
|
||||
| 0x02 | Show CPU frequency (0 or 1) |
|
||||
| 0x03 | Show CPU temperature (0 or 1) |
|
||||
| 0x04 | Show GPU memory frequency (0 or 1) |
|
||||
| 0x05 | Show GPU usage (0 or 1) |
|
||||
| 0x06 | Show pump (0 or 1)|
|
||||
| 0x07 | Show radiator fan (0 or 1) |
|
||||
| 0x08 | Show waterblock fan (0 or 1) |
|
||||
| 0x09 | How many radiator fan speeds to show separately (1 or 3) |
|
||||
|
||||
### Set User Message - `0x93`
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x01 | 0x93 |
|
||||
| 0x02-0x3E | Message bytes (ASCII) |
|
||||
| 0x3F | 0x20 |
|
||||
|
||||
### Set Clock Display - `0x7A`
|
||||
|
||||
Sets the clock display style on the OLED screen. `clock_style` determines the visual style of the clock.
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x01 | 0x7A |
|
||||
| 0x02 | `clock_style` (0, 1 or 2) |
|
||||
|
||||
### Set Brightness and Direction - `0x7E`
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x01 | 0x7E |
|
||||
| 0x02 | brightness (0-100) |
|
||||
| 0x03 | direction (0-3) |
|
||||
|
||||
## Image Upload Commands
|
||||
|
||||
### Upload Image - `0xC0` (GIF) or `0xD0` (Banner)
|
||||
|
||||
File uploads are initiated by a single report, after which the data is transferred in chunks of 60 bytes. Uploaded images must be 240x320 px size, and in the standard 24-bit color BMP format. A short sleep (2 seconds has proven safe) should be placed between the transfer initiation and start of the data transfer to make sure the device is ready.
|
||||
|
||||
**Transfer initiation report**
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x01 | 0xC0/0xD0 (GIF/Banner) |
|
||||
| 0x02-0x05 | file size to be transferred in bytes (uint32) |
|
||||
| 0x06 | Slot where the image is saved |
|
||||
|
||||
**Bulk transfer report**
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x01 | 0xC1/0xD1 (GIF/Banner) |
|
||||
| 0x02-0x3D | data chunk |
|
||||
| 0x3E-0x3F | 0x00 |
|
||||
|
||||
|
||||
### Get Image Checksum - `0xC2` (GIF) or `0xD2` (Banner)
|
||||
|
||||
Response:
|
||||
|
||||
| Byte index | Description |
|
||||
| ---------- | ----------- |
|
||||
| 0x02-0x03 | checksum value |
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
# MSI MPG Coreliquid AIO coolers
|
||||
_Driver API and source code available in [`liquidctl.driver.msi`](../liquidctl/driver/msi)._
|
||||
|
||||
_Currently, only the K360 model is experimentally supported as more testing and feedback is needed._
|
||||
|
||||
This driver is for the MSI MPG coreliquid series of AIO coolers, of which currently only the coreliquid K360 has been tested and verified to be working. The usage of speed profiles for this model requires external periodic updates of the current cpu temperature. As a result, to use variable fan speeds you must be careful to make sure that the current cpu temperature gets sent to the device. An example method to accomplish this is the `--use-device-controller` option in [`extra/yoda`](../extra/yoda). Device configuration, including lighting and fan profiles persist until a new configuration is sent to the device, but saved settings are lost after power is lost (power state S5). Uploaded display images are saved onto the device, and so they can be accessed by their uploaded type and index even after loss of power, preventing the need to repeatedly upload the same files. The lcd display resets to the default animation when the system is suspended (S3).
|
||||
|
||||
LED lighting is controlled via preset modes, which are sent once to the device as a configuration, after which the device then independently commands the LEDs until a new configuration is received.
|
||||
|
||||
The K360 model includes an LCD screen capable of displaying various preset animations, hardware status, ASCII banners with a preset or custom background image, and preset or custom images.
|
||||
|
||||
## Initialization
|
||||
|
||||
Controlling the device does not always require initialization, but some features, such as changing the display settings may not function before initialization. Initialization on its own will set default fan curves and LCD screen settings.
|
||||
|
||||
```
|
||||
# liquidctl initialize --pump-mode smart
|
||||
MSI MPG Coreliquid K360
|
||||
├── Display firmware version 2
|
||||
├── APROM firmware version 0
|
||||
├── LDROM firmware version 256
|
||||
├── Serial number A02020123456
|
||||
└── Pump mode smart
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
The AIO unit is able to report fan speeds, pump speed, water block speed, and duties.
|
||||
|
||||
```
|
||||
# liquidctl status
|
||||
MSI MPG Coreliquid K360
|
||||
├── Fan 1 speed 1546 rpm
|
||||
├── Fan 1 duty 60 %
|
||||
├── Fan 2 speed 1562 rpm
|
||||
├── Fan 2 duty 60 %
|
||||
├── Fan 3 speed 1530 rpm
|
||||
├── Fan 3 duty 60 %
|
||||
├── Water block speed 2400 rpm
|
||||
├── Water block duty 50 %
|
||||
├── Pump speed 2777 rpm
|
||||
├── Pump duty 100 %
|
||||
```
|
||||
## Fan and pump speeds
|
||||
|
||||
First, some important notes...
|
||||
|
||||
*You must carefully consider what pump and fan speeds to run. Heat output, case airflow, radiator size, installed fans and ambient temperature are some of the factors to take into account. Test your settings under different scenarios, and make sure that they are appropriate, correctly applied and persistent.*
|
||||
|
||||
*The device has no internal temperature measurement to control the fan speeds, and simply running a liquidctl command to set a speed profile will not persistently provide this necessary data to the device. You can use [`extra/yoda`](../extra/yoda) to communicate with the cooler, or create your own service to keep the device updated on the current temperature.*
|
||||
|
||||
*You should also consider monitoring your hardware temperatures and setting alerts for overheating components or pump failures.*
|
||||
|
||||
With those out of the way, the pump speed can be configured to a fixed duty value or with a profile dependent on a (temperature) signal that MUST be periodically sent to the device.
|
||||
|
||||
Fixed speeds can be set by specifying the desired channel and duty value.
|
||||
|
||||
```
|
||||
# liquidctl set pump speed 90
|
||||
```
|
||||
|
||||
| Channel | Minimum duty | Maximum duty |
|
||||
| --- | -- | --- |
|
||||
| `pump` | 60% | 100% |
|
||||
| radiator fans (`fans`, `fan1`, `fan2`, `fan3`) | 20% | 100% | |
|
||||
| `waterblock-fan` | 0% | 100% | |
|
||||
|
||||
For profiles, one or more temperature–duty pairs are supplied instead of single value.
|
||||
|
||||
```
|
||||
# liquidctl set pump speed 20 30 30 50 34 80 40 90 50 100
|
||||
^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^^
|
||||
pairs of temperature (°C) -> duty (%)
|
||||
```
|
||||
|
||||
liquidctl will normalize and optimize this profile before pushing it to the device. Adding `--verbose` will trace the final profile that is being applied.
|
||||
|
||||
The device also has preset pump/fan curves that can be applied independently for each channel with [`yoda`](../extra/yoda). Perhaps most notable is the "smart" mode, which enables fan-stop for two of the three radiator fans. Fan-stop is locked by the device for custom fan profiles, likely to prevent the liquid from overheating.
|
||||
|
||||
The preset device profiles are:
|
||||
|
||||
- Silent
|
||||
- Balance
|
||||
- Game
|
||||
- Default
|
||||
- Smart
|
||||
|
||||
The preset, named modes are supported in the driver and they currently have experimental support in [`yoda`](../extra/yoda), support in the liquidctl cli is on the way.
|
||||
|
||||
## RGB lighting with LEDs
|
||||
|
||||
LEDs on the device are always synced with the same effect, so the only supported channel argument is "sync".
|
||||
|
||||
Colors can be specified in RGB, HSV or HSL (see [Supported color specification formats](../README.md#supported-color-specification-formats)). Each animation mode supports zero to two colors, and some animation modes include an additional "rainbow" mode.
|
||||
|
||||
Lighting effect speed can be controlled with the `--speed` parameter, which can vary between 0 and 3.
|
||||
|
||||
```
|
||||
# liquidctl set sync color clock aa00aa 00aaaa --speed 0
|
||||
# liquidctl set sync color steady ffb6c1
|
||||
```
|
||||
|
||||
Some lighting modes are intended to react to the sounds currently playing on the system. These modes do not currently function as intended with this driver.
|
||||
|
||||
| Mode | Colors | Rainbow option | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `disable` | None | None | |
|
||||
| `steady` | One | No | |
|
||||
| `blink` | ? | ? | Not working as intended |
|
||||
| `breathing` | One | No | Yes |
|
||||
| `clock` | Two | Yes | |
|
||||
| `color pulse` | ? | ? | Not working as intended |
|
||||
| `color ring` | None | None | |
|
||||
| `color ring double flashing` | None | None | |
|
||||
| `color ring flashing` | None | None | |
|
||||
| `color shift` | None | None | Not working as intended |
|
||||
| `color wave` | Two | Yes | |
|
||||
| `corsair ique` | ? | ? | Unclear |
|
||||
| `disable2` | None | None | |
|
||||
| `double flashing` | One | Yes | |
|
||||
| `double meteor` | None | None | |
|
||||
| `energy` | None | None | |
|
||||
| `fan control` | ? | ? | Not working as intended |
|
||||
| `fire` | Two | No | |
|
||||
| `flashing` | One | Yes | |
|
||||
| `jazz` | ? | ? | Not working as intended |
|
||||
| `jrainbow` | ? | ? | Unclear |
|
||||
| `lava` | ? | ? | Not working as intended |
|
||||
| `lightning` | One | No | |
|
||||
| `marquee` | One | No | |
|
||||
| `meteor` | One | Yes | |
|
||||
| `movie` | ? | ? | Not working as intended |
|
||||
| `msi marquee` | One | Yes | |
|
||||
| `msi rainbow` | ? | ? | Not working as intended |
|
||||
| `planetary` | None | None | |
|
||||
| `play` | ? | ? | Not working as intended |
|
||||
| `pop` | ? | ? | Not working as intended |
|
||||
| `rainbow` | ? | ? | Very jittery and slow, rainbow wave is recommended instead |
|
||||
| `rainbow double flashing` | None | None | |
|
||||
| `rainbow flashing` | None | None | |
|
||||
| `rainbow wave` | None | None | |
|
||||
| `random` | None | None | |
|
||||
| `rap` | ? | ? | Not working as intended |
|
||||
| `stack` | One | Yes | |
|
||||
| `visor` | Two | Yes | |
|
||||
| `water drop` | One | Yes | |
|
||||
|
||||
Support for the rainbow option is not yet exposed by the liquidctl cli, but it is included in the driver as the color_selection flag.
|
||||
|
||||
```
|
||||
>>> from liquidctl.driver import find_liquidctl_devices
|
||||
>>> coreliquid_device = next(find_liquidctl_devices())
|
||||
>>> coreliquid_device.connect()
|
||||
>>> coreliquid_device.set_colors("sync", "clock", [], color_selection=0)
|
||||
```
|
||||
|
||||
## The LCD screen
|
||||
|
||||
The screen resolution is 320 x 240 px, and custom images uploaded with this driver are resized to fit this requirement. The screen orientation and brightness (0-100) can also be controlled. The only channel available for the K360 model is "lcd".
|
||||
|
||||
Uploading and choosing which images or banner backgrounds to display is experimentally supported, sometimes an uploaded file may be viewable from a different index on the device than the one it was originally uploaded to.
|
||||
|
||||
```
|
||||
# liquidctl set lcd screen settings '80;0'
|
||||
# liquidctl set lcd screen hardware 'cpu_temp;cpu_freq;fan_radiator'
|
||||
```
|
||||
```
|
||||
# liquidctl set lcd screen banner '1;4;A cool message in 62 characters!;/home/username/Pictures/pic.jpg'
|
||||
# liquidctl set lcd screen banner '0;1;Hello, MSI?'
|
||||
# liquidctl set lcd screen banner '1;4;I can also show a previously uploaded background!'
|
||||
```
|
||||
|
||||
```
|
||||
# liquidctl set lcd screen disable
|
||||
```
|
||||
|
||||
Maximum length of the displayed banner mesages is 62 ASCII characters. hardware status display functionality is limited, as the displayed data must be communicated to the device. This functionality is implemented in the driver, but currently its usage is limited to yoda, which is gpu-unaware so the gpu_freq and gpu_usage parameters will not display correct information without custom update services.
|
||||
|
||||
| mode name | action | options |
|
||||
| --- | --- | --- |
|
||||
| hardware | set the screen to display hardware info | up to 3 semicolon delimited keys from the available sensors |
|
||||
| image | set the screen to display a custom or preset image | \<type (0=preset,1=custom)\>;\<index (0-5)\>[;\<filename\>] |
|
||||
| banner | set the screen to display a message with custom or preset image as background | \<type (0=preset,1=custom)\>;\<index (0-3)\>;\<message\>[;\<filename\>]
|
||||
| clock | set the screen to display system time (requires control service to send the time to the device) | integer between 0 and 2 to specify the style of the clock display |
|
||||
| settings | set the screen brightness and orientation | \<brightness (0-100)\>;\<direction (0-3)\> |
|
||||
| disable | disables the lcd screen | |
|
||||
|
||||
The index field of image and banner modes denotes which of the device-stored images to show on the display, or to which save slot to store an uploaded image in. Use type=0 to display one of the preset animations, and type=1 to upload or display a custom image.
|
||||
|
||||
| Display orientation | value |
|
||||
| --- | --- |
|
||||
| Default (up) | 0 |
|
||||
| Right | 1 |
|
||||
| Down | 2 |
|
||||
| Left | 3 |
|
||||
|
||||
|
||||
| Displayed sensor data | Notes |
|
||||
| --- | --- |
|
||||
| cpu_freq | |
|
||||
| cpu_temp | this is the sensor value that controls set profile fan duties |
|
||||
| gpu_freq | Used by the manufacturer to display gpu memory frequency |
|
||||
| gpu_usage | |
|
||||
| fan_pump | |
|
||||
| fan_radiator | |
|
||||
| fan_cpumos | waterblock fan speed |
|
|
@ -464,6 +464,9 @@ SUBSYSTEMS=="usb", ATTRS{idVendor}=="048d", ATTRS{idProduct}=="5702", TAG+="uacc
|
|||
# Gigabyte RGB Fusion 2.0 8297 Controller
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="048d", ATTRS{idProduct}=="8297", TAG+="uaccess"
|
||||
|
||||
# MSI MPG Coreliquid K360
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0db0", ATTRS{idProduct}=="b130", TAG+="uaccess"
|
||||
|
||||
# NZXT E500
|
||||
SUBSYSTEMS=="usb", ATTRS{idVendor}=="7793", ATTRS{idProduct}=="5911", TAG+="uaccess"
|
||||
|
||||
|
|
140
extra/yoda
140
extra/yoda
|
@ -14,6 +14,11 @@ Profiles are specified as comma-separated lists of `(temperature,duty)` pairs.
|
|||
For example: `(20,50),(40,65),(40,65),(50,100)` specifies a duty of 65% at
|
||||
40°C. The profile will be linearly interpolated between the specified points.
|
||||
|
||||
In device controlled mode, sets the device internal control profile and periodically
|
||||
sends sensor data, but the device will independently control duty cycles to match the temperature.
|
||||
Named profiles implemented by the device manufacturer, such as "silent", "game", "smart",
|
||||
are only available in device controlled mode.
|
||||
|
||||
Escape sequences or appropriate single or double quotes should be employed to
|
||||
escape characters that are reserved by the shell in use (e.g. in the case of
|
||||
bash, the parenthesis and any optional whitespace). In practice, wrapping the
|
||||
|
@ -24,6 +29,7 @@ Examples:
|
|||
yoda --match grid control fan1 with "(20,20),(35,100)" on nct6793.systin
|
||||
yoda --match kraken show-sensors
|
||||
yoda --match kraken control pump with "(20,50),(50,100)" on istats.cpu and fan with "(20,25),(34,100)" on _internal.liquid
|
||||
yoda --match msi control pump with "smart" on coretemp.package_id_0 and fans with "silent" on coretemp.package_id_0
|
||||
|
||||
Usage:
|
||||
yoda [options] show-sensors
|
||||
|
@ -45,6 +51,7 @@ Options:
|
|||
-v, --verbose Output additional information
|
||||
-g, --debug Show debug information on stderr
|
||||
--legacy-690lc Use Asetek 690LC in legacy mode (old Krakens)
|
||||
--use-device-controller Use the control loop integrated to the device (MPG coreliquid device)
|
||||
--version Display the version number
|
||||
--help Show this message
|
||||
|
||||
|
@ -65,16 +72,18 @@ Copyright Jonas Malaco and contributors
|
|||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""
|
||||
|
||||
|
||||
import ast
|
||||
import logging
|
||||
import math
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from docopt import docopt
|
||||
import liquidctl.cli as _borrow
|
||||
from liquidctl.util import normalize_profile, interpolate_profile
|
||||
from liquidctl.driver import *
|
||||
import liquidctl.driver
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
import re
|
||||
|
@ -109,6 +118,7 @@ def read_sensors(device):
|
|||
for label, current, _, _ in li:
|
||||
sensor_name = label.lower().replace(' ', '_')
|
||||
sensors[f'{m}.{sensor_name}'] = current
|
||||
sensors['cpu_freq'] = psutil.cpu_freq().current
|
||||
return sensors
|
||||
|
||||
|
||||
|
@ -120,9 +130,12 @@ def show_sensors(device):
|
|||
print('{:<70} {:>6}{}'.format(k, v, '°C'))
|
||||
|
||||
|
||||
def parse_profile(arg, mintemp=0, maxtemp=100, minduty=0, maxduty=100):
|
||||
def parse_profile(arg, mintemp=0, maxtemp=100, minduty=0, maxduty=100, str_allowed=False):
|
||||
"""Parse, validate and normalize a temperature–duty profile.
|
||||
|
||||
>>> parse_profile('smart', 0, 60, 25, 100)
|
||||
'smart'
|
||||
|
||||
>>> parse_profile('(20,30),(30,50),(34,80),(40,90)', 0, 60, 25, 100)
|
||||
[(20, 30), (30, 50), (34, 80), (40, 90), (60, 100)]
|
||||
>>> parse_profile('35', 0, 60, 25, 100)
|
||||
|
@ -155,33 +168,92 @@ def parse_profile(arg, mintemp=0, maxtemp=100, minduty=0, maxduty=100):
|
|||
|
||||
"""
|
||||
try:
|
||||
val = ast.literal_eval('[' + arg + ']')
|
||||
if len(val) == 1 and isinstance(val[0], int):
|
||||
# for arg == '<number>' set fixed duty between mintemp and maxtemp - 1
|
||||
val = [(mintemp, val[0]), (maxtemp - 1, val[0])]
|
||||
if arg in liquidctl.driver.msi.MpgCooler.BUILTIN_MODES.keys() and str_allowed:
|
||||
return arg
|
||||
else:
|
||||
val = ast.literal_eval('[' + arg + ']')
|
||||
if len(val) == 1 and isinstance(val[0], int):
|
||||
# for arg == '<number>' set fixed duty between mintemp and maxtemp - 1
|
||||
val = [(mintemp, val[0]), (maxtemp - 1, val[0])]
|
||||
except:
|
||||
raise ValueError('profile must be comma-separated (temperature, duty) tuples')
|
||||
msg = (
|
||||
'profile must be comma-separated (temperature, duty) tuples or '
|
||||
+ f'recognised cooler mode string {str(liquidctl.driver.msi.MpgCooler.BUILTIN_MODES.keys())}'
|
||||
)
|
||||
raise ValueError(msg)
|
||||
for step in val:
|
||||
if not isinstance(step, tuple) or len(step) != 2:
|
||||
raise ValueError('profile must be comma-separated (temperature, duty) tuples')
|
||||
temp, duty = step
|
||||
if not isinstance(temp, int) or temp < mintemp or temp > maxtemp:
|
||||
raise ValueError('temperature must be integer between {} and {}'.format(mintemp, maxtemp))
|
||||
raise ValueError(
|
||||
'temperature must be integer between {} and {}'.format(mintemp, maxtemp)
|
||||
)
|
||||
if not isinstance(duty, int) or duty < minduty or duty > maxduty:
|
||||
raise ValueError('duty must be integer between {} and {}'.format(minduty, maxduty))
|
||||
return normalize_profile(val, critx=maxtemp)
|
||||
|
||||
|
||||
def auto_control(device, channels, profiles, sensors, update_interval):
|
||||
"""Communicate sensor data directly with the device.
|
||||
|
||||
Implemented for use with the MSI coreliquid AIO.
|
||||
Allows compatible devices to utilize their internal control loop
|
||||
to determine appropriate fan speeds for the CPU temperature.
|
||||
"""
|
||||
assert getattr(
|
||||
device, 'HAS_AUTOCONTROL', False
|
||||
), f'No registered control loop capability for device {device}!'
|
||||
from datetime import datetime
|
||||
|
||||
device.set_profiles(channels, profiles)
|
||||
|
||||
assert all(
|
||||
s == sensors[0] for s in sensors
|
||||
), 'Controlling different channels with different sensors not possible with device control'
|
||||
sensor = sensors[0]
|
||||
|
||||
LOGGER.info('starting...')
|
||||
failures = 0
|
||||
while True:
|
||||
try:
|
||||
sensor_data = read_sensors(device)
|
||||
temp = sensor_data[sensor]
|
||||
freq = sensor_data.get('cpu_freq', 0)
|
||||
|
||||
device.set_time(datetime.now())
|
||||
device.set_hardware_status(
|
||||
temp,
|
||||
cpu_f=freq,
|
||||
gpu_f=sensor_data.get('gpu_freq', 0),
|
||||
gpu_U=sensor_data.get('gpu_usage', 0),
|
||||
)
|
||||
failures = 0
|
||||
except Exception as err:
|
||||
failures += 1
|
||||
LOGGER.error(err)
|
||||
if failures >= MAX_FAILURES:
|
||||
LOGGER.critical('too many failures in a row: %d', failures)
|
||||
raise
|
||||
time.sleep(update_interval)
|
||||
|
||||
|
||||
def control(device, channels, profiles, sensors, update_interval):
|
||||
LOGGER.info('device: %s on bus %s and address %s', device.description, device.bus, device.address)
|
||||
LOGGER.info(
|
||||
'device: %s on bus %s and address %s', device.description, device.bus, device.address
|
||||
)
|
||||
for channel, profile, sensor in zip(channels, profiles, sensors):
|
||||
LOGGER.info('channel: %s following profile %s on %s', channel, str(profile), sensor)
|
||||
|
||||
averages = [None] * len(channels)
|
||||
cutoff_freq = 1 / update_interval / 10
|
||||
alpha = 1 - math.exp(-2 * math.pi * cutoff_freq)
|
||||
LOGGER.info('update interval: %d s; cutoff frequency (low-pass): %.2f Hz; ema alpha: %.2f',
|
||||
update_interval, cutoff_freq, alpha)
|
||||
LOGGER.info(
|
||||
'update interval: %d s; cutoff frequency (low-pass): %.2f Hz; ema alpha: %.2f',
|
||||
update_interval,
|
||||
cutoff_freq,
|
||||
alpha,
|
||||
)
|
||||
|
||||
try:
|
||||
# more efficient and safer API, but only supported by very few devices
|
||||
|
@ -206,9 +278,25 @@ def control(device, channels, profiles, sensors, update_interval):
|
|||
|
||||
# interpolate on sensor ema and apply corresponding duty
|
||||
duty = interpolate_profile(profile, ema)
|
||||
LOGGER.info('%s control: lpf(%s) = lpf(%.1f°C) = %.1f°C => duty := %d%%',
|
||||
channel, sensor, sample, ema, duty)
|
||||
LOGGER.info(
|
||||
'%s control: lpf(%s) = lpf(%.1f°C) = %.1f°C => duty := %d%%',
|
||||
channel,
|
||||
sensor,
|
||||
sample,
|
||||
ema,
|
||||
duty,
|
||||
)
|
||||
apply_duty(channel, duty)
|
||||
if getattr(device, 'NEEDS_TIME', False):
|
||||
device.set_time(datetime.now())
|
||||
if getattr(device, 'NEEDS_HWSTATUS', False):
|
||||
device.set_hardware_status(
|
||||
sensor_data[sensors[0]],
|
||||
cpu_f=sensor_data.get('cpu_freq', 0),
|
||||
gpu_f=sensor_data.get('gpu_freq', 0),
|
||||
gpu_U=sensor_data.get('gpu_usage', 0),
|
||||
)
|
||||
|
||||
failures = 0
|
||||
except Exception as err:
|
||||
failures += 1
|
||||
|
@ -222,6 +310,7 @@ def control(device, channels, profiles, sensors, update_interval):
|
|||
if __name__ == '__main__':
|
||||
if len(sys.argv) == 2 and sys.argv[1] == 'doctest':
|
||||
import doctest
|
||||
|
||||
doctest.testmod(verbose=True)
|
||||
sys.exit(0)
|
||||
|
||||
|
@ -231,6 +320,7 @@ if __name__ == '__main__':
|
|||
args['--verbose'] = True
|
||||
logging.basicConfig(level=logging.DEBUG, format='[%(levelname)s] %(name)s: %(message)s')
|
||||
import liquidctl.version
|
||||
|
||||
LOGGER.debug('yoda v%s', VERSION)
|
||||
LOGGER.debug('liquidctl v%s', liquidctl.version.__version__)
|
||||
elif args['--verbose']:
|
||||
|
@ -241,9 +331,11 @@ if __name__ == '__main__':
|
|||
sys.tracebacklimit = 0
|
||||
|
||||
frwd = _borrow._make_opts(args)
|
||||
selected = list(find_liquidctl_devices(**frwd))
|
||||
selected = list(liquidctl.driver.find_liquidctl_devices(**frwd))
|
||||
if len(selected) > 1:
|
||||
raise SystemExit('too many devices, filter or select one. See liquidctl --help and yoda --help.')
|
||||
raise SystemExit(
|
||||
'too many devices, filter or select one. See liquidctl --help and yoda --help.'
|
||||
)
|
||||
elif len(selected) == 0:
|
||||
raise SystemExit('no devices matches available drivers and selection criteria')
|
||||
|
||||
|
@ -253,8 +345,22 @@ if __name__ == '__main__':
|
|||
if args['show-sensors']:
|
||||
show_sensors(device)
|
||||
elif args['control']:
|
||||
control(device, args['<channel>'], list(map(parse_profile, args['<profile>'])),
|
||||
args['<sensor>'], update_interval=int(args['--interval']))
|
||||
if args['--use-device-controller']:
|
||||
auto_control(
|
||||
device,
|
||||
args['<channel>'],
|
||||
list(map(lambda p: parse_profile(p, str_allowed=True), args['<profile>'])),
|
||||
args['<sensor>'],
|
||||
update_interval=int(args['--interval']),
|
||||
)
|
||||
else:
|
||||
control(
|
||||
device,
|
||||
args['<channel>'],
|
||||
list(map(parse_profile, args['<profile>'])),
|
||||
args['<sensor>'],
|
||||
update_interval=int(args['--interval']),
|
||||
)
|
||||
else:
|
||||
raise Exception('nothing to do')
|
||||
except KeyboardInterrupt:
|
||||
|
|
62
liquidctl.8
62
liquidctl.8
|
@ -376,6 +376,68 @@ Channel Mode #colors (Platinum) #colors (Pro XT/Elite RGB) #colors (Platinum SE)
|
|||
\fIled\fR \fIsuper\-fixed\fR 24 16 48
|
||||
.TE
|
||||
.
|
||||
.SS MSI MPG Coreliquid k360
|
||||
Cooling channels: \fIfan1\fR, \fIfan2\fR, \fIfan3\fR, \fIfans\fR, \fIwaterblock-fan\fR, \fIpump\fR.
|
||||
.PP
|
||||
Lighting channels: \fIsync\fR.
|
||||
.PP
|
||||
LCD screens: \fIlcd\fR.
|
||||
.TS
|
||||
l c
|
||||
----
|
||||
l c
|
||||
.
|
||||
Mode #colors
|
||||
\fIdisable\fR 0
|
||||
\fIsteady\fR 1
|
||||
\fIblink\fR 0
|
||||
\fIbreathing\fR 1
|
||||
\fIclock\fR 2
|
||||
\fIcolor pulse\fR 0
|
||||
\fIcolor ring\fR 0
|
||||
\fIcolor ring double flashing\fR 0
|
||||
\fIcolor ring flashing\fR 0
|
||||
\fIcolor shift\fR 0
|
||||
\fIcolor wave\fR 2
|
||||
\fIcorsair ique\fR 0
|
||||
\fIdisable2\fR 0
|
||||
\fIdouble flashing\fR 1
|
||||
\fIdouble meteor\fR 0
|
||||
\fIenergy\fR 0
|
||||
\fIfan control\fR 0
|
||||
\fIfire\fR 2
|
||||
\fIflashing\fR 1
|
||||
\fIjazz\fR 0
|
||||
\fIjrainbow\fR 0
|
||||
\fIlava\fR 0
|
||||
\fIlightning\fR 1
|
||||
\fImarquee\fR 1
|
||||
\fImeteor\fR 1\(en2
|
||||
\fImovie\fR 0
|
||||
\fImsi marquee\fR 1
|
||||
\fImsi rainbow\fR 0
|
||||
\fIplanetary\fR 0
|
||||
\fIplay\fR 0
|
||||
\fIpop\fR 0
|
||||
\fIrainbow\fR 0
|
||||
\fIrainbow double flashing\fR 0
|
||||
\fIrainbow flashing\fR 0
|
||||
\fIrainbow wave\fR 0
|
||||
\fIrandom\fR 0
|
||||
\fIrap\fR 0
|
||||
\fIstack\fR 1
|
||||
\fIvisor\fR 2
|
||||
\fIwater drop\fR 1
|
||||
.TE
|
||||
.PP
|
||||
When applicable the animation speed can be set with
|
||||
.BI \-\-speed= value ,
|
||||
where the allowed values are: \fI0\fR, \fI1\fR and \fI2\fR.
|
||||
The LCD screen settings can be controlled with the
|
||||
\fBsettings\fR flag, that requires the arguments "\fIbrightness\fR;\fIdirection\fR"
|
||||
where an allowed brightness is an integer in the range \fI0-100\fR,
|
||||
and orientations \fI0-3\fR correspond to the orientations up, right, down and left.
|
||||
.
|
||||
.SS NZXT Kraken X40, X60
|
||||
.SS NZXT Kraken X31, X41, X61
|
||||
Supports the same modes and options as a Corsair Hydro H80i GT (or similar), but
|
||||
|
|
|
@ -34,6 +34,7 @@ from liquidctl.driver import corsair_hid_psu
|
|||
from liquidctl.driver import hydro_platinum
|
||||
from liquidctl.driver import kraken2
|
||||
from liquidctl.driver import kraken3
|
||||
from liquidctl.driver import msi
|
||||
from liquidctl.driver import nzxt_epsu
|
||||
from liquidctl.driver import rgb_fusion2
|
||||
from liquidctl.driver import smart_device
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,332 @@
|
|||
# uses the psf/black style
|
||||
|
||||
from struct import pack
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from _testutils import MockHidapiDevice, Report
|
||||
|
||||
from liquidctl.driver.msi import MpgCooler, _REPORT_LENGTH, _DEFAULT_FEATURE_DATA, _LightingMode
|
||||
from liquidctl.error import UnsafeFeaturesNotEnabled
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mpgCoreLiquidK360Device():
|
||||
description = "Mock MPG CoreLiquid K360"
|
||||
device = _MockCoreLiquid(vendor_id=0xFFFF, product_id=0xB130)
|
||||
dev = MpgCooler(device, description)
|
||||
|
||||
dev.connect()
|
||||
return dev
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mpgCoreLiquidDeviceExperimental():
|
||||
_, pid, desc, kwargs = MpgCooler._MATCHES[-1]
|
||||
description = "Mock " + desc
|
||||
unsafe = kwargs["unsafe"]
|
||||
device = _MockCoreLiquid(vendor_id=0xFFFF, product_id=pid)
|
||||
dev = MpgCooler(device, description, unsafe=unsafe)
|
||||
|
||||
dev.connect(unsafe=unsafe)
|
||||
return dev
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mpgCoreLiquidK360DeviceInvalid():
|
||||
description = "Mock MPG CoreLiquid K360"
|
||||
device = _MockCoreLiquidInvalid(vendor_id=0xFFFF, product_id=0xB130)
|
||||
dev = MpgCooler(device, description)
|
||||
|
||||
dev.connect()
|
||||
return dev
|
||||
|
||||
|
||||
class _MockCoreLiquid(MockHidapiDevice):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._fan_configs = (4, 20, 40, 50, 60, 70, 80, 90)
|
||||
self._fan_temp_configs = (4, 30, 40, 50, 60, 70, 80, 90)
|
||||
self._model_idx = 255 # TODO: check the correct model index from device
|
||||
self._feature_data = Report(_DEFAULT_FEATURE_DATA[0], _DEFAULT_FEATURE_DATA[1:])
|
||||
|
||||
self.preload_read(self._feature_data)
|
||||
|
||||
def write(self, data):
|
||||
reply = bytearray(_REPORT_LENGTH)
|
||||
reply[0:2] = data[0:2]
|
||||
if list(data[:2]) == [0x01, 0xB1]: # get current model idx request
|
||||
reply[2] = self._model_idx
|
||||
elif list(data[:2]) == [0xD0, 0x31]: # get status request
|
||||
reply[2:23] = (
|
||||
pack("<h", 496)
|
||||
+ pack("<h", 517)
|
||||
+ pack("<h", 509)
|
||||
+ pack("<h", 1045)
|
||||
+ pack("<h", 1754)
|
||||
+ bytearray([0, 0, 0, 0, 0x7D, 0, 0x7D, 0, 0, 0])
|
||||
+ pack("<h", 20)
|
||||
+ pack("<h", 20)
|
||||
+ pack("<h", 20)
|
||||
+ pack("<h", 20)
|
||||
+ pack("<h", 50)
|
||||
)
|
||||
self.preload_read(Report(0, reply))
|
||||
elif list(data[:2]) == [0xD0, 0x32]: # get fan config request
|
||||
for i in (2, 10, 18, 26, 34):
|
||||
reply[i : i + 10] = self._fan_configs
|
||||
self.preload_read(Report(0, reply))
|
||||
elif list(data[:2]) == [0xD0, 0x33]: # get fan T config request
|
||||
reply[1] = 0x32
|
||||
for i in (2, 10, 18, 26, 34):
|
||||
reply[i : i + 10] = self._fan_temp_configs
|
||||
self.preload_read(Report(0, reply))
|
||||
elif list(data[1:3]) == [0xB0, 0xCC]: # get ldrom fw
|
||||
self.preload_read(Report(0, reply))
|
||||
elif list(data[1:3]) == [0xB6, 0xCC]: # get aprom fw
|
||||
self.preload_read(Report(0, reply))
|
||||
elif data[1] == 0xF1: # get screen fw
|
||||
self.preload_read(Report(0, reply))
|
||||
return super().write(data)
|
||||
|
||||
|
||||
class _MockCoreLiquidInvalid(MockHidapiDevice):
|
||||
def read(self, length):
|
||||
buf = bytearray([0xD0, 0x32] + (62 * [0]))
|
||||
return buf[:length]
|
||||
|
||||
|
||||
def test_mpg_core_liquid_k360_initializes(mpgCoreLiquidK360Device):
|
||||
mpgCoreLiquidK360Device.initialize()
|
||||
|
||||
writes = len(mpgCoreLiquidK360Device.device.sent)
|
||||
assert (
|
||||
writes
|
||||
== 9
|
||||
# 1 get fan config,
|
||||
# 2 get fan T config,
|
||||
# 3 get aprom fw,
|
||||
# 4 get ldrom fw,
|
||||
# 5 get display fw,
|
||||
# 6 set screen settings,
|
||||
# 7 set fan config,
|
||||
# 8 set fan T config,
|
||||
# 9 set safe control temperature
|
||||
)
|
||||
|
||||
|
||||
def test_mpg_core_liquid_k360_get_status(mpgCoreLiquidK360Device):
|
||||
dev = mpgCoreLiquidK360Device
|
||||
(
|
||||
fan1,
|
||||
fan1d,
|
||||
fan2,
|
||||
fan2d,
|
||||
fan3,
|
||||
fan3d,
|
||||
wbfan,
|
||||
wbfand,
|
||||
pump,
|
||||
pumpd,
|
||||
) = dev.get_status()
|
||||
assert fan1[1] == 496
|
||||
assert fan1d[1] == 20
|
||||
assert fan2[1] == 517
|
||||
assert fan2d[1] == 20
|
||||
assert fan3[1] == 509
|
||||
assert fan3d[1] == 20
|
||||
assert wbfan[1] == 1045
|
||||
assert wbfand[1] == 20
|
||||
assert pump[1] == 1754
|
||||
assert pumpd[1] == 50
|
||||
assert dev.device.sent[-1].number == 0xD0
|
||||
assert dev.device.sent[-1].data[0] == 0x31
|
||||
|
||||
|
||||
def test_mpg_core_liquid_k360_get_status_invalid_read(mpgCoreLiquidK360DeviceInvalid):
|
||||
dev = mpgCoreLiquidK360DeviceInvalid
|
||||
with pytest.raises(AssertionError):
|
||||
dev.get_status()
|
||||
|
||||
|
||||
def test_mpg_core_liquid_k360_set_fixed_speeds(mpgCoreLiquidK360Device):
|
||||
mpgCoreLiquidK360Device.set_fixed_speed("pump", 65)
|
||||
|
||||
fan_report, T_report = mpgCoreLiquidK360Device.device.sent[-2:]
|
||||
assert fan_report.data[33:41] == [3] + [65] * 7
|
||||
|
||||
|
||||
def test_mpg_core_liquid_k360_set_speed_profile(mpgCoreLiquidK360Device):
|
||||
duties = [20, 30, 34, 40, 50]
|
||||
temps = [30, 50, 80, 90, 100]
|
||||
curve_profile = zip(duties, temps)
|
||||
|
||||
mpgCoreLiquidK360Device.set_speed_profile("fans", curve_profile)
|
||||
|
||||
fan_report, T_report = mpgCoreLiquidK360Device.device.sent[-3:-1]
|
||||
|
||||
# fan 1
|
||||
assert fan_report.data[1:9] == [3] + duties + [0] * 2
|
||||
assert T_report.data[1:9] == [3] + temps + [0] * 2
|
||||
# fan 2
|
||||
assert fan_report.data[9:17] == [3] + duties + [0] * 2
|
||||
assert T_report.data[9:17] == [3] + temps + [0] * 2
|
||||
# fan 3
|
||||
assert fan_report.data[17:25] == [3] + duties + [0] * 2
|
||||
assert T_report.data[17:25] == [3] + temps + [0] * 2
|
||||
|
||||
|
||||
def test_mpg_core_liquid_k360_set_color(mpgCoreLiquidK360Device):
|
||||
colors = [[255, 255, 0], [0, 255, 255]]
|
||||
mode = "clock"
|
||||
speed = 2
|
||||
brightness = 5
|
||||
use_color = 1
|
||||
|
||||
mpgCoreLiquidK360Device.set_color(
|
||||
"sync", mode, colors, speed=speed, brightness=brightness, color_selection=use_color
|
||||
)
|
||||
report = mpgCoreLiquidK360Device.device.sent[-1]
|
||||
|
||||
assert report.data[30] == 20 # mode
|
||||
assert report.data[34] == (brightness << 2) | speed # brightness, speed
|
||||
assert report.data[31:34] == colors[0]
|
||||
assert report.data[35:38] == colors[1]
|
||||
|
||||
|
||||
def test_mpg_core_liquid_k360_not_totally_broken(mpgCoreLiquidK360Device):
|
||||
"""Reasonable calls to untested APIs do not raise exceptions"""
|
||||
dev = mpgCoreLiquidK360Device
|
||||
dev.initialize()
|
||||
_ = dev.get_status()
|
||||
|
||||
profile = ((0, 30), (25, 40), (60, 60), (100, 75))
|
||||
dev.set_speed_profile("pump", profile)
|
||||
dev.set_fixed_speed("waterblock-fan", 42)
|
||||
dev.set_screen("lcd", "image", "0;4")
|
||||
dev.set_screen("lcd", "banner", "1;0;Hello, world")
|
||||
dev.set_screen("lcd", "disable", "")
|
||||
dev.set_screen("lcd", "clock", "0")
|
||||
dev.set_screen("lcd", "hardware", "cpu_temp;cpu_freq")
|
||||
|
||||
|
||||
def test_mpg_core_liquid_k360_set_clock(mpgCoreLiquidK360Device):
|
||||
time = datetime(2012, 12, 21, 9, 54, 20)
|
||||
|
||||
mpgCoreLiquidK360Device.set_time(time)
|
||||
|
||||
report = mpgCoreLiquidK360Device.device.sent[-1]
|
||||
assert report.data[:8] == [131, 12, 12, 21, 4, 9, 54, 20]
|
||||
|
||||
|
||||
def test_mpg_core_liquid_k360_set_hw_status(mpgCoreLiquidK360Device):
|
||||
cpu_freq = 3500.0
|
||||
cpu_T = 54.0
|
||||
gpu_f = 7000
|
||||
|
||||
mpgCoreLiquidK360Device.set_hardware_status(cpu_T, cpu_f=cpu_freq, gpu_f=gpu_f)
|
||||
|
||||
cpu_report = mpgCoreLiquidK360Device.device.sent[-2]
|
||||
assert cpu_report.data[:5] == [133, 172, 13, 54, 0]
|
||||
|
||||
gpu_report = mpgCoreLiquidK360Device.device.sent[-1]
|
||||
assert gpu_report.data[:5] == [134, 88, 27, 0, 0]
|
||||
|
||||
|
||||
def test_unsafe_core_liquid_get_status(mpgCoreLiquidDeviceExperimental):
|
||||
status = mpgCoreLiquidDeviceExperimental.get_status()
|
||||
assert status == []
|
||||
|
||||
status = mpgCoreLiquidDeviceExperimental.get_status(unsafe=["other"])
|
||||
assert status == []
|
||||
|
||||
status = mpgCoreLiquidDeviceExperimental.get_status(unsafe=["experimental_coreliquid_cooler"])
|
||||
assert mpgCoreLiquidDeviceExperimental.device.sent[-1].number == 0xD0
|
||||
|
||||
|
||||
def test_unsafe_core_liquid_set_fixed_speed(mpgCoreLiquidDeviceExperimental):
|
||||
with pytest.raises(UnsafeFeaturesNotEnabled):
|
||||
mpgCoreLiquidDeviceExperimental.set_fixed_speed("pump", 65)
|
||||
|
||||
with pytest.raises(UnsafeFeaturesNotEnabled):
|
||||
mpgCoreLiquidDeviceExperimental.set_fixed_speed("pump", 65, unsafe=["other"])
|
||||
|
||||
mpgCoreLiquidDeviceExperimental.set_fixed_speed(
|
||||
"pump", 65, unsafe=["experimental_coreliquid_cooler"]
|
||||
)
|
||||
|
||||
|
||||
def test_unsafe_core_liquid_set_speed_profile(mpgCoreLiquidDeviceExperimental):
|
||||
duties = [20, 30, 34, 40, 50]
|
||||
temps = [30, 50, 80, 90, 100]
|
||||
curve_profile = zip(duties, temps)
|
||||
with pytest.raises(UnsafeFeaturesNotEnabled):
|
||||
mpgCoreLiquidDeviceExperimental.set_speed_profile("fans", curve_profile)
|
||||
|
||||
with pytest.raises(UnsafeFeaturesNotEnabled):
|
||||
mpgCoreLiquidDeviceExperimental.set_speed_profile("fans", curve_profile, unsafe=["other"])
|
||||
|
||||
mpgCoreLiquidDeviceExperimental.set_speed_profile(
|
||||
"fans", curve_profile, unsafe=["experimental_coreliquid_cooler"]
|
||||
)
|
||||
|
||||
|
||||
def test_unsafe_core_liquid_set_color(mpgCoreLiquidDeviceExperimental):
|
||||
colors = [[255, 255, 0], [0, 255, 255]]
|
||||
mode = "clock"
|
||||
speed = 2
|
||||
brightness = 5
|
||||
use_color = 1
|
||||
|
||||
with pytest.raises(UnsafeFeaturesNotEnabled):
|
||||
mpgCoreLiquidDeviceExperimental.set_color(
|
||||
"sync", mode, colors, speed=speed, brightness=brightness, color_selection=use_color
|
||||
)
|
||||
|
||||
with pytest.raises(UnsafeFeaturesNotEnabled):
|
||||
mpgCoreLiquidDeviceExperimental.set_color(
|
||||
"sync",
|
||||
mode,
|
||||
colors,
|
||||
speed=speed,
|
||||
brightness=brightness,
|
||||
color_selection=use_color,
|
||||
unsafe=["other"],
|
||||
)
|
||||
|
||||
mpgCoreLiquidDeviceExperimental.set_color(
|
||||
"sync",
|
||||
mode,
|
||||
colors,
|
||||
speed=speed,
|
||||
brightness=brightness,
|
||||
color_selection=use_color,
|
||||
unsafe=["experimental_coreliquid_cooler"],
|
||||
)
|
||||
|
||||
|
||||
def test_unsafe_core_liquid_set_clock(mpgCoreLiquidDeviceExperimental):
|
||||
time = datetime(2012, 12, 21, 9, 54, 20)
|
||||
|
||||
with pytest.raises(UnsafeFeaturesNotEnabled):
|
||||
mpgCoreLiquidDeviceExperimental.set_time(time)
|
||||
|
||||
with pytest.raises(UnsafeFeaturesNotEnabled):
|
||||
mpgCoreLiquidDeviceExperimental.set_time(time, unsafe=["other"])
|
||||
|
||||
mpgCoreLiquidDeviceExperimental.set_time(time, unsafe=["experimental_coreliquid_cooler"])
|
||||
|
||||
|
||||
def test_unsafe_core_liquid_set_hw_status(mpgCoreLiquidDeviceExperimental):
|
||||
cpu_freq = 3500.0
|
||||
cpu_T = 54.0
|
||||
|
||||
with pytest.raises(UnsafeFeaturesNotEnabled):
|
||||
mpgCoreLiquidDeviceExperimental.set_hardware_status(cpu_T, cpu_f=cpu_freq)
|
||||
|
||||
with pytest.raises(UnsafeFeaturesNotEnabled):
|
||||
mpgCoreLiquidDeviceExperimental.set_hardware_status(cpu_T, cpu_f=cpu_freq, unsafe=["other"])
|
||||
|
||||
mpgCoreLiquidDeviceExperimental.set_hardware_status(
|
||||
cpu_T, cpu_f=cpu_freq, unsafe=["experimental_coreliquid_cooler"]
|
||||
)
|
Loading…
Reference in New Issue