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

Corsair Link H80i #129

Closed
ehaupt opened this issue Jun 6, 2020 · 9 comments
Closed

Corsair Link H80i #129

ehaupt opened this issue Jun 6, 2020 · 9 comments
Labels
new device Support for a new device

Comments

@ehaupt
Copy link

ehaupt commented Jun 6, 2020

I've been successfully using audiohacked/OpenCorsairLink by @audiohacked on FreeBSD for quite some time.

I've just noticed that the project is now archived and retired. Luckily the README.md mentioned this wonderful project so I thought I'd give it a go.

I made sure all the dependencies are installed and I ran liquidctl list as root. Unfortunately my Corsair Link H80i is not recognized.

I followed the instructions and ran the command with PYUSB_DEBUG and LIBUSB_DEBUG:

$ export PYUSB_DEBUG=debug
$ export LIBUSB_DEBUG=4
$ liquidctl list
2020-06-06 18:35:27,503 DEBUG:usb.backend.libusb1:_LibUSB.__init__(<CDLL 'libusb.so.3', handle 801c82000 at 0x802516c50>)
DEBUG: _LibUSB.__init__(<CDLL 'libusb.so.3', handle 801c82000 at 0x802516c50>)
2020-06-06 18:35:27,503 INFO:usb.core:find(): using backend "usb.backend.libusb1"
INFO: find(): using backend "usb.backend.libusb1"
2020-06-06 18:35:27,504 DEBUG:usb.backend.libusb1:_LibUSB.enumerate_devices()
DEBUG: _LibUSB.enumerate_devices()
2020-06-06 18:35:27,504 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534d90>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534d90>)
2020-06-06 18:35:27,504 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534dd0>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534dd0>)
2020-06-06 18:35:27,504 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
2020-06-06 18:35:27,505 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x80207f1d0>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x80207f1d0>)
2020-06-06 18:35:27,505 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802527d10>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802527d10>)
2020-06-06 18:35:27,505 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534d90>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534d90>)
2020-06-06 18:35:27,505 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
2020-06-06 18:35:27,505 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x80207f1d0>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x80207f1d0>)
2020-06-06 18:35:27,505 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802527d10>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802527d10>)
2020-06-06 18:35:27,505 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534d90>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534d90>)
2020-06-06 18:35:27,506 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
2020-06-06 18:35:27,506 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x80207f1d0>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x80207f1d0>)
2020-06-06 18:35:27,506 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802527d10>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802527d10>)
2020-06-06 18:35:27,506 DEBUG:usb.backend.libusb1:_LibUSB._finalize_object()
DEBUG: _LibUSB._finalize_object()
2020-06-06 18:35:27,506 DEBUG:usb.backend.libusb1:_LibUSB.__init__(<CDLL 'libusb.so.3', handle 801c82000 at 0x802516c50>)
DEBUG: _LibUSB.__init__(<CDLL 'libusb.so.3', handle 801c82000 at 0x802516c50>)
2020-06-06 18:35:27,506 INFO:usb.core:find(): using backend "usb.backend.libusb1"
INFO: find(): using backend "usb.backend.libusb1"
2020-06-06 18:35:27,506 DEBUG:usb.backend.libusb1:_LibUSB.enumerate_devices()
DEBUG: _LibUSB.enumerate_devices()
2020-06-06 18:35:27,506 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534c50>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534c50>)
2020-06-06 18:35:27,507 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534dd0>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534dd0>)
2020-06-06 18:35:27,507 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534d90>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x802534d90>)
2020-06-06 18:35:27,507 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x8018a2a10>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x8018a2a10>)
2020-06-06 18:35:27,507 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
2020-06-06 18:35:27,507 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x80207f1d0>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x80207f1d0>)
2020-06-06 18:35:27,507 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
2020-06-06 18:35:27,507 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x8018a2a10>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x8018a2a10>)
2020-06-06 18:35:27,508 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
2020-06-06 18:35:27,508 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801785750>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801785750>)
2020-06-06 18:35:27,508 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
2020-06-06 18:35:27,508 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x80207f1d0>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x80207f1d0>)
2020-06-06 18:35:27,508 DEBUG:usb.backend.libusb1:_LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
DEBUG: _LibUSB.get_device_descriptor(<usb.backend.libusb1._Device object at 0x801543510>)
2020-06-06 18:35:27,508 DEBUG:usb.backend.libusb1:_LibUSB._finalize_object()
DEBUG: _LibUSB._finalize_object()

Here is the --debug output of OpenCorsairLink that might provide some insight:

$ /usr/local/bin/OpenCorsairLink --debug 
Checking USB device 0 (0000:0000)...
Checking USB device 1 (0000:0000)...
Checking USB device 2 (0000:0000)...
Checking USB device 3 (0000:0000)...
Checking USB device 4 (8087:0024)...
Checking USB device 5 (8087:0024)...
Checking USB device 6 (1b1c:0c04)...
Corsair product detected. Checking if device is H80... 
---- Packet dump: -----------------------------
81 07 3b 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
-----------------------------------------------
No (device_id 0x3B)
Corsair product detected. Checking if device is Cooling Node... 
---- Packet dump: -----------------------------
82 07 3b 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
-----------------------------------------------
No (device_id 0x3B)
Corsair product detected. Checking if device is Lighting Node... 
---- Packet dump: -----------------------------
83 07 3b 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
-----------------------------------------------
No (device_id 0x3B)
Corsair product detected. Checking if device is H100... 
---- Packet dump: -----------------------------
84 07 3b 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
-----------------------------------------------
No (device_id 0x3B)
Corsair product detected. Checking if device is H80i... 
---- Packet dump: -----------------------------
85 07 3b 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
-----------------------------------------------
Dev=0, CorsairLink Device Found: H80i!
Checking USB device 7 (1a40:0101)...
Checking USB device 8 (046a:0011)...
Checking USB device 9 (046d:c52b)...
Checking USB device 10 (05e3:0608)...
Checking USB device 11 (046d:0a29)...
Checking USB device 12 (05e3:0608)...

DEBUG: scan done, start routines
DEBUG: selected device_number = -1

Here is some info to my system:

$ usbconfig  | grep -i corsair
ugen1.3: <Corsair Memory, Inc. Integrated USB Bridge> at usbus1, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (100mA)
# usbconfig -d ugen1.3 dump_device_desc
ugen1.3: <Corsair Memory, Inc. Integrated USB Bridge> at usbus1, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (100mA)

  bLength = 0x0012 
  bDescriptorType = 0x0001 
  bcdUSB = 0x0200 
  bDeviceClass = 0x0000  <Probed by interface class>
  bDeviceSubClass = 0x0000 
  bDeviceProtocol = 0x0000 
  bMaxPacketSize0 = 0x0040 
  idVendor = 0x1b1c 
  idProduct = 0x0c04 
  bcdDevice = 0x0200 
  iManufacturer = 0x0001  <Corsair Memory, Inc.>
  iProduct = 0x0002  <Integrated USB Bridge>
  iSerialNumber = 0x0000  <no string>
  bNumConfigurations = 0x0001 

Here are the module/python versions:

python37-3.7.7
py37-docopt-0.6.2_1
py37-hidapi-0.9.0
py37-usb-1.0.2

Operating system:

FreeBSD 12.1-RELEASE-p5 amd64

Please let me know if there is any additional information I can provide.

@jonasmalacofilho
Copy link
Member

The non-GT/non-v2 H80i (1b1c:0c04; made by CoolIT) isn't currently supported in liquidctl. But it should be fairly straightforward to port the relevant OCL code to liquidctl.

I would very much welcome a pull request here. If you're willing to work on this, I can you give you directions and help you along the way.

@jonasmalacofilho jonasmalacofilho added help wanted new device Support for a new device labels Jun 6, 2020
@jonasmalacofilho jonasmalacofilho changed the title Corsair Link H80i not recognized on FreeBSD Corsair Link H80i Jun 6, 2020
@ehaupt
Copy link
Author

ehaupt commented Jun 6, 2020

Hello @jonasmalacofilho

I would very much welcome a pull request here. If you're willing to work on this, I can you give you directions and help you along the way.

Thank you for the offer. I'd love to work on this.

@ehaupt
Copy link
Author

ehaupt commented Jun 6, 2020

I started by adding the product id: ehaupt@eb6eb86

This gives me:

$ liquidctl list
Device ID 0: Corsair H80i (non-GT/non-v2, made by CoolIT) (experimental)

But obviously more is needed:

$ liquidctl status
ERROR: Unexpected error with Corsair H80i (non-GT/non-v2, made by CoolIT) (experimental)
ValueError: 0 is not a valid OCPMode

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
ValueError: 0 is not a valid OCPMode

liquidctl-status-stderr.log

@jonasmalacofilho
Copy link
Member

I'll take a better look at the OCL code and give you some specific pointers.

In the mean time, you're mostly on the right track, I just (strongly) suggest you work on a clear/new subclass of UsbHidDriver, instead of reusing CorsairHidPsuDriver. You don't want to send random PSU commands to the cooler.

@audiohacked
Copy link

@ehaupt @jonasmalacofilho I'm available to answer any questions you may have.

@ehaupt
Copy link
Author

ehaupt commented Jun 6, 2020

Thank you @audiohacked and @jonasmalacofilho

@jonasmalacofilho
Copy link
Member

jonasmalacofilho commented Jun 7, 2020

This comment became Porting drivers from OpenCorsairLink.

Original comment Hi again @ehaupt,

In essence, writing a new liquidctl driver means implementing all (suitable) methods of a liquidctl.base.BaseDriver.

Note that you shouldn't directly subclass the BaseDriver; instead you'll inherit from a bus-specific base driver like liquidctl.usb.UsbDriver or liquidctl.usb.UsbHidDevice, which will already include default implementations for many methods and properties.

And for the new driver to work out-of-the-box it's sufficient to import its module in liquidctl/driver/__init__.py.

Next, in order to port a driver from OCL, the first step is to check the corsair_device_info struct that matches the device, which defines the low-level and driver (protocol) functions used for it in OCL, besides a few other important parameters.

    {
        .vendor_id = 0x1b1c,
        .product_id = 0x0c04,
        .device_id = 0x3b,
        .name = "H80i",
        .read_endpoint = 0x01 | LIBUSB_ENDPOINT_IN,
        .write_endpoint = 0x00 | LIBUSB_ENDPOINT_OUT,
        .driver = &corsairlink_driver_coolit,
        .lowlevel = &corsairlink_lowlevel_coolit,
        .led_control_count = 1,
        .fan_control_count = 4,
        .pump_index = 5,
    },

in device.c

The low-level functions

Starting with the low-level functions specified by corsairlink_lowlevel_coolit, and implemented in lowlevel/coolit.c: the equivalence between these and the methods in a liquidctl driver is:

  • init -> connect (in some cases and/or initialize)
  • deinit -> disconnect
  • read/write -> self.device.read/self.device.write (see next paragraphs)

This is a HID device, so the liquidctl driver should inherit liquidctl.usb.UsbHidDriver, meaning that in the driver self.device will be a liquidctl.usb.HidapiDevice. Additionally, liquidctl already automatically handles how to write to a HID, but does so mimicking hidapi; HidapiDevice.write follows the specification:

The first byte of data[] must contain the Report ID. For devices which only support a single report, this must be set to 0x0. The remaining bytes contain the report data. Since the Report ID is mandatory, calls to hid_write() will always contain one more byte than the report contains.
from hidapi/hidapi.h

Practically, it means that you only need to implement init and deinit, and that in the translated driver, when OCL would call corsairlink_coolit_write with [byte1, byte2, byte3, ...], you'll instead call self.device.write with [0x00, byte1, byte2, byte3, ...] (note the prepended 0x00 byte)

Higher-level functionality

The remaining get_status, set_fixed_speed, set_speed_profile and set_color methods (required by BaseDriver) will encapsulate the functionality specified by corsairlink_driver_coolit (implemented in protocol/coolit/*.c), and are for the most part what users will access through the CLI.

Data that is read from the cooler, like the pump speed, will generally go into get_status. The firmware version is an exception in this case: it's read with a specific command (instead of being part of other replies), and so it belongs in the output of initialize.

(You can fetch the firmware version directly in initialize or, if you need to use it anywhere else, you read it and cache it in connect, and only return the cached value in initialize.)

The other three methods are self-explanatory and should be fairly straightforward to implement, apart from the special considerations that I go into next.

Protocols with interdependent messages

A big aspect in the design of the liquidctl CLI was not requiring the user to configure different aspects of the cooler in a single command: you should be able to set the pump speed without resetting the fan speed or the LED colors.

For most devices there's a clear mapping between the CLI and the implementation: the CLI command set <channel> speed <fixed duty> implemented with set_fixed_speed won't depend on other BaseDriver methods (apart from connect and disconnect).

There are however "complicated" devices where, at the protocol level, functionality is grouped (all channels must be set at once) or even completely consolidated into a single "state" (everything must be reset when changing a single parameter). Messages can also be required to follow an arbitrary order.

So, besides looking at how each individual parameter is configured, you also need to check the "logic" part of OCL, in this case implemented in hydro_coolit_settings. This doesn't mean that all OCL devices will fall into the "complicated" category, or that you'll necessarily need to match that order exactly.

In fact, in the case of the H80i (or other devices using the same protocol) I think that the different aspects of the cooler can indeed be configured independently, at least for the most part.

This is mostly due to the empty implementations of init and deinit: in more complex cases these functions usually involve some type of opening and closing of a "transaction", but there's nothing of the sort here.

The ordering in hydro_coolit_settings also seems to be strictly due to natural requirements (you need to know how many sensors there are before reading them), instead of being totally arbitrary. But I could be wrong...

Anyway, the main concern I have right now is the CommandId byte that's sent in every message.
It starts at 0x81 and is continually incremented. On one hand it clearly doesn't need to be a perfect sequence number (as OCL doesn't guarantee that in multiple invocations), but on the other the shorter message chains in liquidctl (due to only a few parameters being read or changed at a time) could cause the cooler to complain.

I'd start following OCL: initialize a similar variable to 0x81 every time the driver is instantiated, and increment it every time it's used. But if that somehow doesn't work, you can use the internal keyval API (example usage) to temporarily persist it to disk, allowing you to implement a true (wrapping) sequence number across liquidctl invocations.

No matter what, just don't forget to explicitly wrap CommandId it at 255, you'll probably be using a normal Python integer instead of a u8.

Advanced driver binding

liquidctl driver don't normally need to check anything super special to know whether or not they are compatible with a particular device. As long as SUPPORTED_DEVICES lists the compatible USB vendor and product IDs, besides any additional parameters required by __init__, the bus-specific base driver will do the rest.

This wont be the case with the H80i: it shares a common vendor and product ID with other devices, and is only differentiated by a "device ID", that has to be explicitly read. Reading of this device ID is implemented in OCL by corsairlink_coolit_device_id.

There are two ways of handling this in liquidctl. One way is to override probe (implemented in UsbHidDriver) to fetch the device ID, filter out any unknown IDs, and (only) yield driver instances that have as field a know ID; each instance should also map that ID to the corresponding parameters for that device (description, fan count, pump index, etc.). Another way is to have a generic driver that only fetches the ID and customizes itself accordingly at connect time, meaning that before that it identifies itself as something like "Undetermined Corsair device".

Because having the driver instance in an undetermined state will cause some issues, both for us and for the user, I think you should try the probe method first.


This is it, at least for now. Let me know if you have any questions.

P.S. I tried to be a bit more general than needed here because i want to be able to refer to this comment in case other drivers are ported from OCL.

@ehaupt
Copy link
Author

ehaupt commented Jun 9, 2020

Thank you @jonasmalacofilho for the detailed instructions. I'll try my luck.

@jonasmalacofilho
Copy link
Member

Merging this with #142, since both should use the exact same driver. Keeping the later issue simply because there's some additional information there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new device Support for a new device
Projects
None yet
Development

No branches or pull requests

3 participants