From 0be2ccc3aec4f7cb6cd37c085082116dfa906df8 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Tue, 27 Jan 2026 19:13:39 -0500 Subject: [PATCH 01/46] hwmon: (corsair-hydro-platinum) Add driver for Corsair H150i Elite RGB Adds a new hwmon driver `corsair-hydro-platinum` to support Corsair AIO coolers using the "Hydro Platinum" protocol (e.g., H100i/H150i Elite RGB). This device uses a distinct protocol from the "Commander Core" based devices, communicating via 64-byte HID reports. Technical Implementation Details: 1. Write Operations (Control Transfer): Standard `hid_hw_output_report` fails with -38 (ENOSYS) as the device lacks an Interrupt OUT endpoint. Commands are instead sent via `hid_hw_raw_request` using `HID_REQ_SET_REPORT` (Control Transfer) over Endpoint 0. 2. Packet Structure: To avoid -32 (EPIPE) stalls during Control Transfers, the firmware requires a 65-byte padded buffer structure: [0x00] (Report ID padding) + [0x3F] (Command Prefix) + Payload. 3. Asynchronous Reporting: Status reports are received asynchronously via standard HID Input Reports (raw_event). `hid_device_io_start()` is explicitly called in probe to ensure these reports are delivered to the driver when using `HID_CONNECT_HIDRAW`. 4. Protocol Split (Fan 3 Quirk): The main cooling command (Feature 0x00) only supports 2 fans + Pump. For 360mm models (H150i/Elite), a second command (Feature 0x03) must be sent to control the 3rd fan. Command ordering is critical: Feature 0x00 MUST be sent before Feature 0x03 to avoid device stalls. Features: - Monitoring: - Liquid Temperature. - Pump Speed and Duty Cycle. - Fan Speeds and Duty Cycles (up to 3 fans). - Control (PWM): - Pump Mode Control (Quiet/Balanced/Extreme) via pwm1. - Fan Speed Control (0-100% Duty Cycle) via pwm[2-4]. - Initialization logic defaults fans to 50% to prevent startup noise. - Attempted Robustness: - CRC-8 verification on all received reports. - Synchronous transaction logic (Write + Wait for Report) to ensure data validity. - Exposes human-readable model name (e.g., "Corsair iCUE H150i Elite RGB") via standard `label` sysfs attribute. Signed-off-by: Jack Greiner --- drivers/hwmon/Makefile | 2 +- drivers/hwmon/corsair-hydro-platinum.c | 749 +++++++++++++++++++++++++ drivers/hwmon/dkms.conf.in | 3 + 3 files changed, 753 insertions(+), 1 deletion(-) create mode 100644 drivers/hwmon/corsair-hydro-platinum.c diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile index 64207a8..611e77c 100644 --- a/drivers/hwmon/Makefile +++ b/drivers/hwmon/Makefile @@ -1 +1 @@ -obj-m := nzxt-kraken2.o nzxt-grid3.o nzxt-kraken3.o nzxt-smart2.o +obj-m := nzxt-kraken2.o nzxt-grid3.o nzxt-kraken3.o nzxt-smart2.o corsair-hydro-platinum.o diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c new file mode 100644 index 0000000..0ca1bd2 --- /dev/null +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -0,0 +1,749 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * hwmon driver for Corsair Hydro Platinum / Pro XT / Elite RGB liquid coolers + * + * Supports monitoring of: + * - Liquid temperature + * - Pump speed and duty cycle + * - Fan speeds and duty cycles (up to 3 fans) + * + * Supports control of: + * - Pump mode (Quiet, Balanced, Extreme) + * - Fan duty cycle (0-100%) + * + * Devices supported: + * - Corsair Hydro H100i Platinum / SE (Untested) + * - Corsair Hydro H115i Platinum (Untested) + * - Corsair Hydro H60i / H100i / H115i / H150i Pro XT (Untested) + * - Corsair iCUE H100i / H115i / H150i Elite RGB (Tested) + * + * Technical Description: + * The device communicates via USB HID. Unlike standard HID devices, it requires + * commands to be sent via Control Transfers (Set Report, Endpoint 0). + * Status reports are received asynchronously via Input Reports on the Interrupt + * IN endpoint. + * + * Initialization: + * The device requires an initialization command (Set Cooling) to begin + * reporting status and to set the fans/pump to a safe default state. + * + * Copyright 2026 Jack Greiner + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if KERNEL_VERSION(6, 12, 0) <= LINUX_VERSION_CODE +#include +#else +#include +#endif + +#define DRIVER_NAME "corsair_hydro_platinum" + +/* USB Vendor/Product IDs */ +#define USB_VENDOR_ID_CORSAIR 0x1b1c + +/* Constants */ +#define REPORT_LENGTH 64 +#define RESPONSE_LENGTH 64 +#define STATUS_VALIDITY 1000 /* ms */ +#define MAX_FAN_COUNT 3 + +#define CMD_WRITE_PREFIX 0x3f +#define FEATURE_COOLING 0x00 /* Main Cooling: Pump + Fan 1 + Fan 2 */ +#define FEATURE_COOLING_FAN3 0x03 /* Extension: Fan 3 only (Main report is full) */ +#define CMD_GET_STATUS 0xff +#define CMD_SET_COOLING 0x14 + +/* Pump Modes */ +#define PUMP_MODE_QUIET 0x00 +#define PUMP_MODE_BALANCED 0x01 +#define PUMP_MODE_EXTREME 0x02 + +/* Fan Modes */ +#define FAN_MODE_CUSTOM_PROFILE 0x00 +#define FAN_MODE_FIXED_DUTY 0x02 +#define FAN_MODE_FIXED_RPM 0x04 + +/* Offsets in Cooling Payload (Cmd 0x14) */ +#define OFFSET_FAN1_MODE 8 +#define OFFSET_FAN1_DUTY 13 +#define OFFSET_FAN2_MODE 14 +#define OFFSET_FAN2_DUTY 19 +#define OFFSET_PUMP_MODE 20 +#define OFFSET_PROFILE_LEN 26 + +DECLARE_CRC8_TABLE(corsair_crc8_table); + +struct hydro_platinum_data { + struct hid_device *hdev; + struct device *hwmon_dev; + struct mutex lock; + u8 *buffer; + u8 sequence; + + /* Sensor values */ + u16 fan_speeds[MAX_FAN_COUNT]; + u8 fan_duty[MAX_FAN_COUNT]; + u16 pump_speed; + u8 pump_duty; + long liquid_temp; /* millidegrees C */ + + /* Control targets */ + u8 target_pump_mode; + u8 target_fan_mode[MAX_FAN_COUNT]; + u8 target_fan_duty[MAX_FAN_COUNT]; + + /* Detected configuration */ + int fan_count; + const char *model_name; + u8 fw_version[3]; + + struct completion wait_for_report; + + unsigned long updated; + bool valid; +}; + +/* Device Info Structs */ +struct hydro_platinum_device_info { + int fan_count; + const char *hwmon_name; + const char *model_name; +}; + +/* SMBus standard CRC-8 polynomial x^8 + x^2 + x + 1 (0x07) */ +static void hydro_platinum_init_crc(void) +{ + crc8_populate_msb(corsair_crc8_table, 0x07); +} + +/** + * hydro_platinum_send_command - Asynchronously send a command to the device. + * @priv: Driver data. + * @feature: Feature ID (e.g. FEATURE_COOLING). + * @command: Command ID. + * @data: Optional payload data. + * @data_len: Length of payload data. + * + * Constructs the report buffer with CRC and sends it via `hid_hw_raw_request`. + */ +static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feature, u8 command, u8 *data, int data_len) +{ + int ret; + int start_at; + + /* + * Construct 65-byte buffer with Report ID 0 padding. + * Some devices/firmware revisions require the alignment of 64-byte payload + * to be offset by the Report ID byte even in Control Transfers. + */ + memset(priv->buffer, 0, REPORT_LENGTH + 1); + + priv->buffer[0] = 0x00; /* Report ID Padding */ + priv->buffer[1] = CMD_WRITE_PREFIX; + + /* Sequence and feature/command logic */ + priv->sequence = (priv->sequence % 31) + 1; + priv->buffer[2] = (priv->sequence << 3) | feature; + priv->buffer[3] = command; + start_at = 4; + + if (data && data_len > 0) { + memcpy(priv->buffer + start_at, data, min(data_len, REPORT_LENGTH - start_at - 1)); + } + + /* Calculate CRC over buf[2] to buf[REPORT_LENGTH-1+1] */ + /* Payload is buf[1]..buf[64]. CRC is usually last byte of payload. */ + priv->buffer[REPORT_LENGTH] = crc8(corsair_crc8_table, priv->buffer + 2, REPORT_LENGTH - 2, 0); + + /* Send Report - 65 bytes */ + + /* Use HID_REQ_SET_REPORT (Control Transfer) */ + ret = hid_hw_raw_request(priv->hdev, 0 /* Report ID */, priv->buffer, REPORT_LENGTH + 1, + HID_OUTPUT_REPORT, HID_REQ_SET_REPORT); + + /* raw_request returns number of bytes written on success */ + if (ret > 0) ret = 0; + + return ret; +} + +/** + * hydro_platinum_transaction - Send a command and wait for a response. + * @priv: Driver data. + * @feature: Feature ID. + * @command: Command ID. + * @data: Optional payload data. + * @data_len: Length of payload data. + * + * Sends a command and waits up to 500ms for an Input Report on the Interrupt IN endpoint. + * This ensures strict command-response ordering to prevent device confusion. + */ +static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 feature, u8 command, u8 *data, int data_len) +{ + int ret; + + reinit_completion(&priv->wait_for_report); + + ret = hydro_platinum_send_command(priv, feature, command, data, data_len); + if (ret < 0) + return ret; + + ret = wait_for_completion_interruptible_timeout(&priv->wait_for_report, msecs_to_jiffies(500)); + if (ret == 0) + return -ETIMEDOUT; + else if (ret < 0) + return ret; + + /* + * CRC Verification + * liquidctl checks CRC over bytes [1..63] (assuming 64 byte report). + * If standard SMBus CRC-8 algorithm is used, checksumming (Data + CRC) should yield 0. + */ + if (crc8(corsair_crc8_table, priv->buffer + 1, REPORT_LENGTH - 1, 0) != 0) { + return -EIO; + } + + return 0; +} + +/** + * hydro_platinum_write_cooling - Commit target fan/pump settings to the device. + * @priv: Driver data. + * + * Generates the configuration payload based on `target_*` members and sends it. + * Handles the split between Main (Fan 1, 2, Pump) and Secondary (Fan 3) cooling features. + * Sends Main feature FIRST to ensure core cooling is applied, then Secondary. + */ +static int hydro_platinum_write_cooling(struct hydro_platinum_data *priv) +{ + int ret; + u8 data[60]; + + /* + * Construct the 60-byte payload for the Command 0x14. + * The payload is appended to the 4-byte header in hydro_platinum_send_command. + */ + + /* + * Payload Prefix: 00 ff 05 ff ff ff ff ff + */ + memset(data, 0, sizeof(data)); + data[0] = 0x00; + data[1] = 0xff; + data[2] = 0x05; + memset(data + 3, 0xff, 5); + + data[OFFSET_PROFILE_LEN] = 7; + + /* Pump Mode */ + data[OFFSET_PUMP_MODE] = priv->target_pump_mode; + + /* Fan 1 (Index 0) */ + if (priv->fan_count >= 1) { + data[OFFSET_FAN1_MODE] = priv->target_fan_mode[0]; + /* If fixed duty */ + if (priv->target_fan_mode[0] == FAN_MODE_FIXED_DUTY) + data[OFFSET_FAN1_DUTY] = priv->target_fan_duty[0]; + } + + /* Fan 2 (Index 1) */ + if (priv->fan_count >= 2) { + data[OFFSET_FAN2_MODE] = priv->target_fan_mode[1]; + if (priv->target_fan_mode[1] == FAN_MODE_FIXED_DUTY) + data[OFFSET_FAN2_DUTY] = priv->target_fan_duty[1]; + } + + /* Send Feature Cooling (Fan 1, 2, Pump) */ + ret = hydro_platinum_transaction(priv, FEATURE_COOLING, CMD_SET_COOLING, data, sizeof(data)); + if (ret) + return ret; + + /* + * Command Ordering Note: + * The device requires the "Main" cooling command (Feature 0x00) to be sent + * BEFORE the "Secondary" cooling command (Feature 0x03, for Fan 3). + * reversing this order may cause the device to stall or return -EPIPE. + */ + + /* Fan 3 (Index 2) - Requires Feature Cooling 2 */ + if (priv->fan_count >= 3) { + u8 data2[60]; + memcpy(data2, data, sizeof(data)); + + /* Ensure Pump Mode is set correctly even in secondary command */ + data2[OFFSET_PUMP_MODE] = priv->target_pump_mode; + + /* Reset Fan 1/2 slots */ + data2[OFFSET_FAN1_MODE] = 0; + data2[OFFSET_FAN1_DUTY] = 0; + data2[OFFSET_FAN2_MODE] = 0; + data2[OFFSET_FAN2_DUTY] = 0; + + /* Fan 3 goes into Fan 1 slot */ + data2[OFFSET_FAN1_MODE] = priv->target_fan_mode[2]; + if (priv->target_fan_mode[2] == FAN_MODE_FIXED_DUTY) + data2[OFFSET_FAN1_DUTY] = priv->target_fan_duty[2]; + + ret = hydro_platinum_transaction(priv, FEATURE_COOLING_FAN3, CMD_SET_COOLING, data2, sizeof(data2)); + if (ret) + hid_warn(priv->hdev, "Failed to set Fan 3 speed: %d\n", ret); + } + + return 0; +} + +static int hydro_platinum_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) +{ + struct hydro_platinum_data *priv = hid_get_drvdata(hdev); + + /* We only care about Input reports (Responses) */ + if (report->type != HID_INPUT_REPORT) + return 0; + + /* + * The driver buffer expects [0]=ReportID, [1]=Prefix. + * We treat the raw data as the payload. + */ + + /* Safety check size */ + if (size > REPORT_LENGTH + 16) size = REPORT_LENGTH + 16; + + memcpy(priv->buffer, data, size); + + complete(&priv->wait_for_report); + return 0; +} + +/** + * hydro_platinum_update - Fetch latest status from device. + * @priv: Driver data. + * + * Uses `CMD_GET_STATUS` to poll sensor data. + * Refresh rate limited by `STATUS_VALIDITY` (1s). + */ +static int hydro_platinum_update(struct hydro_platinum_data *priv) +{ + int ret; + + mutex_lock(&priv->lock); + + if (time_after(jiffies, priv->updated + msecs_to_jiffies(STATUS_VALIDITY)) || !priv->valid) { + + reinit_completion(&priv->wait_for_report); + + ret = hydro_platinum_transaction(priv, FEATURE_COOLING, CMD_GET_STATUS, NULL, 0); + if (ret < 0) + goto out; + + /* Data is now in priv->buffer (populated by raw_event) */ + + /* Firmware Version: res[2] (Major << 4 | Minor), res[3] (Patch) */ + if (!priv->valid) { + priv->fw_version[0] = priv->buffer[2] >> 4; + priv->fw_version[1] = priv->buffer[2] & 0x0f; + priv->fw_version[2] = priv->buffer[3]; + } + + /* Temp */ + priv->liquid_temp = ((int)priv->buffer[8] * 1000) + ((int)priv->buffer[7] * 1000 / 255); + + /* + * Parse Sensor Data: + * - Fan 1 Speed: Offset 14, Duty: Offset 15 + * - Fan 2 Speed: Offset 21, Duty: Offset 22 + * - Fan 3 Speed: Offset 42, Duty: Offset 43 + * - Pump Speed: Offset 28, Duty: Offset 29 + */ + + /* Pump (Base 28) */ + priv->pump_speed = get_unaligned_le16(priv->buffer + 28 + 1); + priv->pump_duty = priv->buffer[28]; + + /* Fan 1 (Base 14) */ + if (priv->fan_count >= 1) { + priv->fan_speeds[0] = get_unaligned_le16(priv->buffer + 14 + 1); + priv->fan_duty[0] = priv->buffer[14]; + } + + /* Fan 2 (Base 21) */ + if (priv->fan_count >= 2) { + priv->fan_speeds[1] = get_unaligned_le16(priv->buffer + 21 + 1); + priv->fan_duty[1] = priv->buffer[21]; + } + + /* Fan 3 (Base 42) */ + if (priv->fan_count >= 3) { + priv->fan_speeds[2] = get_unaligned_le16(priv->buffer + 42 + 1); + priv->fan_duty[2] = priv->buffer[42]; + } + + priv->updated = jiffies; + priv->valid = true; + } + ret = 0; + +out: + mutex_unlock(&priv->lock); + return ret; +} + +static umode_t hydro_platinum_is_visible(const void *data, enum hwmon_sensor_types type, u32 attr, int channel) +{ + const struct hydro_platinum_data *priv = data; + + switch (type) { + case hwmon_temp: + return 0444; + case hwmon_fan: + case hwmon_pwm: + /* Channel 0: Pump */ + /* Channel 1..N: Fans */ + if (channel == 0) return 0644; /* Pump (Mode control via PWM) */ + if (channel <= priv->fan_count) return 0644; /* Fans */ + return 0; + default: + return 0; + } +} + +static int hydro_platinum_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long *val) +{ + struct hydro_platinum_data *priv = dev_get_drvdata(dev); + int ret = hydro_platinum_update(priv); + if (ret < 0) return ret; + + switch (type) { + case hwmon_fan: + if (channel == 0) + *val = priv->pump_speed; + else + *val = priv->fan_speeds[channel - 1]; + break; + case hwmon_pwm: + if (channel == 0) + *val = priv->pump_duty; + else + *val = priv->fan_duty[channel - 1]; + break; + case hwmon_temp: + *val = priv->liquid_temp; + break; + default: + return -EOPNOTSUPP; + } + return 0; +} + +static int hydro_platinum_write(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long val) +{ + struct hydro_platinum_data *priv = dev_get_drvdata(dev); + int ret = 0; + int i; + + mutex_lock(&priv->lock); + + switch (type) { + case hwmon_pwm: + if (channel == 0) { + /* + * Pump Control: + * Map 0-255 PWM to discrete modes: + * 0-84: Quiet (Mode 0) + * 85-169: Balanced (Mode 1) + * 170-255: Extreme (Mode 2) + */ + u8 mode; + val = clamp_val(val, 0, 255); + + if (val < 85) mode = PUMP_MODE_QUIET; + else if (val < 170) mode = PUMP_MODE_BALANCED; + else mode = PUMP_MODE_EXTREME; + + priv->target_pump_mode = mode; + + } else { + /* Fan Control */ + /* Index is channel - 1 */ + i = channel - 1; + if (i >= priv->fan_count) { + ret = -EINVAL; + goto out; + } + + val = clamp_val(val, 0, 255); + priv->target_fan_duty[i] = (u8)val; + priv->target_fan_mode[i] = FAN_MODE_FIXED_DUTY; + } + + ret = hydro_platinum_write_cooling(priv); + break; + default: + ret = -EOPNOTSUPP; + break; + } + +out: + mutex_unlock(&priv->lock); + return ret; +} + + + +static const char *const hydro_platinum_fan_labels[] = { + "Pump", + "Fan 1", + "Fan 2", + "Fan 3" +}; + +static int hydro_platinum_read_string_impl(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, const char **str) +{ + switch (type) { + case hwmon_fan: + if (attr == hwmon_fan_label) { + if (channel < ARRAY_SIZE(hydro_platinum_fan_labels)) { + *str = hydro_platinum_fan_labels[channel]; + return 0; + } + } + break; + case hwmon_temp: + if (attr == hwmon_temp_label) { + *str = "Coolant temp"; + return 0; + } + break; + default: + break; + } + return -EOPNOTSUPP; +} + +static const struct hwmon_channel_info *hydro_platinum_info[] = { + HWMON_CHANNEL_INFO(fan, + HWMON_F_INPUT | HWMON_F_LABEL, /* Pump */ + HWMON_F_INPUT | HWMON_F_LABEL, /* Fan 1 */ + HWMON_F_INPUT | HWMON_F_LABEL, /* Fan 2 */ + HWMON_F_INPUT | HWMON_F_LABEL), /* Fan 3 */ + HWMON_CHANNEL_INFO(pwm, + HWMON_PWM_INPUT, /* Pump */ + HWMON_PWM_INPUT, /* Fan 1 */ + HWMON_PWM_INPUT, /* Fan 2 */ + HWMON_PWM_INPUT), /* Fan 3 */ + HWMON_CHANNEL_INFO(temp, + HWMON_T_INPUT | HWMON_T_LABEL), + NULL +}; + +static const struct hwmon_ops hydro_platinum_hwmon_ops = { + .is_visible = hydro_platinum_is_visible, + .read = hydro_platinum_read, + .write = hydro_platinum_write, + .read_string = hydro_platinum_read_string_impl, +}; + +static ssize_t hydro_platinum_label_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct hydro_platinum_data *priv = dev_get_drvdata(dev); + return sysfs_emit(buf, "%s\n", priv->model_name); +} +static DEVICE_ATTR(label, 0444, hydro_platinum_label_show, NULL); + +static struct attribute *hydro_platinum_attrs[] = { + &dev_attr_label.attr, + NULL +}; + +static const struct attribute_group hydro_platinum_group = { + .attrs = hydro_platinum_attrs, +}; + +static const struct attribute_group *hydro_platinum_groups[] = { + &hydro_platinum_group, + NULL +}; + +static const struct hwmon_chip_info hydro_platinum_chip_info = { + .ops = &hydro_platinum_hwmon_ops, + .info = hydro_platinum_info, +}; + +static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device_id *id) +{ + struct hydro_platinum_data *priv; + int ret; + int i; + + priv = devm_kzalloc(&hdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->hdev = hdev; + /* Buffer needs to be large enough + safety */ + priv->buffer = devm_kzalloc(&hdev->dev, REPORT_LENGTH + 16, GFP_KERNEL); + if (!priv->buffer) + return -ENOMEM; + + mutex_init(&priv->lock); + init_completion(&priv->wait_for_report); + hid_set_drvdata(hdev, priv); + + /* + * Retrieve device specific info from the ID table directly. + * This avoids a large switch statement and redundant data. + */ + const struct hydro_platinum_device_info *info = + (const struct hydro_platinum_device_info *)id->driver_data; + + if (!info) + return -ENODEV; + + priv->fan_count = info->fan_count; + priv->model_name = info->model_name; + + ret = hid_parse(hdev); + if (ret) + return ret; + + ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); + if (ret) + return ret; + + ret = hid_hw_open(hdev); + if (ret) + goto fail_and_stop; + + /* Enable HID input during probe (required for raw_event) */ + hid_device_io_start(hdev); + + /* Initialize CRC table */ + hydro_platinum_init_crc(); + + /* + * Initialize Device + * Default: Pump Balanced, Fans 50% + */ + priv->target_pump_mode = PUMP_MODE_BALANCED; + for (i = 0; i < MAX_FAN_COUNT; i++) { + priv->target_fan_mode[i] = FAN_MODE_FIXED_DUTY; + priv->target_fan_duty[i] = 128; /* 50% approx */ + } + + hid_info(hdev, "Initializing device (Set Cooling: Pump Balanced, Fans 50%%)...\n"); + ret = hydro_platinum_write_cooling(priv); + if (ret) { + hid_warn(hdev, "initialization command failed: %d\n", ret); + } + + /* Wait for response to init command. hydro_platinum_write_cooling handles the delay. */ + + /* Initial update to get firmware version */ + hid_info(hdev, "Requesting initial status update...\n"); + ret = hydro_platinum_update(priv); + if (ret) + hid_warn(hdev, "initial update failed: %d\n", ret); + else + hid_info(hdev, "Firmware version: %d.%d.%d\n", + priv->fw_version[0], priv->fw_version[1], priv->fw_version[2]); + + priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, info->hwmon_name, + priv, &hydro_platinum_chip_info, + hydro_platinum_groups); + if (IS_ERR(priv->hwmon_dev)) { + ret = PTR_ERR(priv->hwmon_dev); + goto fail_and_close; + } + + return 0; + +fail_and_close: + hid_hw_close(hdev); +fail_and_stop: + hid_hw_stop(hdev); + return ret; +} + +static void hydro_platinum_remove(struct hid_device *hdev) +{ + struct hydro_platinum_data *priv = hid_get_drvdata(hdev); + + hwmon_device_unregister(priv->hwmon_dev); + hid_hw_close(hdev); + hid_hw_stop(hdev); +} + +/* Driver Data used for Fan Count */ +#define HYDRO_PLATINUM_INFO(_fans, _name, _model) \ + static const struct hydro_platinum_device_info info_##_name = { \ + .fan_count = _fans, \ + .hwmon_name = #_name, \ + .model_name = _model \ + } + +HYDRO_PLATINUM_INFO(2, corsair_h100i_plat, "Corsair Hydro H100i Platinum"); +HYDRO_PLATINUM_INFO(2, corsair_h100i_plat_se, "Corsair Hydro H100i Platinum SE"); +HYDRO_PLATINUM_INFO(2, corsair_h115i_plat, "Corsair Hydro H115i Platinum"); +HYDRO_PLATINUM_INFO(2, corsair_h60i_xt, "Corsair Hydro H60i Pro XT"); +HYDRO_PLATINUM_INFO(2, corsair_h100i_xt, "Corsair Hydro H100i Pro XT"); +HYDRO_PLATINUM_INFO(2, corsair_h115i_xt, "Corsair Hydro H115i Pro XT"); +HYDRO_PLATINUM_INFO(3, corsair_h150i_xt, "Corsair Hydro H150i Pro XT"); +HYDRO_PLATINUM_INFO(2, corsair_h100i_elite, "Corsair iCUE H100i Elite RGB"); +HYDRO_PLATINUM_INFO(2, corsair_h115i_elite, "Corsair iCUE H115i Elite RGB"); +HYDRO_PLATINUM_INFO(3, corsair_h150i_elite, "Corsair iCUE H150i Elite RGB"); +HYDRO_PLATINUM_INFO(2, corsair_h100i_elite_w, "Corsair iCUE H100i Elite RGB (White)"); +HYDRO_PLATINUM_INFO(3, corsair_h150i_elite_w, "Corsair iCUE H150i Elite RGB (White)"); + +static const struct hid_device_id hydro_platinum_table[] = { + /* H100i Platinum */ + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c18), .driver_data = (kernel_ulong_t)&info_corsair_h100i_plat }, + /* H100i Platinum SE */ + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c19), .driver_data = (kernel_ulong_t)&info_corsair_h100i_plat_se }, + /* H115i Platinum */ + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c17), .driver_data = (kernel_ulong_t)&info_corsair_h115i_plat }, + /* H60i Pro XT */ + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c29), .driver_data = (kernel_ulong_t)&info_corsair_h60i_xt }, + /* H100i Pro XT */ + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c20), .driver_data = (kernel_ulong_t)&info_corsair_h100i_xt }, + /* H115i Pro XT */ + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c21), .driver_data = (kernel_ulong_t)&info_corsair_h115i_xt }, + /* H150i Pro XT */ + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c22), .driver_data = (kernel_ulong_t)&info_corsair_h150i_xt }, + /* iCUE H100i Elite RGB */ + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c35), .driver_data = (kernel_ulong_t)&info_corsair_h100i_elite }, + /* iCUE H115i Elite RGB */ + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c36), .driver_data = (kernel_ulong_t)&info_corsair_h115i_elite }, + /* iCUE H150i Elite RGB */ + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c37), .driver_data = (kernel_ulong_t)&info_corsair_h150i_elite }, + /* iCUE H100i Elite RGB (White) */ + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c40), .driver_data = (kernel_ulong_t)&info_corsair_h100i_elite_w }, + /* iCUE H150i Elite RGB (White) */ + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c41), .driver_data = (kernel_ulong_t)&info_corsair_h150i_elite_w }, + { } +}; +MODULE_DEVICE_TABLE(hid, hydro_platinum_table); + +static struct hid_driver hydro_platinum_driver = { + .name = DRIVER_NAME, + .id_table = hydro_platinum_table, + .probe = hydro_platinum_probe, + .remove = hydro_platinum_remove, + .raw_event = hydro_platinum_raw_event, +}; + +module_hid_driver(hydro_platinum_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Hwmon driver for Corsair Hydro Platinum / Pro XT / Elite RGB"); diff --git a/drivers/hwmon/dkms.conf.in b/drivers/hwmon/dkms.conf.in index a448580..99be2ff 100644 --- a/drivers/hwmon/dkms.conf.in +++ b/drivers/hwmon/dkms.conf.in @@ -13,4 +13,7 @@ DEST_MODULE_LOCATION[2]="/kernel/drivers/hwmon" BUILT_MODULE_NAME[3]="nzxt-smart2" DEST_MODULE_LOCATION[3]="/kernel/drivers/hwmon" +BUILT_MODULE_NAME[4]="corsair-hydro-platinum" +DEST_MODULE_LOCATION[4]="/kernel/drivers/hwmon" + AUTOINSTALL="yes" From ea894c87cb14260c51b0d129dd5b6223892d9130 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Tue, 27 Jan 2026 19:43:47 -0500 Subject: [PATCH 02/46] corsair-hydro-platinum: Cleanup unused includes Also make sure we pull completion.h explicitly just in case. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 0ca1bd2..3728bb3 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -30,7 +30,7 @@ * Copyright 2026 Jack Greiner */ -#include +#include #include #include #include @@ -38,8 +38,6 @@ #include #include #include -#include -#include #include #include From 2056edfdb0a5d95b396ada812f613604a9a1d3d4 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Tue, 27 Jan 2026 21:21:56 -0500 Subject: [PATCH 03/46] corsair-hydro-platinum: Improve resiliance to userspace interference The driver previously used a single shared buffer for both transmitting commands and receiving responses. This may have caused race conditions when the driver was accessed concurrently, or when userspace tools (like OpenRGB or liquidctl) were communicating with the device at the same time as the kernel driver was. This is easily replicated by using the "Effects" plugin for OpenRGB It seems like I didn't quite implement the CRC validation correctly, it will now properly detect and ignore responses not intended for the driver. Additionally rate-limited logging will report CRC check failures instead. This allows the kernel driver to coexist and remain somewhat resiliant when userspace tools are also using the device. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 86 +++++++++++++++++--------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 3728bb3..e9c8d63 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -88,7 +88,8 @@ struct hydro_platinum_data { struct hid_device *hdev; struct device *hwmon_dev; struct mutex lock; - u8 *buffer; + u8 *tx_buffer; + u8 *rx_buffer; u8 sequence; /* Sensor values */ @@ -147,29 +148,29 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat * Some devices/firmware revisions require the alignment of 64-byte payload * to be offset by the Report ID byte even in Control Transfers. */ - memset(priv->buffer, 0, REPORT_LENGTH + 1); + memset(priv->tx_buffer, 0, REPORT_LENGTH + 1); - priv->buffer[0] = 0x00; /* Report ID Padding */ - priv->buffer[1] = CMD_WRITE_PREFIX; + priv->tx_buffer[0] = 0x00; /* Report ID Padding */ + priv->tx_buffer[1] = CMD_WRITE_PREFIX; /* Sequence and feature/command logic */ priv->sequence = (priv->sequence % 31) + 1; - priv->buffer[2] = (priv->sequence << 3) | feature; - priv->buffer[3] = command; + priv->tx_buffer[2] = (priv->sequence << 3) | feature; + priv->tx_buffer[3] = command; start_at = 4; if (data && data_len > 0) { - memcpy(priv->buffer + start_at, data, min(data_len, REPORT_LENGTH - start_at - 1)); + memcpy(priv->tx_buffer + start_at, data, min(data_len, REPORT_LENGTH - start_at - 1)); } /* Calculate CRC over buf[2] to buf[REPORT_LENGTH-1+1] */ /* Payload is buf[1]..buf[64]. CRC is usually last byte of payload. */ - priv->buffer[REPORT_LENGTH] = crc8(corsair_crc8_table, priv->buffer + 2, REPORT_LENGTH - 2, 0); + priv->tx_buffer[REPORT_LENGTH] = crc8(corsair_crc8_table, priv->tx_buffer + 2, REPORT_LENGTH - 2, 0); /* Send Report - 65 bytes */ /* Use HID_REQ_SET_REPORT (Control Transfer) */ - ret = hid_hw_raw_request(priv->hdev, 0 /* Report ID */, priv->buffer, REPORT_LENGTH + 1, + ret = hid_hw_raw_request(priv->hdev, 0 /* Report ID */, priv->tx_buffer, REPORT_LENGTH + 1, HID_OUTPUT_REPORT, HID_REQ_SET_REPORT); /* raw_request returns number of bytes written on success */ @@ -192,25 +193,41 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 feature, u8 command, u8 *data, int data_len) { int ret; + u8 sent_seq; reinit_completion(&priv->wait_for_report); ret = hydro_platinum_send_command(priv, feature, command, data, data_len); - if (ret < 0) + if (ret < 0) { + hid_err_ratelimited(priv->hdev, "Failed to send command %02x: %d\n", command, ret); return ret; + } + + sent_seq = priv->sequence; ret = wait_for_completion_interruptible_timeout(&priv->wait_for_report, msecs_to_jiffies(500)); - if (ret == 0) + if (ret == 0) { + hid_warn_ratelimited(priv->hdev, "Timeout waiting for response to command %02x\n", command); return -ETIMEDOUT; - else if (ret < 0) + } else if (ret < 0) { return ret; + } + /* * CRC Verification * liquidctl checks CRC over bytes [1..63] (assuming 64 byte report). * If standard SMBus CRC-8 algorithm is used, checksumming (Data + CRC) should yield 0. + * + * NOTE: When userspace tools (like OpenRGB or liquidctl) are accessing the device + * concurrently, we may intercept their response packets or see collisions. + * In these cases, the CRC check will often fail (or the sequence number might mismatch). + * This is expected behavior in a multi-client scenario and catching it here + * prevents the driver from processing invalid data, which could otherwise + * confuse the device state machine and cause firmware crashes/reboots. */ - if (crc8(corsair_crc8_table, priv->buffer + 1, REPORT_LENGTH - 1, 0) != 0) { + if (crc8(corsair_crc8_table, priv->rx_buffer + 1, REPORT_LENGTH - 1, 0) != 0) { + hid_warn_ratelimited(priv->hdev, "CRC check failed for command %02x - possible userspace collision\n", command); return -EIO; } @@ -319,7 +336,12 @@ static int hydro_platinum_raw_event(struct hid_device *hdev, struct hid_report * /* Safety check size */ if (size > REPORT_LENGTH + 16) size = REPORT_LENGTH + 16; - memcpy(priv->buffer, data, size); + /* + * Copy to RX buffer. + * We accept any input report here to unblock the waiter, and let the + * transaction logic (CRC check) validate if it's the correct response. + */ + memcpy(priv->rx_buffer, data, size); complete(&priv->wait_for_report); return 0; @@ -346,17 +368,17 @@ static int hydro_platinum_update(struct hydro_platinum_data *priv) if (ret < 0) goto out; - /* Data is now in priv->buffer (populated by raw_event) */ + /* Data is now in priv->rx_buffer (populated by raw_event) */ /* Firmware Version: res[2] (Major << 4 | Minor), res[3] (Patch) */ if (!priv->valid) { - priv->fw_version[0] = priv->buffer[2] >> 4; - priv->fw_version[1] = priv->buffer[2] & 0x0f; - priv->fw_version[2] = priv->buffer[3]; + priv->fw_version[0] = priv->rx_buffer[2] >> 4; + priv->fw_version[1] = priv->rx_buffer[2] & 0x0f; + priv->fw_version[2] = priv->rx_buffer[3]; } /* Temp */ - priv->liquid_temp = ((int)priv->buffer[8] * 1000) + ((int)priv->buffer[7] * 1000 / 255); + priv->liquid_temp = ((int)priv->rx_buffer[8] * 1000) + ((int)priv->rx_buffer[7] * 1000 / 255); /* * Parse Sensor Data: @@ -367,25 +389,25 @@ static int hydro_platinum_update(struct hydro_platinum_data *priv) */ /* Pump (Base 28) */ - priv->pump_speed = get_unaligned_le16(priv->buffer + 28 + 1); - priv->pump_duty = priv->buffer[28]; + priv->pump_speed = get_unaligned_le16(priv->rx_buffer + 28 + 1); + priv->pump_duty = priv->rx_buffer[28]; /* Fan 1 (Base 14) */ if (priv->fan_count >= 1) { - priv->fan_speeds[0] = get_unaligned_le16(priv->buffer + 14 + 1); - priv->fan_duty[0] = priv->buffer[14]; + priv->fan_speeds[0] = get_unaligned_le16(priv->rx_buffer + 14 + 1); + priv->fan_duty[0] = priv->rx_buffer[14]; } /* Fan 2 (Base 21) */ if (priv->fan_count >= 2) { - priv->fan_speeds[1] = get_unaligned_le16(priv->buffer + 21 + 1); - priv->fan_duty[1] = priv->buffer[21]; + priv->fan_speeds[1] = get_unaligned_le16(priv->rx_buffer + 21 + 1); + priv->fan_duty[1] = priv->rx_buffer[21]; } /* Fan 3 (Base 42) */ if (priv->fan_count >= 3) { - priv->fan_speeds[2] = get_unaligned_le16(priv->buffer + 42 + 1); - priv->fan_duty[2] = priv->buffer[42]; + priv->fan_speeds[2] = get_unaligned_le16(priv->rx_buffer + 42 + 1); + priv->fan_duty[2] = priv->rx_buffer[42]; } priv->updated = jiffies; @@ -590,9 +612,13 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device return -ENOMEM; priv->hdev = hdev; - /* Buffer needs to be large enough + safety */ - priv->buffer = devm_kzalloc(&hdev->dev, REPORT_LENGTH + 16, GFP_KERNEL); - if (!priv->buffer) + /* Buffers need to be large enough + safety */ + priv->tx_buffer = devm_kzalloc(&hdev->dev, REPORT_LENGTH + 16, GFP_KERNEL); + if (!priv->tx_buffer) + return -ENOMEM; + + priv->rx_buffer = devm_kzalloc(&hdev->dev, REPORT_LENGTH + 16, GFP_KERNEL); + if (!priv->rx_buffer) return -ENOMEM; mutex_init(&priv->lock); From 99808a660e70b408a81af9a4bd58c996e39ad71a Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Tue, 27 Jan 2026 21:31:38 -0500 Subject: [PATCH 04/46] corsair-hydro-platinum: Improve logging of command write failures Previously this was wishy washy and only really done for FEATURE_COOLING_FAN3 commands (I was having particular issues there). Now it is more consistant and covers all fans + pump commands. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index e9c8d63..457d7ae 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -314,7 +314,7 @@ static int hydro_platinum_write_cooling(struct hydro_platinum_data *priv) ret = hydro_platinum_transaction(priv, FEATURE_COOLING_FAN3, CMD_SET_COOLING, data2, sizeof(data2)); if (ret) - hid_warn(priv->hdev, "Failed to set Fan 3 speed: %d\n", ret); + return ret; } return 0; @@ -509,6 +509,12 @@ static int hydro_platinum_write(struct device *dev, enum hwmon_sensor_types type } ret = hydro_platinum_write_cooling(priv); + if (ret) { + if (channel == 0) + hid_warn_ratelimited(priv->hdev, "Failed to set Pump speed: %d\n", ret); + else + hid_warn_ratelimited(priv->hdev, "Failed to set Fan %d speed: %d\n", channel, ret); + } break; default: ret = -EOPNOTSUPP; From ce45c17d2e2a862c21e9ba0c858dc373c081ef88 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Tue, 27 Jan 2026 22:07:59 -0500 Subject: [PATCH 05/46] corsair-hydro-platinum: Fix compiler warnings So we can support older kernel versions we need to use dev_warn_ratelimited and dev_err_ratelimited as it turns out hid_err_ratelimited and hid_warn_ratelimited are newer helper macros. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 457d7ae..9d5e98a 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -193,21 +193,18 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 feature, u8 command, u8 *data, int data_len) { int ret; - u8 sent_seq; reinit_completion(&priv->wait_for_report); ret = hydro_platinum_send_command(priv, feature, command, data, data_len); if (ret < 0) { - hid_err_ratelimited(priv->hdev, "Failed to send command %02x: %d\n", command, ret); + dev_err_ratelimited(&priv->hdev->dev, "Failed to send command %02x: %d\n", command, ret); return ret; } - sent_seq = priv->sequence; - ret = wait_for_completion_interruptible_timeout(&priv->wait_for_report, msecs_to_jiffies(500)); if (ret == 0) { - hid_warn_ratelimited(priv->hdev, "Timeout waiting for response to command %02x\n", command); + dev_warn_ratelimited(&priv->hdev->dev, "Timeout waiting for response to command %02x\n", command); return -ETIMEDOUT; } else if (ret < 0) { return ret; @@ -227,7 +224,7 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu * confuse the device state machine and cause firmware crashes/reboots. */ if (crc8(corsair_crc8_table, priv->rx_buffer + 1, REPORT_LENGTH - 1, 0) != 0) { - hid_warn_ratelimited(priv->hdev, "CRC check failed for command %02x - possible userspace collision\n", command); + dev_warn_ratelimited(&priv->hdev->dev, "CRC check failed for command %02x - possible userspace collision\n", command); return -EIO; } @@ -511,9 +508,9 @@ static int hydro_platinum_write(struct device *dev, enum hwmon_sensor_types type ret = hydro_platinum_write_cooling(priv); if (ret) { if (channel == 0) - hid_warn_ratelimited(priv->hdev, "Failed to set Pump speed: %d\n", ret); + dev_warn_ratelimited(&priv->hdev->dev, "Failed to set Pump speed: %d\n", ret); else - hid_warn_ratelimited(priv->hdev, "Failed to set Fan %d speed: %d\n", channel, ret); + dev_warn_ratelimited(&priv->hdev->dev, "Failed to set Fan %d speed: %d\n", channel, ret); } break; default: From 0afdc6442535958cc058bcb695019974e30e8ea9 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Tue, 27 Jan 2026 23:50:58 -0500 Subject: [PATCH 06/46] corsair-hydro-platinum: Fix checkpatch linter errors Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 283 ++++++++++++++----------- 1 file changed, 160 insertions(+), 123 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 9d5e98a..d0bc83a 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -87,11 +87,11 @@ DECLARE_CRC8_TABLE(corsair_crc8_table); struct hydro_platinum_data { struct hid_device *hdev; struct device *hwmon_dev; - struct mutex lock; + struct mutex lock; /* lock for transfer buffer and data access */ u8 *tx_buffer; u8 *rx_buffer; u8 sequence; - + /* Sensor values */ u16 fan_speeds[MAX_FAN_COUNT]; u8 fan_duty[MAX_FAN_COUNT]; @@ -103,14 +103,14 @@ struct hydro_platinum_data { u8 target_pump_mode; u8 target_fan_mode[MAX_FAN_COUNT]; u8 target_fan_duty[MAX_FAN_COUNT]; - + /* Detected configuration */ int fan_count; const char *model_name; u8 fw_version[3]; - + struct completion wait_for_report; - + unsigned long updated; bool valid; }; @@ -138,44 +138,47 @@ static void hydro_platinum_init_crc(void) * * Constructs the report buffer with CRC and sends it via `hid_hw_raw_request`. */ -static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feature, u8 command, u8 *data, int data_len) +static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feature, u8 command, + u8 *data, int data_len) { int ret; int start_at; - - /* + + /* * Construct 65-byte buffer with Report ID 0 padding. - * Some devices/firmware revisions require the alignment of 64-byte payload + * Some devices/firmware revisions require the alignment of 64-byte payload * to be offset by the Report ID byte even in Control Transfers. */ memset(priv->tx_buffer, 0, REPORT_LENGTH + 1); - + priv->tx_buffer[0] = 0x00; /* Report ID Padding */ priv->tx_buffer[1] = CMD_WRITE_PREFIX; - + /* Sequence and feature/command logic */ priv->sequence = (priv->sequence % 31) + 1; priv->tx_buffer[2] = (priv->sequence << 3) | feature; priv->tx_buffer[3] = command; start_at = 4; - - if (data && data_len > 0) { - memcpy(priv->tx_buffer + start_at, data, min(data_len, REPORT_LENGTH - start_at - 1)); - } - + + if (data && data_len > 0) + memcpy(priv->tx_buffer + start_at, data, + min(data_len, REPORT_LENGTH - start_at - 1)); + /* Calculate CRC over buf[2] to buf[REPORT_LENGTH-1+1] */ /* Payload is buf[1]..buf[64]. CRC is usually last byte of payload. */ - priv->tx_buffer[REPORT_LENGTH] = crc8(corsair_crc8_table, priv->tx_buffer + 2, REPORT_LENGTH - 2, 0); + priv->tx_buffer[REPORT_LENGTH] = crc8(corsair_crc8_table, priv->tx_buffer + 2, + REPORT_LENGTH - 2, 0); /* Send Report - 65 bytes */ - + /* Use HID_REQ_SET_REPORT (Control Transfer) */ ret = hid_hw_raw_request(priv->hdev, 0 /* Report ID */, priv->tx_buffer, REPORT_LENGTH + 1, HID_OUTPUT_REPORT, HID_REQ_SET_REPORT); - + /* raw_request returns number of bytes written on success */ - if (ret > 0) ret = 0; - + if (ret > 0) + ret = 0; + return ret; } @@ -190,7 +193,8 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat * Sends a command and waits up to 500ms for an Input Report on the Interrupt IN endpoint. * This ensures strict command-response ordering to prevent device confusion. */ -static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 feature, u8 command, u8 *data, int data_len) +static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 feature, u8 command, + u8 *data, int data_len) { int ret; @@ -198,24 +202,26 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu ret = hydro_platinum_send_command(priv, feature, command, data, data_len); if (ret < 0) { - dev_err_ratelimited(&priv->hdev->dev, "Failed to send command %02x: %d\n", command, ret); + dev_err_ratelimited(&priv->hdev->dev, "Failed to send command %02x: %d\n", + command, ret); return ret; } - ret = wait_for_completion_interruptible_timeout(&priv->wait_for_report, msecs_to_jiffies(500)); + ret = wait_for_completion_interruptible_timeout(&priv->wait_for_report, + msecs_to_jiffies(500)); if (ret == 0) { - dev_warn_ratelimited(&priv->hdev->dev, "Timeout waiting for response to command %02x\n", command); + dev_warn_ratelimited(&priv->hdev->dev, "Timeout waiting for response to command %02x\n", + command); return -ETIMEDOUT; } else if (ret < 0) { return ret; } - - /* + /* * CRC Verification * liquidctl checks CRC over bytes [1..63] (assuming 64 byte report). * If standard SMBus CRC-8 algorithm is used, checksumming (Data + CRC) should yield 0. - * + * * NOTE: When userspace tools (like OpenRGB or liquidctl) are accessing the device * concurrently, we may intercept their response packets or see collisions. * In these cases, the CRC check will often fail (or the sequence number might mismatch). @@ -224,7 +230,9 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu * confuse the device state machine and cause firmware crashes/reboots. */ if (crc8(corsair_crc8_table, priv->rx_buffer + 1, REPORT_LENGTH - 1, 0) != 0) { - dev_warn_ratelimited(&priv->hdev->dev, "CRC check failed for command %02x - possible userspace collision\n", command); + dev_warn_ratelimited(&priv->hdev->dev, + "CRC check failed for command %02x - possible userspace collision\n", + command); return -EIO; } @@ -243,13 +251,13 @@ static int hydro_platinum_write_cooling(struct hydro_platinum_data *priv) { int ret; u8 data[60]; - - /* + + /* * Construct the 60-byte payload for the Command 0x14. * The payload is appended to the 4-byte header in hydro_platinum_send_command. */ - - /* + + /* * Payload Prefix: 00 ff 05 ff ff ff ff ff */ memset(data, 0, sizeof(data)); @@ -257,9 +265,9 @@ static int hydro_platinum_write_cooling(struct hydro_platinum_data *priv) data[1] = 0xff; data[2] = 0x05; memset(data + 3, 0xff, 5); - + data[OFFSET_PROFILE_LEN] = 7; - + /* Pump Mode */ data[OFFSET_PUMP_MODE] = priv->target_pump_mode; @@ -279,37 +287,40 @@ static int hydro_platinum_write_cooling(struct hydro_platinum_data *priv) } /* Send Feature Cooling (Fan 1, 2, Pump) */ - ret = hydro_platinum_transaction(priv, FEATURE_COOLING, CMD_SET_COOLING, data, sizeof(data)); + ret = hydro_platinum_transaction(priv, FEATURE_COOLING, CMD_SET_COOLING, data, + sizeof(data)); if (ret) return ret; - /* + /* * Command Ordering Note: - * The device requires the "Main" cooling command (Feature 0x00) to be sent + * The device requires the "Main" cooling command (Feature 0x00) to be sent * BEFORE the "Secondary" cooling command (Feature 0x03, for Fan 3). * reversing this order may cause the device to stall or return -EPIPE. */ - + /* Fan 3 (Index 2) - Requires Feature Cooling 2 */ if (priv->fan_count >= 3) { u8 data2[60]; + memcpy(data2, data, sizeof(data)); - + /* Ensure Pump Mode is set correctly even in secondary command */ data2[OFFSET_PUMP_MODE] = priv->target_pump_mode; - + /* Reset Fan 1/2 slots */ data2[OFFSET_FAN1_MODE] = 0; data2[OFFSET_FAN1_DUTY] = 0; data2[OFFSET_FAN2_MODE] = 0; data2[OFFSET_FAN2_DUTY] = 0; - + /* Fan 3 goes into Fan 1 slot */ data2[OFFSET_FAN1_MODE] = priv->target_fan_mode[2]; if (priv->target_fan_mode[2] == FAN_MODE_FIXED_DUTY) data2[OFFSET_FAN1_DUTY] = priv->target_fan_duty[2]; - - ret = hydro_platinum_transaction(priv, FEATURE_COOLING_FAN3, CMD_SET_COOLING, data2, sizeof(data2)); + + ret = hydro_platinum_transaction(priv, FEATURE_COOLING_FAN3, CMD_SET_COOLING, + data2, sizeof(data2)); if (ret) return ret; } @@ -317,7 +328,8 @@ static int hydro_platinum_write_cooling(struct hydro_platinum_data *priv) return 0; } -static int hydro_platinum_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) +static int hydro_platinum_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, + int size) { struct hydro_platinum_data *priv = hid_get_drvdata(hdev); @@ -325,21 +337,22 @@ static int hydro_platinum_raw_event(struct hid_device *hdev, struct hid_report * if (report->type != HID_INPUT_REPORT) return 0; - /* + /* * The driver buffer expects [0]=ReportID, [1]=Prefix. * We treat the raw data as the payload. */ - + /* Safety check size */ - if (size > REPORT_LENGTH + 16) size = REPORT_LENGTH + 16; - - /* + if (size > REPORT_LENGTH + 16) + size = REPORT_LENGTH + 16; + + /* * Copy to RX buffer. - * We accept any input report here to unblock the waiter, and let the + * We accept any input report here to unblock the waiter, and let the * transaction logic (CRC check) validate if it's the correct response. */ memcpy(priv->rx_buffer, data, size); - + complete(&priv->wait_for_report); return 0; } @@ -354,53 +367,54 @@ static int hydro_platinum_raw_event(struct hid_device *hdev, struct hid_report * static int hydro_platinum_update(struct hydro_platinum_data *priv) { int ret; - + mutex_lock(&priv->lock); - if (time_after(jiffies, priv->updated + msecs_to_jiffies(STATUS_VALIDITY)) || !priv->valid) { - + if (time_after(jiffies, priv->updated + msecs_to_jiffies(STATUS_VALIDITY)) || + !priv->valid) { reinit_completion(&priv->wait_for_report); ret = hydro_platinum_transaction(priv, FEATURE_COOLING, CMD_GET_STATUS, NULL, 0); if (ret < 0) goto out; - + /* Data is now in priv->rx_buffer (populated by raw_event) */ - + /* Firmware Version: res[2] (Major << 4 | Minor), res[3] (Patch) */ if (!priv->valid) { priv->fw_version[0] = priv->rx_buffer[2] >> 4; priv->fw_version[1] = priv->rx_buffer[2] & 0x0f; priv->fw_version[2] = priv->rx_buffer[3]; } - + /* Temp */ - priv->liquid_temp = ((int)priv->rx_buffer[8] * 1000) + ((int)priv->rx_buffer[7] * 1000 / 255); - - /* + priv->liquid_temp = ((int)priv->rx_buffer[8] * 1000) + + ((int)priv->rx_buffer[7] * 1000 / 255); + + /* * Parse Sensor Data: * - Fan 1 Speed: Offset 14, Duty: Offset 15 * - Fan 2 Speed: Offset 21, Duty: Offset 22 * - Fan 3 Speed: Offset 42, Duty: Offset 43 * - Pump Speed: Offset 28, Duty: Offset 29 */ - + /* Pump (Base 28) */ priv->pump_speed = get_unaligned_le16(priv->rx_buffer + 28 + 1); priv->pump_duty = priv->rx_buffer[28]; - + /* Fan 1 (Base 14) */ if (priv->fan_count >= 1) { priv->fan_speeds[0] = get_unaligned_le16(priv->rx_buffer + 14 + 1); priv->fan_duty[0] = priv->rx_buffer[14]; } - + /* Fan 2 (Base 21) */ if (priv->fan_count >= 2) { priv->fan_speeds[1] = get_unaligned_le16(priv->rx_buffer + 21 + 1); priv->fan_duty[1] = priv->rx_buffer[21]; } - + /* Fan 3 (Base 42) */ if (priv->fan_count >= 3) { priv->fan_speeds[2] = get_unaligned_le16(priv->rx_buffer + 42 + 1); @@ -417,10 +431,11 @@ static int hydro_platinum_update(struct hydro_platinum_data *priv) return ret; } -static umode_t hydro_platinum_is_visible(const void *data, enum hwmon_sensor_types type, u32 attr, int channel) +static umode_t hydro_platinum_is_visible(const void *data, enum hwmon_sensor_types type, u32 attr, + int channel) { const struct hydro_platinum_data *priv = data; - + switch (type) { case hwmon_temp: return 0444; @@ -428,19 +443,24 @@ static umode_t hydro_platinum_is_visible(const void *data, enum hwmon_sensor_typ case hwmon_pwm: /* Channel 0: Pump */ /* Channel 1..N: Fans */ - if (channel == 0) return 0644; /* Pump (Mode control via PWM) */ - if (channel <= priv->fan_count) return 0644; /* Fans */ + if (channel == 0) + return 0644; /* Pump (Mode control via PWM) */ + if (channel <= priv->fan_count) + return 0644; /* Fans */ return 0; default: return 0; } } -static int hydro_platinum_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long *val) +static int hydro_platinum_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, + int channel, long *val) { struct hydro_platinum_data *priv = dev_get_drvdata(dev); int ret = hydro_platinum_update(priv); - if (ret < 0) return ret; + + if (ret < 0) + return ret; switch (type) { case hwmon_fan: @@ -464,7 +484,8 @@ static int hydro_platinum_read(struct device *dev, enum hwmon_sensor_types type, return 0; } -static int hydro_platinum_write(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long val) +static int hydro_platinum_write(struct device *dev, enum hwmon_sensor_types type, u32 attr, + int channel, long val) { struct hydro_platinum_data *priv = dev_get_drvdata(dev); int ret = 0; @@ -475,7 +496,7 @@ static int hydro_platinum_write(struct device *dev, enum hwmon_sensor_types type switch (type) { case hwmon_pwm: if (channel == 0) { - /* + /* * Pump Control: * Map 0-255 PWM to discrete modes: * 0-84: Quiet (Mode 0) @@ -483,14 +504,18 @@ static int hydro_platinum_write(struct device *dev, enum hwmon_sensor_types type * 170-255: Extreme (Mode 2) */ u8 mode; + val = clamp_val(val, 0, 255); - - if (val < 85) mode = PUMP_MODE_QUIET; - else if (val < 170) mode = PUMP_MODE_BALANCED; - else mode = PUMP_MODE_EXTREME; - + + if (val < 85) + mode = PUMP_MODE_QUIET; + else if (val < 170) + mode = PUMP_MODE_BALANCED; + else + mode = PUMP_MODE_EXTREME; + priv->target_pump_mode = mode; - + } else { /* Fan Control */ /* Index is channel - 1 */ @@ -499,19 +524,20 @@ static int hydro_platinum_write(struct device *dev, enum hwmon_sensor_types type ret = -EINVAL; goto out; } - + val = clamp_val(val, 0, 255); priv->target_fan_duty[i] = (u8)val; priv->target_fan_mode[i] = FAN_MODE_FIXED_DUTY; } - + ret = hydro_platinum_write_cooling(priv); - if (ret) { if (channel == 0) - dev_warn_ratelimited(&priv->hdev->dev, "Failed to set Pump speed: %d\n", ret); + dev_warn_ratelimited(&priv->hdev->dev, + "Failed to set Pump speed: %d\n", ret); else - dev_warn_ratelimited(&priv->hdev->dev, "Failed to set Fan %d speed: %d\n", channel, ret); - } + dev_warn_ratelimited(&priv->hdev->dev, + "Failed to set Fan %d speed: %d\n", + channel, ret); break; default: ret = -EOPNOTSUPP; @@ -523,8 +549,6 @@ static int hydro_platinum_write(struct device *dev, enum hwmon_sensor_types type return ret; } - - static const char *const hydro_platinum_fan_labels[] = { "Pump", "Fan 1", @@ -532,7 +556,8 @@ static const char *const hydro_platinum_fan_labels[] = { "Fan 3" }; -static int hydro_platinum_read_string_impl(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, const char **str) +static int hydro_platinum_read_string_impl(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, const char **str) { switch (type) { case hwmon_fan: @@ -557,17 +582,17 @@ static int hydro_platinum_read_string_impl(struct device *dev, enum hwmon_sensor static const struct hwmon_channel_info *hydro_platinum_info[] = { HWMON_CHANNEL_INFO(fan, - HWMON_F_INPUT | HWMON_F_LABEL, /* Pump */ - HWMON_F_INPUT | HWMON_F_LABEL, /* Fan 1 */ - HWMON_F_INPUT | HWMON_F_LABEL, /* Fan 2 */ - HWMON_F_INPUT | HWMON_F_LABEL), /* Fan 3 */ + HWMON_F_INPUT | HWMON_F_LABEL, /* Pump */ + HWMON_F_INPUT | HWMON_F_LABEL, /* Fan 1 */ + HWMON_F_INPUT | HWMON_F_LABEL, /* Fan 2 */ + HWMON_F_INPUT | HWMON_F_LABEL), /* Fan 3 */ HWMON_CHANNEL_INFO(pwm, - HWMON_PWM_INPUT, /* Pump */ - HWMON_PWM_INPUT, /* Fan 1 */ - HWMON_PWM_INPUT, /* Fan 2 */ - HWMON_PWM_INPUT), /* Fan 3 */ + HWMON_PWM_INPUT, /* Pump */ + HWMON_PWM_INPUT, /* Fan 1 */ + HWMON_PWM_INPUT, /* Fan 2 */ + HWMON_PWM_INPUT), /* Fan 3 */ HWMON_CHANNEL_INFO(temp, - HWMON_T_INPUT | HWMON_T_LABEL), + HWMON_T_INPUT | HWMON_T_LABEL), NULL }; @@ -578,12 +603,13 @@ static const struct hwmon_ops hydro_platinum_hwmon_ops = { .read_string = hydro_platinum_read_string_impl, }; -static ssize_t hydro_platinum_label_show(struct device *dev, struct device_attribute *attr, char *buf) +static ssize_t label_show(struct device *dev, struct device_attribute *attr, char *buf) { struct hydro_platinum_data *priv = dev_get_drvdata(dev); + return sysfs_emit(buf, "%s\n", priv->model_name); } -static DEVICE_ATTR(label, 0444, hydro_platinum_label_show, NULL); +static DEVICE_ATTR_RO(label); static struct attribute *hydro_platinum_attrs[] = { &dev_attr_label.attr, @@ -619,7 +645,7 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device priv->tx_buffer = devm_kzalloc(&hdev->dev, REPORT_LENGTH + 16, GFP_KERNEL); if (!priv->tx_buffer) return -ENOMEM; - + priv->rx_buffer = devm_kzalloc(&hdev->dev, REPORT_LENGTH + 16, GFP_KERNEL); if (!priv->rx_buffer) return -ENOMEM; @@ -627,20 +653,20 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device mutex_init(&priv->lock); init_completion(&priv->wait_for_report); hid_set_drvdata(hdev, priv); - - /* + + /* * Retrieve device specific info from the ID table directly. * This avoids a large switch statement and redundant data. */ - const struct hydro_platinum_device_info *info = + const struct hydro_platinum_device_info *info = (const struct hydro_platinum_device_info *)id->driver_data; - + if (!info) return -ENODEV; priv->fan_count = info->fan_count; priv->model_name = info->model_name; - + ret = hid_parse(hdev); if (ret) return ret; @@ -659,7 +685,7 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device /* Initialize CRC table */ hydro_platinum_init_crc(); - /* + /* * Initialize Device * Default: Pump Balanced, Fans 50% */ @@ -671,19 +697,18 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device hid_info(hdev, "Initializing device (Set Cooling: Pump Balanced, Fans 50%%)...\n"); ret = hydro_platinum_write_cooling(priv); - if (ret) { + if (ret) hid_warn(hdev, "initialization command failed: %d\n", ret); - } - + /* Wait for response to init command. hydro_platinum_write_cooling handles the delay. */ - + /* Initial update to get firmware version */ hid_info(hdev, "Requesting initial status update...\n"); ret = hydro_platinum_update(priv); if (ret) hid_warn(hdev, "initial update failed: %d\n", ret); else - hid_info(hdev, "Firmware version: %d.%d.%d\n", + hid_info(hdev, "Firmware version: %d.%d.%d\n", priv->fw_version[0], priv->fw_version[1], priv->fw_version[2]); priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, info->hwmon_name, @@ -735,29 +760,41 @@ HYDRO_PLATINUM_INFO(3, corsair_h150i_elite_w, "Corsair iCUE H150i Elite RGB (Whi static const struct hid_device_id hydro_platinum_table[] = { /* H100i Platinum */ - { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c18), .driver_data = (kernel_ulong_t)&info_corsair_h100i_plat }, + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c18), + .driver_data = (kernel_ulong_t)&info_corsair_h100i_plat }, /* H100i Platinum SE */ - { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c19), .driver_data = (kernel_ulong_t)&info_corsair_h100i_plat_se }, + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c19), + .driver_data = (kernel_ulong_t)&info_corsair_h100i_plat_se }, /* H115i Platinum */ - { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c17), .driver_data = (kernel_ulong_t)&info_corsair_h115i_plat }, + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c17), + .driver_data = (kernel_ulong_t)&info_corsair_h115i_plat }, /* H60i Pro XT */ - { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c29), .driver_data = (kernel_ulong_t)&info_corsair_h60i_xt }, + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c29), + .driver_data = (kernel_ulong_t)&info_corsair_h60i_xt }, /* H100i Pro XT */ - { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c20), .driver_data = (kernel_ulong_t)&info_corsair_h100i_xt }, + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c20), + .driver_data = (kernel_ulong_t)&info_corsair_h100i_xt }, /* H115i Pro XT */ - { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c21), .driver_data = (kernel_ulong_t)&info_corsair_h115i_xt }, + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c21), + .driver_data = (kernel_ulong_t)&info_corsair_h115i_xt }, /* H150i Pro XT */ - { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c22), .driver_data = (kernel_ulong_t)&info_corsair_h150i_xt }, + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c22), + .driver_data = (kernel_ulong_t)&info_corsair_h150i_xt }, /* iCUE H100i Elite RGB */ - { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c35), .driver_data = (kernel_ulong_t)&info_corsair_h100i_elite }, + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c35), + .driver_data = (kernel_ulong_t)&info_corsair_h100i_elite }, /* iCUE H115i Elite RGB */ - { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c36), .driver_data = (kernel_ulong_t)&info_corsair_h115i_elite }, + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c36), + .driver_data = (kernel_ulong_t)&info_corsair_h115i_elite }, /* iCUE H150i Elite RGB */ - { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c37), .driver_data = (kernel_ulong_t)&info_corsair_h150i_elite }, + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c37), + .driver_data = (kernel_ulong_t)&info_corsair_h150i_elite }, /* iCUE H100i Elite RGB (White) */ - { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c40), .driver_data = (kernel_ulong_t)&info_corsair_h100i_elite_w }, + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c40), + .driver_data = (kernel_ulong_t)&info_corsair_h100i_elite_w }, /* iCUE H150i Elite RGB (White) */ - { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c41), .driver_data = (kernel_ulong_t)&info_corsair_h150i_elite_w }, + { HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, 0x0c41), + .driver_data = (kernel_ulong_t)&info_corsair_h150i_elite_w }, { } }; MODULE_DEVICE_TABLE(hid, hydro_platinum_table); From ce5cc6fd779a47b386b2d622b3d189cde9536191 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 28 Jan 2026 00:07:27 -0500 Subject: [PATCH 07/46] corsair-hydro-platinum: Add fallback implementation for when CONFIG_CRC8 is disabled Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 33 +++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index d0bc83a..71d6286 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -82,7 +82,9 @@ #define OFFSET_PUMP_MODE 20 #define OFFSET_PROFILE_LEN 26 +#if IS_REACHABLE(CONFIG_CRC8) DECLARE_CRC8_TABLE(corsair_crc8_table); +#endif struct hydro_platinum_data { struct hid_device *hdev; @@ -125,7 +127,32 @@ struct hydro_platinum_device_info { /* SMBus standard CRC-8 polynomial x^8 + x^2 + x + 1 (0x07) */ static void hydro_platinum_init_crc(void) { +#if IS_REACHABLE(CONFIG_CRC8) crc8_populate_msb(corsair_crc8_table, 0x07); +#endif +} + +static u8 hydro_platinum_calc_crc(const u8 *data, size_t len, u8 crc) +{ +#if IS_REACHABLE(CONFIG_CRC8) + return crc8(corsair_crc8_table, data, len, crc); +#else + /* Table-less fallback for when CONFIG_CRC8 is disabled */ + size_t i, j; + u8 val; + + for (i = 0; i < len; i++) { + val = data[i]; + crc ^= val; + for (j = 0; j < 8; j++) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x07; + else + crc <<= 1; + } + } + return crc; +#endif } /** @@ -166,8 +193,8 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat /* Calculate CRC over buf[2] to buf[REPORT_LENGTH-1+1] */ /* Payload is buf[1]..buf[64]. CRC is usually last byte of payload. */ - priv->tx_buffer[REPORT_LENGTH] = crc8(corsair_crc8_table, priv->tx_buffer + 2, - REPORT_LENGTH - 2, 0); + priv->tx_buffer[REPORT_LENGTH] = hydro_platinum_calc_crc(priv->tx_buffer + 2, + REPORT_LENGTH - 2, 0); /* Send Report - 65 bytes */ @@ -229,7 +256,7 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu * prevents the driver from processing invalid data, which could otherwise * confuse the device state machine and cause firmware crashes/reboots. */ - if (crc8(corsair_crc8_table, priv->rx_buffer + 1, REPORT_LENGTH - 1, 0) != 0) { + if (hydro_platinum_calc_crc(priv->rx_buffer + 1, REPORT_LENGTH - 1, 0) != 0) { dev_warn_ratelimited(&priv->hdev->dev, "CRC check failed for command %02x - possible userspace collision\n", command); From e1cabe393c21629c8a2d5a2d31d26af4c10130de Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 28 Jan 2026 00:12:49 -0500 Subject: [PATCH 08/46] Vagrantfile: Add new USD ids (corsair-hydro-platinum) Signed-off-by: Jack Greiner --- Vagrantfile | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Vagrantfile b/Vagrantfile index 165921b..67f1afe 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -22,6 +22,19 @@ USB_IDS = [ { :vendor => "0x1e71", :product => "0x2010" }, { :vendor => "0x1e71", :product => "0x2011" }, { :vendor => "0x1e71", :product => "0x2019" }, + # corsair-hydro-platinum + { :vendor => "0x1b1c", :product => "0x0c18" }, # H100i Platinum + { :vendor => "0x1b1c", :product => "0x0c19" }, # H100i Platinum SE + { :vendor => "0x1b1c", :product => "0x0c17" }, # H115i Platinum + { :vendor => "0x1b1c", :product => "0x0c29" }, # H60i Pro XT + { :vendor => "0x1b1c", :product => "0x0c20" }, # H100i Pro XT + { :vendor => "0x1b1c", :product => "0x0c21" }, # H115i Pro XT + { :vendor => "0x1b1c", :product => "0x0c22" }, # H150i Pro XT + { :vendor => "0x1b1c", :product => "0x0c35" }, # H100i Elite RGB + { :vendor => "0x1b1c", :product => "0x0c36" }, # H115i Elite RGB + { :vendor => "0x1b1c", :product => "0x0c37" }, # H150i Elite RGB + { :vendor => "0x1b1c", :product => "0x0c40" }, # H100i Elite RGB (White) + { :vendor => "0x1b1c", :product => "0x0c41" }, # H150i Elite RGB (White) ] Vagrant.configure("2") do |config| From 5ce03084779fd2f2c2a96d4d1ed55d6cbcf0137c Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 28 Jan 2026 09:34:23 -0500 Subject: [PATCH 09/46] corsair-hydro-platinum: Add firmware version to debugfs as "firmware_version" Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 71d6286..ae2ce68 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -31,6 +31,7 @@ */ #include +#include #include #include #include @@ -115,6 +116,7 @@ struct hydro_platinum_data { unsigned long updated; bool valid; + struct dentry *debugfs; }; /* Device Info Structs */ @@ -558,6 +560,7 @@ static int hydro_platinum_write(struct device *dev, enum hwmon_sensor_types type } ret = hydro_platinum_write_cooling(priv); + if (ret) { if (channel == 0) dev_warn_ratelimited(&priv->hdev->dev, "Failed to set Pump speed: %d\n", ret); @@ -565,6 +568,7 @@ static int hydro_platinum_write(struct device *dev, enum hwmon_sensor_types type dev_warn_ratelimited(&priv->hdev->dev, "Failed to set Fan %d speed: %d\n", channel, ret); + } break; default: ret = -EOPNOTSUPP; @@ -647,6 +651,29 @@ static const struct attribute_group hydro_platinum_group = { .attrs = hydro_platinum_attrs, }; +static int firmware_version_show(struct seq_file *seq, void *offset) +{ + struct hydro_platinum_data *priv = seq->private; + + seq_printf(seq, "%d.%d.%d\n", + priv->fw_version[0], priv->fw_version[1], priv->fw_version[2]); + + return 0; +} +DEFINE_SHOW_ATTRIBUTE(firmware_version); + +static void hydro_platinum_debugfs_init(struct hydro_platinum_data *priv) +{ + char name[64]; + + snprintf(name, sizeof(name), "corsair_hydro_platinum-%s", + dev_name(&priv->hdev->dev)); + + priv->debugfs = debugfs_create_dir(name, NULL); + debugfs_create_file("firmware_version", 0444, priv->debugfs, priv, + &firmware_version_fops); +} + static const struct attribute_group *hydro_platinum_groups[] = { &hydro_platinum_group, NULL @@ -746,6 +773,8 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device goto fail_and_close; } + hydro_platinum_debugfs_init(priv); + return 0; fail_and_close: @@ -759,6 +788,7 @@ static void hydro_platinum_remove(struct hid_device *hdev) { struct hydro_platinum_data *priv = hid_get_drvdata(hdev); + debugfs_remove_recursive(priv->debugfs); hwmon_device_unregister(priv->hwmon_dev); hid_hw_close(hdev); hid_hw_stop(hdev); From 032b57bddcf0b2c6b753098a95e22cd962bd5954 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 28 Jan 2026 09:35:16 -0500 Subject: [PATCH 10/46] corsair-hydro-platinum: Add hwmon documentation for driver This should be inline with what the other drivers in this repo are doing for documentation. Signed-off-by: Jack Greiner --- .../hwmon/corsair-hydro-platinum.rst | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 Documentation/hwmon/corsair-hydro-platinum.rst diff --git a/Documentation/hwmon/corsair-hydro-platinum.rst b/Documentation/hwmon/corsair-hydro-platinum.rst new file mode 100644 index 0000000..9ec5614 --- /dev/null +++ b/Documentation/hwmon/corsair-hydro-platinum.rst @@ -0,0 +1,85 @@ +.. SPDX-License-Identifier: GPL-2.0+ + +Kernel driver corsair-hydro-platinum +==================================== + +Supported devices: + +* Corsair Hydro H100i Platinum +* Corsair Hydro H100i Platinum SE +* Corsair Hydro H115i Platinum +* Corsair Hydro H60i Pro XT +* Corsair Hydro H100i Pro XT +* Corsair Hydro H115i Pro XT +* Corsair Hydro H150i Pro XT +* Corsair iCUE H100i Elite RGB +* Corsair iCUE H115i Elite RGB +* Corsair iCUE H150i Elite RGB +* Corsair iCUE H100i Elite RGB (White) +* Corsair iCUE H150i Elite RGB (White) + +Author: Jack Greiner + +Description +----------- + +This driver enables hardware monitoring support for Corsair Hydro Platinum, +Pro XT and Elite RGB all-in-one CPU liquid coolers. + +The driver exposes the following sensor readings: +* Liquid temperature +* Pump speed +* Fan speeds (up to 3 fans, depending on model) + +The driver exposes the following controls: +* Pump mode (Quiet, Balanced, Extreme) +* Fan duty cycle (0-100%) + +The RGB LEDs are not supported in this driver, but can be controlled through +existing userspace tools, such as `liquidctl`_ or `OpenRGB`_. + +.. _liquidctl: https://github.com/liquidctl/liquidctl +.. _OpenRGB: https://gitlab.com/CalcProgrammer1/OpenRGB + +Usage Notes +----------- + +Pump Control +~~~~~~~~~~~~ +The pump does not support precise PWM duty cycle control. Instead, it supports +three distinct modes: Quiet, Balanced, and Extreme. The driver maps standard +PWM values (0-255) to these modes as follows: + +* 0 - 84: Quiet Mode +* 85 - 169: Balanced Mode +* 170 - 255: Extreme Mode + +Fan Control +~~~~~~~~~~~ +Fans support standard PWM duty cycle control (0-255). + +Sysfs entries +------------- + +============================== =========================================== +fan1_input Pump speed (in rpm) +fan1_label "Pump" +pwm1 Pump mode control (0-255, see above) +fan2_input Fan 1 speed (in rpm) +fan2_label "Fan 1" +pwm2 Fan 1 duty cycle (0-255) +fan3_input Fan 2 speed (in rpm) +fan3_label "Fan 2" +pwm3 Fan 2 duty cycle (0-255) +fan4_input Fan 3 speed (in rpm) (If supported) +fan4_label "Fan 3" +pwm4 Fan 3 duty cycle (0-255) (If supported) +temp1_input Coolant temperature (millidegrees C) +temp1_label "Coolant temp" +============================== =========================================== + +Debugfs entries +--------------- + +The driver exposes the firmware version via debugfs: +`/sys/kernel/debug/corsair_hydro_platinum-/firmware_version` From fa608755ad64236a75caf7a49d92e7bbe5b63625 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 28 Jan 2026 09:38:44 -0500 Subject: [PATCH 11/46] README.md: Add corsair-hydro-platinum to README Signed-off-by: Jack Greiner --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index a4f06e0..72db34b 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ This is the current state of the drivers in regards to this process: | NZXT Kraken X42/X52/X62/X72 | `nzxt-kraken2` | `kraken2` | in Linux 5.13 ([patch][p-kraken2-v2]) | | NZXT Kraken X53/X63/X73, Z53/Z63/Z73, Kraken 2023 (standard, Elite) | `nzxt-kraken3` | `kraken3` | in Linux 6.9 ([patch][p-kraken3]), Kraken 2023 - in Linux 6.10 ([patch][p-kraken2023]) | | NZXT Smart Device V2/RGB & Fan Controller | `nzxt-smart2` | `nzxtsmart2` | in Linux 5.17 ([patch][p-smart2]) | +| Corsair Hydro H100i/H115i Platinum/SE, H60i/H100i/H115i/H150i Pro XT, iCUE H100i/H115i/H150i Elite RGB | `corsair-hydro-platinum` | `corsair_hydro_platinum` | | This repository contains the latest state of each driver, including features and bug fixes that are not yet submitted upstream. @@ -43,6 +44,7 @@ $ sudo modprobe nzxt-grid3 # NZXT Grid+ V3/Smart Device (V1) $ sudo modprobe nzxt-kraken2 # NZXT Kraken X42/X52/X62/X72 $ sudo modprobe nzxt-kraken3 # NZXT Kraken X53/X63/X73, Z53/Z63/Z73, Kraken 2023 (standard, Elite) $ sudo modprobe nzxt-smart2 # NZXT Smart Device V2/RGB & Fan Controller +$ sudo modprobe corsair-hydro-platinum # Corsair Hydro H100i/H115i Platinum/SE, Pro XT, Elite RGB ``` Those on other distros can install DKMS files using `dkms_install` `Makefile` @@ -74,6 +76,7 @@ $ sudo insmod drivers/hwmon/nzxt-grid3.ko # NZXT Grid+ V3/Smart Device ( $ sudo insmod drivers/hwmon/nzxt-kraken2.ko # NZXT Kraken X42/X52/X62/X72 $ sudo insmod drivers/hwmon/nzxt-kraken3.ko # NZXT Kraken X53/X63/X73, Z53/Z63/Z73, Kraken 2023 (standard, Elite) $ sudo insmod drivers/hwmon/nzxt-smart2.ko # NZXT Smart Device V2/RGB & Fan Controller +$ sudo insmod drivers/hwmon/corsair-hydro-platinum.ko # Corsair Hydro H100i/H115i Platinum/SE, Pro XT, Elite RGB ``` To unload them, use `rmmod` or `modprobe -r`. From a19487893f141fe933322dfe8a828381fef43a68 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Thu, 29 Jan 2026 10:50:01 -0500 Subject: [PATCH 12/46] Revert "corsair-hydro-platinum: Add fallback implementation for when CONFIG_CRC8 is disabled" This reverts commit 3f9dd62a14d8409ff2245291d36dabb6d9611a8d. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 33 +++----------------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index ae2ce68..9eaab02 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -83,9 +83,7 @@ #define OFFSET_PUMP_MODE 20 #define OFFSET_PROFILE_LEN 26 -#if IS_REACHABLE(CONFIG_CRC8) DECLARE_CRC8_TABLE(corsair_crc8_table); -#endif struct hydro_platinum_data { struct hid_device *hdev; @@ -129,32 +127,7 @@ struct hydro_platinum_device_info { /* SMBus standard CRC-8 polynomial x^8 + x^2 + x + 1 (0x07) */ static void hydro_platinum_init_crc(void) { -#if IS_REACHABLE(CONFIG_CRC8) crc8_populate_msb(corsair_crc8_table, 0x07); -#endif -} - -static u8 hydro_platinum_calc_crc(const u8 *data, size_t len, u8 crc) -{ -#if IS_REACHABLE(CONFIG_CRC8) - return crc8(corsair_crc8_table, data, len, crc); -#else - /* Table-less fallback for when CONFIG_CRC8 is disabled */ - size_t i, j; - u8 val; - - for (i = 0; i < len; i++) { - val = data[i]; - crc ^= val; - for (j = 0; j < 8; j++) { - if (crc & 0x80) - crc = (crc << 1) ^ 0x07; - else - crc <<= 1; - } - } - return crc; -#endif } /** @@ -195,8 +168,8 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat /* Calculate CRC over buf[2] to buf[REPORT_LENGTH-1+1] */ /* Payload is buf[1]..buf[64]. CRC is usually last byte of payload. */ - priv->tx_buffer[REPORT_LENGTH] = hydro_platinum_calc_crc(priv->tx_buffer + 2, - REPORT_LENGTH - 2, 0); + priv->tx_buffer[REPORT_LENGTH] = crc8(corsair_crc8_table, priv->tx_buffer + 2, + REPORT_LENGTH - 2, 0); /* Send Report - 65 bytes */ @@ -258,7 +231,7 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu * prevents the driver from processing invalid data, which could otherwise * confuse the device state machine and cause firmware crashes/reboots. */ - if (hydro_platinum_calc_crc(priv->rx_buffer + 1, REPORT_LENGTH - 1, 0) != 0) { + if (crc8(corsair_crc8_table, priv->rx_buffer + 1, REPORT_LENGTH - 1, 0) != 0) { dev_warn_ratelimited(&priv->hdev->dev, "CRC check failed for command %02x - possible userspace collision\n", command); From 1b28bd57015283c380f567af9eea82e677ecf53b Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Thu, 29 Jan 2026 19:43:23 -0500 Subject: [PATCH 13/46] CI: Fix CRC8 build failure on mainline kernel The `corsair-hydro-platinum` driver relies on the kernel's `crc8` library. In recent mainline kernels, `CONFIG_CRC8` was converted to a hidden symbol (prompt removed), which causes `make allnoconfig` to silently ignore `CONFIG_CRC8=y` in all.config This resulted in `modpost` failures due to undefined `crc8` symbols. See: https://github.com/torvalds/linux/commit/aa09b3223c8500e14d072729488124c614e4f220 Hopefully fix this by: 1. Explicitly enabling `CONFIG_CRC8=y` in all.config 2. Patching the kernel's Kconfig in build.yml to restore the "CRC8" prompt. This makes the symbol visible and selectable again during configuration, bypassing the limitation of `allnoconfig` with hidden symbols. This ensures the driver builds correctly on both stable (6.8) and mainline configurations. Signed-off-by: Jack Greiner --- .github/workflows/all.config | 2 ++ .github/workflows/build.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/all.config b/.github/workflows/all.config index d71a42d..82f9c45 100644 --- a/.github/workflows/all.config +++ b/.github/workflows/all.config @@ -1,9 +1,11 @@ CONFIG_MODULES=y CONFIG_INPUT=y CONFIG_HID=y +CONFIG_CRC8=y CONFIG_HID_SUPPORT=y CONFIG_USB_SUPPORT=y CONFIG_USB=y CONFIG_USB_HID=y CONFIG_HWMON=y +CONFIG_CRC8=y CONFIG_WERROR=y diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4cae9e..8c12b9b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,7 @@ jobs: path: linux ref: ${{ matrix.kernel_version }} - run: cp src/.github/workflows/all.config linux/ + - run: sed -i 's/tristate$/tristate "CRC8"/' linux/lib/crc/Kconfig linux/lib/Kconfig 2>/dev/null || true - run: scripts/config --file all.config ${{ matrix.kconfig_pm }} ${{ matrix.kconfig_debug_fs }} working-directory: linux - run: KCONFIG_ALLCONFIG=1 KCFLAGS=-Werror make C=1 allnoconfig From 294d525e0c30148a55e5da6d9a26205ef599489f Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:16:49 -0400 Subject: [PATCH 14/46] corsair-hydro-platinum: Remove tested/untested annotations from source comments Move testing status metadata out of the source code header comment, as this information belongs in commit messages or PR descriptions, especially when mainlining the driver. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 9eaab02..d4b0030 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -12,10 +12,10 @@ * - Fan duty cycle (0-100%) * * Devices supported: - * - Corsair Hydro H100i Platinum / SE (Untested) - * - Corsair Hydro H115i Platinum (Untested) - * - Corsair Hydro H60i / H100i / H115i / H150i Pro XT (Untested) - * - Corsair iCUE H100i / H115i / H150i Elite RGB (Tested) + * - Corsair Hydro H100i Platinum / SE + * - Corsair Hydro H115i Platinum + * - Corsair Hydro H60i / H100i / H115i / H150i Pro XT + * - Corsair iCUE H100i / H115i / H150i Elite RGB * * Technical Description: * The device communicates via USB HID. Unlike standard HID devices, it requires From c0f5ccf8c24b59bde0368d961c7522c937f931e7 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:18:25 -0400 Subject: [PATCH 15/46] corsair-hydro-platinum: Use 'typical' instead of 'standard' for HID description Set Report is part of the HID spec, so calling other devices 'standard' is misleading. Use 'typical' to clarify this device's behavior differs from most HID devices, not from the spec itself. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index d4b0030..3ceb12a 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -18,7 +18,7 @@ * - Corsair iCUE H100i / H115i / H150i Elite RGB * * Technical Description: - * The device communicates via USB HID. Unlike standard HID devices, it requires + * The device communicates via USB HID. Unlike typical HID devices, it requires * commands to be sent via Control Transfers (Set Report, Endpoint 0). * Status reports are received asynchronously via Input Reports on the Interrupt * IN endpoint. From 2632a0caa485a0a1ce1a014bd20ecdf068505ca7 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:18:55 -0400 Subject: [PATCH 16/46] corsair-hydro-platinum: Remove ad-hoc command name from header comment Remove the informal 'Set Cooling' name from the initialization description to avoid confusion with formal HID spec terminology like 'Set Report'. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 3ceb12a..52dac7f 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -24,8 +24,8 @@ * IN endpoint. * * Initialization: - * The device requires an initialization command (Set Cooling) to begin - * reporting status and to set the fans/pump to a safe default state. + * The device requires an initialization command to begin reporting status + * and to set the fans/pump to a safe default state. * * Copyright 2026 Jack Greiner */ From e18fe2d3297f58874a4c0c8cd9fd5bbb186ef14e Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:19:23 -0400 Subject: [PATCH 17/46] corsair-hydro-platinum: Remove unnecessary null check for device info The driver_data pointer is always set in the ID table entries, so it can never be null when probe is called. Remove the dead code. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 52dac7f..2f5e99d 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -688,9 +688,6 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device const struct hydro_platinum_device_info *info = (const struct hydro_platinum_device_info *)id->driver_data; - if (!info) - return -ENODEV; - priv->fan_count = info->fan_count; priv->model_name = info->model_name; From f84408aeb69521daaa29fe47f8aab76fcb024460 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:19:52 -0400 Subject: [PATCH 18/46] corsair-hydro-platinum: Remove redundant comment about waiting for init response The comment about waiting for the init response is both misplaced (appears after the call) and redundant, since hydro_platinum_write_cooling already handles the response wait internally via hydro_platinum_transaction. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 2f5e99d..3d4d2a1 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -724,8 +724,6 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device if (ret) hid_warn(hdev, "initialization command failed: %d\n", ret); - /* Wait for response to init command. hydro_platinum_write_cooling handles the delay. */ - /* Initial update to get firmware version */ hid_info(hdev, "Requesting initial status update...\n"); ret = hydro_platinum_update(priv); From c4cfa24ef7d0b5535a819298872abf7168c37770 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:20:18 -0400 Subject: [PATCH 19/46] corsair-hydro-platinum: Fail probe if device initialization fails Previously, initialization and initial status update failures were logged as warnings but probe continued. This could leave the device in an undefined state. Fail probe entirely if either step fails. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 3d4d2a1..7152df2 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -719,19 +719,21 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device priv->target_fan_duty[i] = 128; /* 50% approx */ } - hid_info(hdev, "Initializing device (Set Cooling: Pump Balanced, Fans 50%%)...\n"); ret = hydro_platinum_write_cooling(priv); - if (ret) - hid_warn(hdev, "initialization command failed: %d\n", ret); + if (ret) { + hid_err(hdev, "initialization command failed: %d\n", ret); + goto fail_and_close; + } /* Initial update to get firmware version */ - hid_info(hdev, "Requesting initial status update...\n"); ret = hydro_platinum_update(priv); - if (ret) - hid_warn(hdev, "initial update failed: %d\n", ret); - else - hid_info(hdev, "Firmware version: %d.%d.%d\n", - priv->fw_version[0], priv->fw_version[1], priv->fw_version[2]); + if (ret) { + hid_err(hdev, "initial status update failed: %d\n", ret); + goto fail_and_close; + } + + hid_info(hdev, "Firmware version: %d.%d.%d\n", + priv->fw_version[0], priv->fw_version[1], priv->fw_version[2]); priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, info->hwmon_name, priv, &hydro_platinum_chip_info, From 4624ea6c1da390ca5307c626295d2f79f232c369 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:21:18 -0400 Subject: [PATCH 20/46] corsair-hydro-platinum: Add reset_resume handler for power management After a USB reset or resume from suspend, the device needs to be re-initialized with cooling settings. Add a reset_resume callback that re-sends the current cooling configuration, following the pattern used by other drivers in this repository. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 7152df2..10889eb 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -754,6 +754,21 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device return ret; } +static int __maybe_unused hydro_platinum_reset_resume(struct hid_device *hdev) +{ + struct hydro_platinum_data *priv = hid_get_drvdata(hdev); + int ret; + + mutex_lock(&priv->lock); + ret = hydro_platinum_write_cooling(priv); + mutex_unlock(&priv->lock); + + if (ret) + hid_err(hdev, "re-initialization (reset_resume) failed with %d\n", ret); + + return ret; +} + static void hydro_platinum_remove(struct hid_device *hdev) { struct hydro_platinum_data *priv = hid_get_drvdata(hdev); @@ -832,6 +847,9 @@ static struct hid_driver hydro_platinum_driver = { .probe = hydro_platinum_probe, .remove = hydro_platinum_remove, .raw_event = hydro_platinum_raw_event, +#ifdef CONFIG_PM + .reset_resume = hydro_platinum_reset_resume, +#endif }; module_hid_driver(hydro_platinum_driver); From 1c62777669ab1431ac32f3ef9cf9465f2c9f06f5 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:21:56 -0400 Subject: [PATCH 21/46] corsair-hydro-platinum: Add mutex_destroy to error and remove paths Add mutex_destroy calls in the probe error path (fail_and_stop) and in the remove function to properly clean up the mutex. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 10889eb..70b4d82 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -751,6 +751,7 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device hid_hw_close(hdev); fail_and_stop: hid_hw_stop(hdev); + mutex_destroy(&priv->lock); return ret; } @@ -777,6 +778,7 @@ static void hydro_platinum_remove(struct hid_device *hdev) hwmon_device_unregister(priv->hwmon_dev); hid_hw_close(hdev); hid_hw_stop(hdev); + mutex_destroy(&priv->lock); } /* Driver Data used for Fan Count */ From 722bac0ef9aaf8e3eeb20b07dc29d38078603c3b Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:22:23 -0400 Subject: [PATCH 22/46] README.md: Improve spacing before inline comments in insmod examples Add spacing before the '#' comment on the corsair-hydro-platinum insmod line to better distinguish the command from the comment. Signed-off-by: Jack Greiner --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 72db34b..384b7c8 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ $ sudo insmod drivers/hwmon/nzxt-grid3.ko # NZXT Grid+ V3/Smart Device ( $ sudo insmod drivers/hwmon/nzxt-kraken2.ko # NZXT Kraken X42/X52/X62/X72 $ sudo insmod drivers/hwmon/nzxt-kraken3.ko # NZXT Kraken X53/X63/X73, Z53/Z63/Z73, Kraken 2023 (standard, Elite) $ sudo insmod drivers/hwmon/nzxt-smart2.ko # NZXT Smart Device V2/RGB & Fan Controller -$ sudo insmod drivers/hwmon/corsair-hydro-platinum.ko # Corsair Hydro H100i/H115i Platinum/SE, Pro XT, Elite RGB +$ sudo insmod drivers/hwmon/corsair-hydro-platinum.ko # Corsair Hydro H100i/H115i Platinum/SE, Pro XT, Elite RGB ``` To unload them, use `rmmod` or `modprobe -r`. From fe16fc3c19a2a90f067c2c3537283f1ab567a2bd Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:32:51 -0400 Subject: [PATCH 23/46] corsair-hydro-platinum: Return -ENODATA when sensor data is stale Return -ENODATA in the read path when the last successful update is older than STATUS_VALIDITY, aligning with the staleness checks used by nzxt-kraken2, nzxt-kraken3, and nzxt-grid3. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 70b4d82..a2eb7b5 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -464,6 +464,9 @@ static int hydro_platinum_read(struct device *dev, enum hwmon_sensor_types type, if (ret < 0) return ret; + if (time_after(jiffies, priv->updated + msecs_to_jiffies(STATUS_VALIDITY))) + return -ENODATA; + switch (type) { case hwmon_fan: if (channel == 0) From 75b0deec8a2b0d43efbe01e472fba93047648620 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:33:26 -0400 Subject: [PATCH 24/46] corsair-hydro-platinum: Increase STATUS_VALIDITY to 2000ms Increase the staleness threshold from 1000ms to 2000ms, giving headroom for one missed update before data is considered stale. This aligns with nzxt-kraken3 which uses the same 2000ms threshold. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index a2eb7b5..bc6d18f 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -56,7 +56,7 @@ /* Constants */ #define REPORT_LENGTH 64 #define RESPONSE_LENGTH 64 -#define STATUS_VALIDITY 1000 /* ms */ +#define STATUS_VALIDITY 2000 /* ms; equivalent to two missed updates */ #define MAX_FAN_COUNT 3 #define CMD_WRITE_PREFIX 0x3f From 439d0ebfb9b24a82c5c654ddd7eba3687bea3fa1 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:34:56 -0400 Subject: [PATCH 25/46] corsair-hydro-platinum: Use late_initcall instead of module_hid_driver Replace module_hid_driver() with explicit __init/__exit functions and late_initcall(). This ensures the HID bus is fully initialized before the driver registers, avoiding probe ordering issues when compiled into the kernel. Aligns with the pattern used by all other drivers in this repository. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index bc6d18f..4901970 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -857,7 +857,19 @@ static struct hid_driver hydro_platinum_driver = { #endif }; -module_hid_driver(hydro_platinum_driver); +static int __init hydro_platinum_init(void) +{ + return hid_register_driver(&hydro_platinum_driver); +} + +static void __exit hydro_platinum_exit(void) +{ + hid_unregister_driver(&hydro_platinum_driver); +} + +/* When compiled into the kernel, initialize after the HID bus */ +late_initcall(hydro_platinum_init); +module_exit(hydro_platinum_exit); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Hwmon driver for Corsair Hydro Platinum / Pro XT / Elite RGB"); From 858f5c66b5744e17784ade757938d5813121f4a4 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:36:07 -0400 Subject: [PATCH 26/46] corsair-hydro-platinum: Reduce probe logging verbosity Change the firmware version message from hid_info to hid_dbg so probe is silent on success. The firmware version remains available via debugfs and when dynamic debug is enabled. Aligns with the quieter probe style used by the other drivers in this repository. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 4901970..b552bff 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -735,8 +735,8 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device goto fail_and_close; } - hid_info(hdev, "Firmware version: %d.%d.%d\n", - priv->fw_version[0], priv->fw_version[1], priv->fw_version[2]); + hid_dbg(hdev, "Firmware version: %d.%d.%d\n", + priv->fw_version[0], priv->fw_version[1], priv->fw_version[2]); priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, info->hwmon_name, priv, &hydro_platinum_chip_info, From 0dc1a49be37ac4f3eb67dd615f964653ecaa4048 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:38:06 -0400 Subject: [PATCH 27/46] corsair-hydro-platinum: Use hid_err/hid_warn consistently for logging Replace dev_err_ratelimited and dev_warn_ratelimited calls with hid_err and hid_warn to match the logging convention used by the other drivers in this repository. The hid_* helpers automatically include the driver name in the output. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index b552bff..f4f0b50 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -204,16 +204,16 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu ret = hydro_platinum_send_command(priv, feature, command, data, data_len); if (ret < 0) { - dev_err_ratelimited(&priv->hdev->dev, "Failed to send command %02x: %d\n", - command, ret); + hid_err(priv->hdev, "Failed to send command %02x: %d\n", + command, ret); return ret; } ret = wait_for_completion_interruptible_timeout(&priv->wait_for_report, msecs_to_jiffies(500)); if (ret == 0) { - dev_warn_ratelimited(&priv->hdev->dev, "Timeout waiting for response to command %02x\n", - command); + hid_warn(priv->hdev, "Timeout waiting for response to command %02x\n", + command); return -ETIMEDOUT; } else if (ret < 0) { return ret; @@ -232,9 +232,9 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu * confuse the device state machine and cause firmware crashes/reboots. */ if (crc8(corsair_crc8_table, priv->rx_buffer + 1, REPORT_LENGTH - 1, 0) != 0) { - dev_warn_ratelimited(&priv->hdev->dev, - "CRC check failed for command %02x - possible userspace collision\n", - command); + hid_warn(priv->hdev, + "CRC check failed for command %02x - possible userspace collision\n", + command); return -EIO; } @@ -538,12 +538,12 @@ static int hydro_platinum_write(struct device *dev, enum hwmon_sensor_types type ret = hydro_platinum_write_cooling(priv); if (ret) { if (channel == 0) - dev_warn_ratelimited(&priv->hdev->dev, - "Failed to set Pump speed: %d\n", ret); + hid_warn(priv->hdev, + "Failed to set Pump speed: %d\n", ret); else - dev_warn_ratelimited(&priv->hdev->dev, - "Failed to set Fan %d speed: %d\n", - channel, ret); + hid_warn(priv->hdev, + "Failed to set Fan %d speed: %d\n", + channel, ret); } break; default: From d5e9508e7e36f8b307433b7a80c683d91223f166 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:45:32 -0400 Subject: [PATCH 28/46] corsair-hydro-platinum: Filter responses by sequence number in raw_event Previously, raw_event would complete on any input report and rely on the CRC check in transaction to detect collisions. This meant that when userspace tools (liquidctl, OpenRGB) were using the device concurrently, we would frequently pick up their response packets, fail CRC or return wrong data, and waste our one chance to receive the correct response. Filter incoming reports in raw_event by comparing the response sequence+feature byte (data[1]) against the expected value from our last command. Responses that do not match are silently ignored, allowing the completion to wait for the correct response or time out gracefully. This makes concurrent userspace access significantly more robust, as transient collisions no longer immediately fail the transaction. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 30 +++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index f4f0b50..f7b03c9 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -111,6 +111,7 @@ struct hydro_platinum_data { u8 fw_version[3]; struct completion wait_for_report; + u8 expected_seq; unsigned long updated; bool valid; @@ -159,6 +160,7 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat /* Sequence and feature/command logic */ priv->sequence = (priv->sequence % 31) + 1; priv->tx_buffer[2] = (priv->sequence << 3) | feature; + priv->expected_seq = priv->tx_buffer[2]; priv->tx_buffer[3] = command; start_at = 4; @@ -220,21 +222,14 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu } /* - * CRC Verification - * liquidctl checks CRC over bytes [1..63] (assuming 64 byte report). - * If standard SMBus CRC-8 algorithm is used, checksumming (Data + CRC) should yield 0. - * - * NOTE: When userspace tools (like OpenRGB or liquidctl) are accessing the device - * concurrently, we may intercept their response packets or see collisions. - * In these cases, the CRC check will often fail (or the sequence number might mismatch). - * This is expected behavior in a multi-client scenario and catching it here - * prevents the driver from processing invalid data, which could otherwise - * confuse the device state machine and cause firmware crashes/reboots. + * CRC Verification: checksumming (Data + CRC) should yield 0 for + * valid packets. Sequence number filtering in raw_event already + * discards responses to other clients, so a CRC failure here + * indicates data corruption rather than a collision. */ if (crc8(corsair_crc8_table, priv->rx_buffer + 1, REPORT_LENGTH - 1, 0) != 0) { hid_warn(priv->hdev, - "CRC check failed for command %02x - possible userspace collision\n", - command); + "CRC check failed for command %02x\n", command); return -EIO; } @@ -349,10 +344,15 @@ static int hydro_platinum_raw_event(struct hid_device *hdev, struct hid_report * size = REPORT_LENGTH + 16; /* - * Copy to RX buffer. - * We accept any input report here to unblock the waiter, and let the - * transaction logic (CRC check) validate if it's the correct response. + * Check if this response matches our expected sequence number. + * The sequence+feature byte is at data[1] in the response. + * If it doesn't match, this is likely a response to a command sent by + * userspace (e.g. liquidctl, OpenRGB) -- silently ignore it and keep + * waiting for our response. */ + if (size >= 2 && data[1] != priv->expected_seq) + return 0; + memcpy(priv->rx_buffer, data, size); complete(&priv->wait_for_report); From 206bebe7ceacb439e9f92a2efc35a7056c2ac042 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:50:38 -0400 Subject: [PATCH 29/46] corsair-hydro-platinum: Add retry loop and sequence validation to transactions Add a second layer of response validation in hydro_platinum_transaction that checks the sequence+feature byte after CRC verification. This catches edge cases where raw_event sequence filtering is insufficient, such as when userspace tools reuse the same sequence number. Wrap the wait+validate logic in a retry loop (up to 3 attempts) so that a single invalid packet does not immediately fail the transaction. On CRC or sequence mismatch, reinitialize the completion and wait for the next report. Timeouts and signal interruptions are not retried. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 71 +++++++++++++++++++------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index f7b03c9..57f165d 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -197,10 +197,13 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat * Sends a command and waits up to 500ms for an Input Report on the Interrupt IN endpoint. * This ensures strict command-response ordering to prevent device confusion. */ +#define TRANSACTION_RETRIES 3 + static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 feature, u8 command, u8 *data, int data_len) { int ret; + int tries; reinit_completion(&priv->wait_for_report); @@ -211,29 +214,59 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu return ret; } - ret = wait_for_completion_interruptible_timeout(&priv->wait_for_report, - msecs_to_jiffies(500)); - if (ret == 0) { - hid_warn(priv->hdev, "Timeout waiting for response to command %02x\n", - command); - return -ETIMEDOUT; - } else if (ret < 0) { - return ret; - } - /* - * CRC Verification: checksumming (Data + CRC) should yield 0 for - * valid packets. Sequence number filtering in raw_event already - * discards responses to other clients, so a CRC failure here - * indicates data corruption rather than a collision. + * Wait for the matching response, retrying if we receive a packet + * that passes raw_event sequence filtering but fails validation + * here (CRC or sequence mismatch). This handles edge cases where + * raw_event filtering alone is insufficient, e.g. sequence number + * reuse by concurrent userspace tools. */ - if (crc8(corsair_crc8_table, priv->rx_buffer + 1, REPORT_LENGTH - 1, 0) != 0) { - hid_warn(priv->hdev, - "CRC check failed for command %02x\n", command); - return -EIO; + for (tries = 0; tries < TRANSACTION_RETRIES; tries++) { + ret = wait_for_completion_interruptible_timeout( + &priv->wait_for_report, msecs_to_jiffies(500)); + if (ret == 0) { + hid_warn(priv->hdev, + "Timeout waiting for response to command %02x\n", + command); + return -ETIMEDOUT; + } else if (ret < 0) { + return ret; + } + + /* + * CRC Verification: checksumming (Data + CRC) should yield 0 + * for valid packets. Sequence number filtering in raw_event + * already discards most responses to other clients, so a CRC + * failure here indicates data corruption or a rare collision + * from sequence number reuse. + */ + if (crc8(corsair_crc8_table, priv->rx_buffer + 1, + REPORT_LENGTH - 1, 0) != 0) { + hid_dbg(priv->hdev, + "CRC check failed for command %02x (attempt %d/%d)\n", + command, tries + 1, TRANSACTION_RETRIES); + reinit_completion(&priv->wait_for_report); + continue; + } + + /* Verify the sequence+feature byte matches what we sent */ + if (priv->rx_buffer[1] != priv->expected_seq) { + hid_dbg(priv->hdev, + "Sequence mismatch for command %02x: expected %02x, got %02x (attempt %d/%d)\n", + command, priv->expected_seq, + priv->rx_buffer[1], + tries + 1, TRANSACTION_RETRIES); + reinit_completion(&priv->wait_for_report); + continue; + } + + return 0; } - return 0; + hid_warn(priv->hdev, + "Failed to get valid response for command %02x after %d attempts\n", + command, TRANSACTION_RETRIES); + return -EIO; } /** From e12263bf6176cbaa4b5d6eb31598745d0864019f Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 06:55:46 -0400 Subject: [PATCH 30/46] corsair-hydro-platinum: Fix checkpatch style warning Move first argument of wait_for_completion_interruptible_timeout onto the same line to avoid ending a line with '('. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 57f165d..4779e4e 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -222,8 +222,8 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu * reuse by concurrent userspace tools. */ for (tries = 0; tries < TRANSACTION_RETRIES; tries++) { - ret = wait_for_completion_interruptible_timeout( - &priv->wait_for_report, msecs_to_jiffies(500)); + ret = wait_for_completion_interruptible_timeout(&priv->wait_for_report, + msecs_to_jiffies(500)); if (ret == 0) { hid_warn(priv->hdev, "Timeout waiting for response to command %02x\n", From f73327dbb1ebdb70c78ca21d8187555578ada73a Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 07:12:01 -0400 Subject: [PATCH 31/46] corsair-hydro-platinum: Fix rx_buffer data race between raw_event and transaction raw_event runs in interrupt context and writes to rx_buffer without synchronization. A late response from a timed-out transaction, an unsolicited firmware report, or a HIDRAW request from userspace could overwrite rx_buffer while the transaction code is reading it. Add a spinlock (rx_lock) to protect rx_buffer writes in raw_event and reads in hydro_platinum_transaction. The transaction code copies rx_buffer to a local stack buffer under the lock, validates the copy, then writes it back only on success. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 4779e4e..d377eeb 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -89,6 +90,7 @@ struct hydro_platinum_data { struct hid_device *hdev; struct device *hwmon_dev; struct mutex lock; /* lock for transfer buffer and data access */ + spinlock_t rx_lock; /* lock for rx_buffer access from raw_event */ u8 *tx_buffer; u8 *rx_buffer; u8 sequence; @@ -202,6 +204,7 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 feature, u8 command, u8 *data, int data_len) { + u8 rx_copy[REPORT_LENGTH + 16]; int ret; int tries; @@ -233,6 +236,14 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu return ret; } + /* + * Copy rx_buffer under the spinlock to prevent a late or + * unsolicited raw_event from overwriting it mid-read. + */ + spin_lock(&priv->rx_lock); + memcpy(rx_copy, priv->rx_buffer, sizeof(rx_copy)); + spin_unlock(&priv->rx_lock); + /* * CRC Verification: checksumming (Data + CRC) should yield 0 * for valid packets. Sequence number filtering in raw_event @@ -240,7 +251,7 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu * failure here indicates data corruption or a rare collision * from sequence number reuse. */ - if (crc8(corsair_crc8_table, priv->rx_buffer + 1, + if (crc8(corsair_crc8_table, rx_copy + 1, REPORT_LENGTH - 1, 0) != 0) { hid_dbg(priv->hdev, "CRC check failed for command %02x (attempt %d/%d)\n", @@ -250,16 +261,18 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu } /* Verify the sequence+feature byte matches what we sent */ - if (priv->rx_buffer[1] != priv->expected_seq) { + if (rx_copy[1] != priv->expected_seq) { hid_dbg(priv->hdev, "Sequence mismatch for command %02x: expected %02x, got %02x (attempt %d/%d)\n", command, priv->expected_seq, - priv->rx_buffer[1], + rx_copy[1], tries + 1, TRANSACTION_RETRIES); reinit_completion(&priv->wait_for_report); continue; } + /* Validated -- copy back for callers to consume */ + memcpy(priv->rx_buffer, rx_copy, sizeof(rx_copy)); return 0; } @@ -386,7 +399,9 @@ static int hydro_platinum_raw_event(struct hid_device *hdev, struct hid_report * if (size >= 2 && data[1] != priv->expected_seq) return 0; + spin_lock(&priv->rx_lock); memcpy(priv->rx_buffer, data, size); + spin_unlock(&priv->rx_lock); complete(&priv->wait_for_report); return 0; @@ -714,6 +729,7 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device return -ENOMEM; mutex_init(&priv->lock); + spin_lock_init(&priv->rx_lock); init_completion(&priv->wait_for_report); hid_set_drvdata(hdev, priv); From 657842edac61d53721e38f14eb5e15f18d3baae2 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 07:12:52 -0400 Subject: [PATCH 32/46] corsair-hydro-platinum: Use mutex_lock_interruptible in update and write paths Use mutex_lock_interruptible instead of mutex_lock so that userspace can abort a request (e.g. via signal) if it is blocked waiting for the lock, rather than hanging indefinitely. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index d377eeb..00bdc17 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -418,7 +418,9 @@ static int hydro_platinum_update(struct hydro_platinum_data *priv) { int ret; - mutex_lock(&priv->lock); + ret = mutex_lock_interruptible(&priv->lock); + if (ret) + return ret; if (time_after(jiffies, priv->updated + msecs_to_jiffies(STATUS_VALIDITY)) || !priv->valid) { @@ -541,10 +543,12 @@ static int hydro_platinum_write(struct device *dev, enum hwmon_sensor_types type int channel, long val) { struct hydro_platinum_data *priv = dev_get_drvdata(dev); - int ret = 0; + int ret; int i; - mutex_lock(&priv->lock); + ret = mutex_lock_interruptible(&priv->lock); + if (ret) + return ret; switch (type) { case hwmon_pwm: From c4d0afbf2dce9de618afe25c79d17916c9b728f8 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 07:15:21 -0400 Subject: [PATCH 33/46] corsair-hydro-platinum: Replace priv->valid with jiffies initialization pattern Instead of using a separate 'valid' boolean to track whether the first status report has been received, initialize priv->updated to STATUS_VALIDITY in the past. This makes the time_after check in the update function naturally trigger the first update without needing a special case. This follows the same pattern used by nzxt-kraken2, nzxt-kraken3, and nzxt-grid3. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 00bdc17..39e6927 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -116,7 +116,6 @@ struct hydro_platinum_data { u8 expected_seq; unsigned long updated; - bool valid; struct dentry *debugfs; }; @@ -422,8 +421,7 @@ static int hydro_platinum_update(struct hydro_platinum_data *priv) if (ret) return ret; - if (time_after(jiffies, priv->updated + msecs_to_jiffies(STATUS_VALIDITY)) || - !priv->valid) { + if (time_after(jiffies, priv->updated + msecs_to_jiffies(STATUS_VALIDITY))) { reinit_completion(&priv->wait_for_report); ret = hydro_platinum_transaction(priv, FEATURE_COOLING, CMD_GET_STATUS, NULL, 0); @@ -433,11 +431,9 @@ static int hydro_platinum_update(struct hydro_platinum_data *priv) /* Data is now in priv->rx_buffer (populated by raw_event) */ /* Firmware Version: res[2] (Major << 4 | Minor), res[3] (Patch) */ - if (!priv->valid) { - priv->fw_version[0] = priv->rx_buffer[2] >> 4; - priv->fw_version[1] = priv->rx_buffer[2] & 0x0f; - priv->fw_version[2] = priv->rx_buffer[3]; - } + priv->fw_version[0] = priv->rx_buffer[2] >> 4; + priv->fw_version[1] = priv->rx_buffer[2] & 0x0f; + priv->fw_version[2] = priv->rx_buffer[3]; /* Temp */ priv->liquid_temp = ((int)priv->rx_buffer[8] * 1000) + @@ -474,7 +470,6 @@ static int hydro_platinum_update(struct hydro_platinum_data *priv) } priv->updated = jiffies; - priv->valid = true; } ret = 0; @@ -735,6 +730,14 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device mutex_init(&priv->lock); spin_lock_init(&priv->rx_lock); init_completion(&priv->wait_for_report); + + /* + * Initialize ->updated to STATUS_VALIDITY in the past, making + * the initial empty data invalid for hydro_platinum_read without + * the need for a special case there. + */ + priv->updated = jiffies - msecs_to_jiffies(STATUS_VALIDITY); + hid_set_drvdata(hdev, priv); /* From 0451b8e6d05e05229bc37cf5d442bc8f619b6c43 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 07:21:59 -0400 Subject: [PATCH 34/46] corsair-hydro-platinum: Extract cooling payload init and build Fan 3 from scratch Extract the common cooling payload prefix setup into hydro_platinum_init_cooling_payload() and use it for both the main and secondary (Fan 3) commands. The secondary command payload is now built from scratch rather than copying the main payload and overriding fields. This makes it clear exactly which fields are set in each command without needing to mentally track what survived the copy. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 46 ++++++++++++-------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 39e6927..29da987 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -281,6 +281,17 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu return -EIO; } +/* Initialize the common cooling payload prefix (shared by main and secondary commands) */ +static void hydro_platinum_init_cooling_payload(u8 *data, int size) +{ + memset(data, 0, size); + data[0] = 0x00; + data[1] = 0xff; + data[2] = 0x05; + memset(data + 3, 0xff, 5); + data[OFFSET_PROFILE_LEN] = 7; +} + /** * hydro_platinum_write_cooling - Commit target fan/pump settings to the device. * @priv: Driver data. @@ -294,21 +305,7 @@ static int hydro_platinum_write_cooling(struct hydro_platinum_data *priv) int ret; u8 data[60]; - /* - * Construct the 60-byte payload for the Command 0x14. - * The payload is appended to the 4-byte header in hydro_platinum_send_command. - */ - - /* - * Payload Prefix: 00 ff 05 ff ff ff ff ff - */ - memset(data, 0, sizeof(data)); - data[0] = 0x00; - data[1] = 0xff; - data[2] = 0x05; - memset(data + 3, 0xff, 5); - - data[OFFSET_PROFILE_LEN] = 7; + hydro_platinum_init_cooling_payload(data, sizeof(data)); /* Pump Mode */ data[OFFSET_PUMP_MODE] = priv->target_pump_mode; @@ -341,22 +338,21 @@ static int hydro_platinum_write_cooling(struct hydro_platinum_data *priv) * reversing this order may cause the device to stall or return -EPIPE. */ - /* Fan 3 (Index 2) - Requires Feature Cooling 2 */ + /* + * Fan 3 (Index 2) - Uses the secondary cooling feature (0x03). + * The secondary payload has the same structure as the main one, + * but only the Fan 1 slot is populated (with Fan 3's settings). + * Fan 2 slot and pump mode must still be present in the payload. + */ if (priv->fan_count >= 3) { u8 data2[60]; - memcpy(data2, data, sizeof(data)); + hydro_platinum_init_cooling_payload(data2, sizeof(data2)); - /* Ensure Pump Mode is set correctly even in secondary command */ + /* Pump mode must be set in both main and secondary commands */ data2[OFFSET_PUMP_MODE] = priv->target_pump_mode; - /* Reset Fan 1/2 slots */ - data2[OFFSET_FAN1_MODE] = 0; - data2[OFFSET_FAN1_DUTY] = 0; - data2[OFFSET_FAN2_MODE] = 0; - data2[OFFSET_FAN2_DUTY] = 0; - - /* Fan 3 goes into Fan 1 slot */ + /* Fan 3 settings go into the Fan 1 slot of the secondary command */ data2[OFFSET_FAN1_MODE] = priv->target_fan_mode[2]; if (priv->target_fan_mode[2] == FAN_MODE_FIXED_DUTY) data2[OFFSET_FAN1_DUTY] = priv->target_fan_duty[2]; From 7d31c939f37a1087e8ed444147ff8c6348906a8e Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 07:23:43 -0400 Subject: [PATCH 35/46] corsair-hydro-platinum: Remove unnecessary comments and unused RESPONSE_LENGTH Remove the redundant section header comments ('USB Vendor/Product IDs', 'Constants') and the unused RESPONSE_LENGTH define. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 29da987..a1664fa 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -51,12 +51,9 @@ #define DRIVER_NAME "corsair_hydro_platinum" -/* USB Vendor/Product IDs */ #define USB_VENDOR_ID_CORSAIR 0x1b1c -/* Constants */ #define REPORT_LENGTH 64 -#define RESPONSE_LENGTH 64 #define STATUS_VALIDITY 2000 /* ms; equivalent to two missed updates */ #define MAX_FAN_COUNT 3 From 3df0f92892909e379c26e5bcf48214a318f0a272 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 07:24:11 -0400 Subject: [PATCH 36/46] corsair-hydro-platinum: Document sequence number field Add an inline comment to the sequence field explaining it is a protocol sequence number that cycles through 1-31. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index a1664fa..e4c4bdf 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -90,7 +90,7 @@ struct hydro_platinum_data { spinlock_t rx_lock; /* lock for rx_buffer access from raw_event */ u8 *tx_buffer; u8 *rx_buffer; - u8 sequence; + u8 sequence; /* protocol sequence number, cycles 1-31 */ /* Sensor values */ u16 fan_speeds[MAX_FAN_COUNT]; From 4de067065ff8c9371d0b443a4051f11d2f97b729 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 07:24:31 -0400 Subject: [PATCH 37/46] corsair-hydro-platinum: Simplify report ID comment Replace the verbose explanation of the 65-byte buffer layout with a concise comment matching the usbhid convention: byte 0 is the report number, report data starts at byte 1. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index e4c4bdf..dcebabe 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -145,14 +145,8 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat int ret; int start_at; - /* - * Construct 65-byte buffer with Report ID 0 padding. - * Some devices/firmware revisions require the alignment of 64-byte payload - * to be offset by the Report ID byte even in Control Transfers. - */ + /* Byte 0 is the report number. Report data starts at byte 1. */ memset(priv->tx_buffer, 0, REPORT_LENGTH + 1); - - priv->tx_buffer[0] = 0x00; /* Report ID Padding */ priv->tx_buffer[1] = CMD_WRITE_PREFIX; /* Sequence and feature/command logic */ From addba4386ea34ffec07eabf708b539e99e7a4a35 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 07:26:19 -0400 Subject: [PATCH 38/46] corsair-hydro-platinum: Fix misleading CRC range comment The old comment described the CRC range with confusing arithmetic that did not match the code. Replace with a clear description of the CRC-8 calculation: covers buf[2] through buf[REPORT_LENGTH-1], result stored in buf[REPORT_LENGTH]. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index dcebabe..7eedec6 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -160,8 +160,11 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat memcpy(priv->tx_buffer + start_at, data, min(data_len, REPORT_LENGTH - start_at - 1)); - /* Calculate CRC over buf[2] to buf[REPORT_LENGTH-1+1] */ - /* Payload is buf[1]..buf[64]. CRC is usually last byte of payload. */ + /* + * CRC-8 (SMBus polynomial) over buf[2] through buf[REPORT_LENGTH-1]. + * The result is placed in buf[REPORT_LENGTH], the last byte of the + * 65-byte report. The device validates this on receipt. + */ priv->tx_buffer[REPORT_LENGTH] = crc8(corsair_crc8_table, priv->tx_buffer + 2, REPORT_LENGTH - 2, 0); From ea41fedab6216ab44b8413c0d089a4729228d39e Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 07:26:52 -0400 Subject: [PATCH 39/46] corsair-hydro-platinum: Remove redundant inline comments in send_command Remove comments that restate what the code already says: 'Send Report', 'Use HID_REQ_SET_REPORT', 'Report ID', and 'raw_request returns number of bytes written on success'. The function names and parameters are self-documenting. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 7eedec6..41a0c5d 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -168,13 +168,8 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat priv->tx_buffer[REPORT_LENGTH] = crc8(corsair_crc8_table, priv->tx_buffer + 2, REPORT_LENGTH - 2, 0); - /* Send Report - 65 bytes */ - - /* Use HID_REQ_SET_REPORT (Control Transfer) */ - ret = hid_hw_raw_request(priv->hdev, 0 /* Report ID */, priv->tx_buffer, REPORT_LENGTH + 1, + ret = hid_hw_raw_request(priv->hdev, 0, priv->tx_buffer, REPORT_LENGTH + 1, HID_OUTPUT_REPORT, HID_REQ_SET_REPORT); - - /* raw_request returns number of bytes written on success */ if (ret > 0) ret = 0; From 1d9718faaeb9a7184502dd8aef834cc044efa1f5 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 07:28:22 -0400 Subject: [PATCH 40/46] corsair-hydro-platinum: Fix checkpatch alignment warning Align the second argument of wait_for_completion_interruptible_timeout to match the open parenthesis. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 41a0c5d..cd56255 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -214,7 +214,7 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu */ for (tries = 0; tries < TRANSACTION_RETRIES; tries++) { ret = wait_for_completion_interruptible_timeout(&priv->wait_for_report, - msecs_to_jiffies(500)); + msecs_to_jiffies(500)); if (ret == 0) { hid_warn(priv->hdev, "Timeout waiting for response to command %02x\n", From b1908470159ee99348b8ef26801025e4218e35d2 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 15:12:00 -0400 Subject: [PATCH 41/46] corsair-hydro-platinum: Clarify raw_event comment about incoming data Replace the vague comment about driver buffer layout expectations with a concise description of what the code actually does: clamp the incoming report payload to the rx_buffer size. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index cd56255..b4e8887 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -364,12 +364,7 @@ static int hydro_platinum_raw_event(struct hid_device *hdev, struct hid_report * if (report->type != HID_INPUT_REPORT) return 0; - /* - * The driver buffer expects [0]=ReportID, [1]=Prefix. - * We treat the raw data as the payload. - */ - - /* Safety check size */ + /* Clamp incoming report payload to rx_buffer size */ if (size > REPORT_LENGTH + 16) size = REPORT_LENGTH + 16; From 98bccfad56ae97cc57100a8639526b50c791975a Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 15:15:13 -0400 Subject: [PATCH 42/46] corsair-hydro-platinum: Fix off-by-one in memcpy size for command payload The max data length calculation used REPORT_LENGTH - start_at - 1, which is 59 bytes. The correct limit is REPORT_LENGTH - start_at (60 bytes) -- data occupies bytes 4 through 63, with byte 64 reserved for CRC. The old calculation left one payload byte unused. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index b4e8887..2b22773 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -158,7 +158,7 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat if (data && data_len > 0) memcpy(priv->tx_buffer + start_at, data, - min(data_len, REPORT_LENGTH - start_at - 1)); + min(data_len, REPORT_LENGTH - start_at)); /* * CRC-8 (SMBus polynomial) over buf[2] through buf[REPORT_LENGTH-1]. From fb9c8b611d7ff162695726755ea241da435a4057 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 15:20:09 -0400 Subject: [PATCH 43/46] corsair-hydro-platinum: Use loop with offset table for fan sensor parsing Replace the manually unrolled fan parsing with a loop over an offset table. The fan data offsets (14, 21, 42) are irregular so a simple pattern isn't possible, but a lookup table with a loop is cleaner than three near-identical if blocks. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 28 ++++++++------------------ 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 2b22773..d260c92 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -420,33 +420,21 @@ static int hydro_platinum_update(struct hydro_platinum_data *priv) ((int)priv->rx_buffer[7] * 1000 / 255); /* - * Parse Sensor Data: - * - Fan 1 Speed: Offset 14, Duty: Offset 15 - * - Fan 2 Speed: Offset 21, Duty: Offset 22 - * - Fan 3 Speed: Offset 42, Duty: Offset 43 - * - Pump Speed: Offset 28, Duty: Offset 29 + * Parse sensor data. Each channel has duty at the base + * offset and speed as a le16 at base+1. */ + static const u8 fan_offsets[] = { 14, 21, 42 }; + int i; /* Pump (Base 28) */ priv->pump_speed = get_unaligned_le16(priv->rx_buffer + 28 + 1); priv->pump_duty = priv->rx_buffer[28]; - /* Fan 1 (Base 14) */ - if (priv->fan_count >= 1) { - priv->fan_speeds[0] = get_unaligned_le16(priv->rx_buffer + 14 + 1); - priv->fan_duty[0] = priv->rx_buffer[14]; - } - - /* Fan 2 (Base 21) */ - if (priv->fan_count >= 2) { - priv->fan_speeds[1] = get_unaligned_le16(priv->rx_buffer + 21 + 1); - priv->fan_duty[1] = priv->rx_buffer[21]; - } + for (i = 0; i < priv->fan_count; i++) { + u8 base = fan_offsets[i]; - /* Fan 3 (Base 42) */ - if (priv->fan_count >= 3) { - priv->fan_speeds[2] = get_unaligned_le16(priv->rx_buffer + 42 + 1); - priv->fan_duty[2] = priv->rx_buffer[42]; + priv->fan_speeds[i] = get_unaligned_le16(priv->rx_buffer + base + 1); + priv->fan_duty[i] = priv->rx_buffer[base]; } priv->updated = jiffies; From a733725d54427c0e67486ddac9f6929a701baabf Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 15:23:40 -0400 Subject: [PATCH 44/46] corsair-hydro-platinum: Remove unnecessary safety padding from buffer allocations The tx and rx buffers were allocated with 16 bytes of extra padding that was never intended to be accessed. This masks bugs rather than preventing them. Size the buffers to exactly what the protocol requires: - tx_buffer: REPORT_LENGTH + 1 (report ID byte + 64-byte payload) - rx_buffer: REPORT_LENGTH (64-byte input report) Also update the raw_event size clamp and the transaction local copy to match. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index d260c92..20ec7ce 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -192,7 +192,7 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 feature, u8 command, u8 *data, int data_len) { - u8 rx_copy[REPORT_LENGTH + 16]; + u8 rx_copy[REPORT_LENGTH]; int ret; int tries; @@ -365,8 +365,8 @@ static int hydro_platinum_raw_event(struct hid_device *hdev, struct hid_report * return 0; /* Clamp incoming report payload to rx_buffer size */ - if (size > REPORT_LENGTH + 16) - size = REPORT_LENGTH + 16; + if (size > REPORT_LENGTH) + size = REPORT_LENGTH; /* * Check if this response matches our expected sequence number. @@ -686,12 +686,11 @@ static int hydro_platinum_probe(struct hid_device *hdev, const struct hid_device return -ENOMEM; priv->hdev = hdev; - /* Buffers need to be large enough + safety */ - priv->tx_buffer = devm_kzalloc(&hdev->dev, REPORT_LENGTH + 16, GFP_KERNEL); + priv->tx_buffer = devm_kzalloc(&hdev->dev, REPORT_LENGTH + 1, GFP_KERNEL); if (!priv->tx_buffer) return -ENOMEM; - priv->rx_buffer = devm_kzalloc(&hdev->dev, REPORT_LENGTH + 16, GFP_KERNEL); + priv->rx_buffer = devm_kzalloc(&hdev->dev, REPORT_LENGTH, GFP_KERNEL); if (!priv->rx_buffer) return -ENOMEM; From 0e07e6a458c5dcf00bcfc59223a67f4b2d289157 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 16:00:47 -0400 Subject: [PATCH 45/46] corsair-hydro-platinum: Move TRANSACTION_RETRIES define to fix -Werror build The TRANSACTION_RETRIES define was placed between the kerneldoc comment and hydro_platinum_transaction(), causing the doc parser to associate the comment with the macro instead of the function. With -Werror this becomes a build failure. Move the define to the constants section at the top of the file. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 20ec7ce..5c3bf8b 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -62,6 +62,7 @@ #define FEATURE_COOLING_FAN3 0x03 /* Extension: Fan 3 only (Main report is full) */ #define CMD_GET_STATUS 0xff #define CMD_SET_COOLING 0x14 +#define TRANSACTION_RETRIES 3 /* Pump Modes */ #define PUMP_MODE_QUIET 0x00 @@ -187,8 +188,6 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat * Sends a command and waits up to 500ms for an Input Report on the Interrupt IN endpoint. * This ensures strict command-response ordering to prevent device confusion. */ -#define TRANSACTION_RETRIES 3 - static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 feature, u8 command, u8 *data, int data_len) { From d81232f8397d4796af3f28648eba45036950bd93 Mon Sep 17 00:00:00 2001 From: Jack Greiner Date: Wed, 25 Mar 2026 16:51:14 -0400 Subject: [PATCH 46/46] corsair-hydro-platinum: Remove sequence number filtering and validation Testing on hardware revealed that the device uses its own independent sequence counter in responses rather than echoing the sequence number from the command. This caused raw_event to drop every valid response (sequence mismatch), resulting in transaction timeouts during probe. Remove sequence filtering from raw_event and sequence validation from the transaction retry loop. CRC verification is now the sole response validation mechanism, matching what liquidctl does. The retry loop and spinlock remain to handle concurrent userspace access gracefully. Signed-off-by: Jack Greiner --- drivers/hwmon/corsair-hydro-platinum.c | 43 +++++++------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/drivers/hwmon/corsair-hydro-platinum.c b/drivers/hwmon/corsair-hydro-platinum.c index 5c3bf8b..cce7aef 100644 --- a/drivers/hwmon/corsair-hydro-platinum.c +++ b/drivers/hwmon/corsair-hydro-platinum.c @@ -111,7 +111,6 @@ struct hydro_platinum_data { u8 fw_version[3]; struct completion wait_for_report; - u8 expected_seq; unsigned long updated; struct dentry *debugfs; @@ -153,7 +152,6 @@ static int hydro_platinum_send_command(struct hydro_platinum_data *priv, u8 feat /* Sequence and feature/command logic */ priv->sequence = (priv->sequence % 31) + 1; priv->tx_buffer[2] = (priv->sequence << 3) | feature; - priv->expected_seq = priv->tx_buffer[2]; priv->tx_buffer[3] = command; start_at = 4; @@ -205,11 +203,10 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu } /* - * Wait for the matching response, retrying if we receive a packet - * that passes raw_event sequence filtering but fails validation - * here (CRC or sequence mismatch). This handles edge cases where - * raw_event filtering alone is insufficient, e.g. sequence number - * reuse by concurrent userspace tools. + * Wait for a valid response, retrying if the CRC check fails. + * CRC failures can occur when userspace tools (liquidctl, OpenRGB) + * are accessing the device concurrently via HIDRAW, causing us to + * intercept their response packets. */ for (tries = 0; tries < TRANSACTION_RETRIES; tries++) { ret = wait_for_completion_interruptible_timeout(&priv->wait_for_report, @@ -233,10 +230,9 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu /* * CRC Verification: checksumming (Data + CRC) should yield 0 - * for valid packets. Sequence number filtering in raw_event - * already discards most responses to other clients, so a CRC - * failure here indicates data corruption or a rare collision - * from sequence number reuse. + * for valid packets. The device uses its own sequence counter + * so we cannot match by sequence number. CRC is our only + * validation option. */ if (crc8(corsair_crc8_table, rx_copy + 1, REPORT_LENGTH - 1, 0) != 0) { @@ -247,18 +243,7 @@ static int hydro_platinum_transaction(struct hydro_platinum_data *priv, u8 featu continue; } - /* Verify the sequence+feature byte matches what we sent */ - if (rx_copy[1] != priv->expected_seq) { - hid_dbg(priv->hdev, - "Sequence mismatch for command %02x: expected %02x, got %02x (attempt %d/%d)\n", - command, priv->expected_seq, - rx_copy[1], - tries + 1, TRANSACTION_RETRIES); - reinit_completion(&priv->wait_for_report); - continue; - } - - /* Validated -- copy back for callers to consume */ + /* CRC valid -- copy back for callers to consume */ memcpy(priv->rx_buffer, rx_copy, sizeof(rx_copy)); return 0; } @@ -368,15 +353,11 @@ static int hydro_platinum_raw_event(struct hid_device *hdev, struct hid_report * size = REPORT_LENGTH; /* - * Check if this response matches our expected sequence number. - * The sequence+feature byte is at data[1] in the response. - * If it doesn't match, this is likely a response to a command sent by - * userspace (e.g. liquidctl, OpenRGB) -- silently ignore it and keep - * waiting for our response. + * The device uses its own sequence counter in responses rather than + * echoing ours, so we cannot filter by sequence number here. + * Accept any input report and let the transaction logic (CRC check) + * validate the response. */ - if (size >= 2 && data[1] != priv->expected_seq) - return 0; - spin_lock(&priv->rx_lock); memcpy(priv->rx_buffer, data, size); spin_unlock(&priv->rx_lock);