// SPDX-License-Identifier: GPL-2.0+ /* * hwmon driver for Asus ROG Ryujin II 360 AIO cooler. * * Copyright 2024 Aleksa Savic */ #include #include #include #include #include #include #include #define DRIVER_NAME "asus_rog_ryujin" #define USB_VENDOR_ID_ASUS_ROG 0x0b05 #define USB_PRODUCT_ID_RYUJIN_AIO 0x1988 /* ASUS ROG RYUJIN II 360 */ #define STATUS_VALIDITY 1500 /* ms */ #define MAX_REPORT_LENGTH 65 /* Cooler status report offsets */ #define RYUJIN_TEMP_SENSOR_1 3 #define RYUJIN_TEMP_SENSOR_2 4 #define RYUJIN_PUMP_SPEED 5 #define RYUJIN_INTERNAL_FAN_SPEED 7 /* Cooler duty report offsets */ #define RYUJIN_PUMP_DUTY 4 #define RYUJIN_INTERNAL_FAN_DUTY 5 /* Controller status (speeds) report offsets */ #define RYUJIN_CONTROLLER_SPEED_1 5 #define RYUJIN_CONTROLLER_SPEED_2 7 #define RYUJIN_CONTROLLER_SPEED_3 9 #define RYUJIN_CONTROLLER_SPEED_4 3 /* Controller duty report offsets */ #define RYUJIN_CONTROLLER_DUTY 4 /* Control commands and their inner offsets */ #define RYUJIN_CMD_PREFIX 0xEC static const u8 get_cooler_status_cmd[] = { RYUJIN_CMD_PREFIX, 0x99 }; static const u8 get_cooler_duty_cmd[] = { RYUJIN_CMD_PREFIX, 0x9A }; static const u8 get_controller_speed_cmd[] = { RYUJIN_CMD_PREFIX, 0xA0 }; static const u8 get_controller_duty_cmd[] = { RYUJIN_CMD_PREFIX, 0xA1 }; #define RYUJIN_SET_COOLER_PUMP_DUTY_OFFSET 3 #define RYUJIN_SET_COOLER_FAN_DUTY_OFFSET 4 static const u8 set_cooler_duty_cmd[] = { RYUJIN_CMD_PREFIX, 0x1A, 0x00, 0x00, 0x00 }; #define RYUJIN_SET_CONTROLLER_FAN_DUTY_OFFSET 4 static const u8 set_controller_duty_cmd[] = { RYUJIN_CMD_PREFIX, 0x21, 0x00, 0x00, 0x00 }; /* Command lengths */ #define GET_CMD_LENGTH 2 /* Same length for all get commands */ #define SET_CMD_LENGTH 5 /* Same length for all set commands */ /* Command response headers */ #define RYUJIN_GET_COOLER_STATUS_CMD_RESPONSE 0x19 #define RYUJIN_GET_COOLER_DUTY_CMD_RESPONSE 0x1A #define RYUJIN_GET_CONTROLLER_SPEED_CMD_RESPONSE 0x20 #define RYUJIN_GET_CONTROLLER_DUTY_CMD_RESPONSE 0x21 static const char *const rog_ryujin_temp_label[] = { "Coolant temp" }; static const char *const rog_ryujin_speed_label[] = { "Pump speed", "Internal fan speed", "Controller fan 1 speed", "Controller fan 2 speed", "Controller fan 3 speed", "Controller fan 4 speed", }; struct rog_ryujin_data { struct hid_device *hdev; struct device *hwmon_dev; /* For locking access to buffer */ struct mutex buffer_lock; /* For queueing multiple readers */ struct mutex status_report_request_mutex; /* For reinitializing the completions below */ spinlock_t status_report_request_lock; struct completion cooler_status_received; struct completion controller_status_received; struct completion cooler_duty_received; struct completion controller_duty_received; struct completion cooler_duty_set; struct completion controller_duty_set; /* Sensor data */ s32 temp_input[1]; u16 speed_input[6]; /* Pump, internal fan and four controller fan speeds in RPM */ u8 duty_input[3]; /* Pump, internal fan and controller fan duty in PWM */ u8 *buffer; unsigned long updated; /* jiffies */ }; static int rog_ryujin_percent_to_pwm(u16 val) { return DIV_ROUND_CLOSEST(val * 255, 100); } static int rog_ryujin_pwm_to_percent(long val) { return DIV_ROUND_CLOSEST(val * 100, 255); } static umode_t rog_ryujin_is_visible(const void *data, enum hwmon_sensor_types type, u32 attr, int channel) { switch (type) { case hwmon_temp: switch (attr) { case hwmon_temp_label: case hwmon_temp_input: return 0444; default: break; } break; case hwmon_fan: switch (attr) { case hwmon_fan_label: case hwmon_fan_input: return 0444; default: break; } break; case hwmon_pwm: switch (attr) { case hwmon_pwm_input: return 0644; default: break; } break; default: break; } return 0; } /* Writes the command to the device with the rest of the report filled with zeroes */ static int rog_ryujin_write_expanded(struct rog_ryujin_data *priv, const u8 *cmd, int cmd_length) { int ret; mutex_lock(&priv->buffer_lock); memcpy_and_pad(priv->buffer, MAX_REPORT_LENGTH, cmd, cmd_length, 0x00); ret = hid_hw_output_report(priv->hdev, priv->buffer, MAX_REPORT_LENGTH); mutex_unlock(&priv->buffer_lock); return ret; } /* Assumes priv->status_report_request_mutex is locked */ static int rog_ryujin_execute_cmd(struct rog_ryujin_data *priv, const u8 *cmd, int cmd_length, struct completion *status_completion) { int ret; /* * Disable raw event parsing for a moment to safely reinitialize the * completion. Reinit is done because hidraw could have triggered * the raw event parsing and marked the passed in completion as done. */ spin_lock_bh(&priv->status_report_request_lock); reinit_completion(status_completion); spin_unlock_bh(&priv->status_report_request_lock); /* Send command for getting data */ ret = rog_ryujin_write_expanded(priv, cmd, cmd_length); if (ret < 0) return ret; ret = wait_for_completion_interruptible_timeout(status_completion, msecs_to_jiffies(STATUS_VALIDITY)); if (ret == 0) return -ETIMEDOUT; else if (ret < 0) return ret; return 0; } static int rog_ryujin_get_status(struct rog_ryujin_data *priv) { int ret = mutex_lock_interruptible(&priv->status_report_request_mutex); if (ret < 0) return ret; if (!time_after(jiffies, priv->updated + msecs_to_jiffies(STATUS_VALIDITY))) { /* Data is up to date */ goto unlock_and_return; } /* Retrieve cooler status */ ret = rog_ryujin_execute_cmd(priv, get_cooler_status_cmd, GET_CMD_LENGTH, &priv->cooler_status_received); if (ret < 0) goto unlock_and_return; /* Retrieve controller status (speeds) */ ret = rog_ryujin_execute_cmd(priv, get_controller_speed_cmd, GET_CMD_LENGTH, &priv->controller_status_received); if (ret < 0) goto unlock_and_return; /* Retrieve cooler duty */ ret = rog_ryujin_execute_cmd(priv, get_cooler_duty_cmd, GET_CMD_LENGTH, &priv->cooler_duty_received); if (ret < 0) goto unlock_and_return; /* Retrieve controller duty */ ret = rog_ryujin_execute_cmd(priv, get_controller_duty_cmd, GET_CMD_LENGTH, &priv->controller_duty_received); if (ret < 0) goto unlock_and_return; priv->updated = jiffies; unlock_and_return: mutex_unlock(&priv->status_report_request_mutex); if (ret < 0) return ret; return 0; } static int rog_ryujin_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long *val) { struct rog_ryujin_data *priv = dev_get_drvdata(dev); int ret = rog_ryujin_get_status(priv); if (ret < 0) return ret; switch (type) { case hwmon_temp: *val = priv->temp_input[channel]; break; case hwmon_fan: *val = priv->speed_input[channel]; break; case hwmon_pwm: switch (attr) { case hwmon_pwm_input: *val = priv->duty_input[channel]; break; default: return -EOPNOTSUPP; } break; default: return -EOPNOTSUPP; /* unreachable */ } return 0; } static int rog_ryujin_read_string(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, const char **str) { switch (type) { case hwmon_temp: *str = rog_ryujin_temp_label[channel]; break; case hwmon_fan: *str = rog_ryujin_speed_label[channel]; break; default: return -EOPNOTSUPP; /* unreachable */ } return 0; } static int rog_ryujin_write_fixed_duty(struct rog_ryujin_data *priv, int channel, int val) { u8 set_cmd[SET_CMD_LENGTH]; int ret; if (channel < 2) { /* * Retrieve cooler duty since both pump and internal fan are set * together, then write back with one of them modified. */ ret = mutex_lock_interruptible(&priv->status_report_request_mutex); if (ret < 0) return ret; ret = rog_ryujin_execute_cmd(priv, get_cooler_duty_cmd, GET_CMD_LENGTH, &priv->cooler_duty_received); if (ret < 0) goto unlock_and_return; memcpy(set_cmd, set_cooler_duty_cmd, SET_CMD_LENGTH); /* Cooler duties are set as 0-100% */ val = rog_ryujin_pwm_to_percent(val); if (channel == 0) { /* Cooler pump duty */ set_cmd[RYUJIN_SET_COOLER_PUMP_DUTY_OFFSET] = val; set_cmd[RYUJIN_SET_COOLER_FAN_DUTY_OFFSET] = rog_ryujin_pwm_to_percent(priv->duty_input[1]); } else if (channel == 1) { /* Cooler internal fan duty */ set_cmd[RYUJIN_SET_COOLER_PUMP_DUTY_OFFSET] = rog_ryujin_pwm_to_percent(priv->duty_input[0]); set_cmd[RYUJIN_SET_COOLER_FAN_DUTY_OFFSET] = val; } ret = rog_ryujin_execute_cmd(priv, set_cmd, SET_CMD_LENGTH, &priv->cooler_duty_set); unlock_and_return: mutex_unlock(&priv->status_report_request_mutex); if (ret < 0) return ret; } else { /* * Controller fan duty (channel == 2). No need to retrieve current * duty, so just send the command. */ memcpy(set_cmd, set_controller_duty_cmd, SET_CMD_LENGTH); set_cmd[RYUJIN_SET_CONTROLLER_FAN_DUTY_OFFSET] = val; ret = rog_ryujin_execute_cmd(priv, set_cmd, SET_CMD_LENGTH, &priv->controller_duty_set); if (ret < 0) return ret; } /* Lock onto this value until next refresh cycle */ priv->duty_input[channel] = val; return 0; } static int rog_ryujin_write(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long val) { struct rog_ryujin_data *priv = dev_get_drvdata(dev); int ret; switch (type) { case hwmon_pwm: switch (attr) { case hwmon_pwm_input: if (val < 0 || val > 255) return -EINVAL; ret = rog_ryujin_write_fixed_duty(priv, channel, val); if (ret < 0) return ret; break; default: return -EOPNOTSUPP; } break; default: return -EOPNOTSUPP; } return 0; } static const struct hwmon_ops rog_ryujin_hwmon_ops = { .is_visible = rog_ryujin_is_visible, .read = rog_ryujin_read, .read_string = rog_ryujin_read_string, .write = rog_ryujin_write }; static const struct hwmon_channel_info *rog_ryujin_info[] = { HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT | HWMON_T_LABEL), HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT | HWMON_F_LABEL, HWMON_F_INPUT | HWMON_F_LABEL, HWMON_F_INPUT | HWMON_F_LABEL, HWMON_F_INPUT | HWMON_F_LABEL, HWMON_F_INPUT | HWMON_F_LABEL, HWMON_F_INPUT | HWMON_F_LABEL), HWMON_CHANNEL_INFO(pwm, HWMON_PWM_INPUT, HWMON_PWM_INPUT, HWMON_PWM_INPUT), NULL }; static const struct hwmon_chip_info rog_ryujin_chip_info = { .ops = &rog_ryujin_hwmon_ops, .info = rog_ryujin_info, }; static int rog_ryujin_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) { struct rog_ryujin_data *priv = hid_get_drvdata(hdev); if (data[0] != RYUJIN_CMD_PREFIX) return 0; if (data[1] == RYUJIN_GET_COOLER_STATUS_CMD_RESPONSE) { /* Received coolant temp and speeds of pump and internal fan */ priv->temp_input[0] = data[RYUJIN_TEMP_SENSOR_1] * 1000 + data[RYUJIN_TEMP_SENSOR_2] * 100; priv->speed_input[0] = get_unaligned_le16(data + RYUJIN_PUMP_SPEED); priv->speed_input[1] = get_unaligned_le16(data + RYUJIN_INTERNAL_FAN_SPEED); if (!completion_done(&priv->cooler_status_received)) complete_all(&priv->cooler_status_received); } else if (data[1] == RYUJIN_GET_CONTROLLER_SPEED_CMD_RESPONSE) { /* Received speeds of four fans attached to the controller */ priv->speed_input[2] = get_unaligned_le16(data + RYUJIN_CONTROLLER_SPEED_1); priv->speed_input[3] = get_unaligned_le16(data + RYUJIN_CONTROLLER_SPEED_2); priv->speed_input[4] = get_unaligned_le16(data + RYUJIN_CONTROLLER_SPEED_3); priv->speed_input[5] = get_unaligned_le16(data + RYUJIN_CONTROLLER_SPEED_4); if (!completion_done(&priv->controller_status_received)) complete_all(&priv->controller_status_received); } else if (data[1] == RYUJIN_GET_COOLER_DUTY_CMD_RESPONSE) { /* Received report for pump and internal fan duties (in %) */ if (data[RYUJIN_PUMP_DUTY] == 0 && data[RYUJIN_INTERNAL_FAN_DUTY] == 0) { /* * We received a report with zeroes for duty in both places. * The device returns this as a confirmation that setting values * is successful. If we initiated a write, mark it as complete. */ if (!completion_done(&priv->cooler_duty_set)) complete_all(&priv->cooler_duty_set); else if (!completion_done(&priv->cooler_duty_received)) /* * We didn't initiate a write, but received both zeroes. * This means that either both duties are actually zero, * or that we received a success report caused by userspace. * We're expecting a report, so parse it. */ goto read_cooler_duty; return 0; } read_cooler_duty: priv->duty_input[0] = rog_ryujin_percent_to_pwm(data[RYUJIN_PUMP_DUTY]); priv->duty_input[1] = rog_ryujin_percent_to_pwm(data[RYUJIN_INTERNAL_FAN_DUTY]); if (!completion_done(&priv->cooler_duty_received)) complete_all(&priv->cooler_duty_received); } else if (data[1] == RYUJIN_GET_CONTROLLER_DUTY_CMD_RESPONSE) { /* Received report for controller duty for fans (in PWM) */ if (data[RYUJIN_CONTROLLER_DUTY] == 0) { /* * We received a report with a zero for duty. The device returns this as * a confirmation that setting the controller duty value was successful. * If we initiated a write, mark it as complete. */ if (!completion_done(&priv->controller_duty_set)) complete_all(&priv->controller_duty_set); else if (!completion_done(&priv->controller_duty_received)) /* * We didn't initiate a write, but received a zero for duty. * This means that either the duty is actually zero, or that * we received a success report caused by userspace. * We're expecting a report, so parse it. */ goto read_controller_duty; return 0; } read_controller_duty: priv->duty_input[2] = data[RYUJIN_CONTROLLER_DUTY]; if (!completion_done(&priv->controller_duty_received)) complete_all(&priv->controller_duty_received); } return 0; } static int rog_ryujin_probe(struct hid_device *hdev, const struct hid_device_id *id) { struct rog_ryujin_data *priv; int ret; priv = devm_kzalloc(&hdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->hdev = hdev; hid_set_drvdata(hdev, priv); /* * Initialize priv->updated to STATUS_VALIDITY seconds in the past, making * the initial empty data invalid for rog_ryujin_read() without the need for * a special case there. */ priv->updated = jiffies - msecs_to_jiffies(STATUS_VALIDITY); ret = hid_parse(hdev); if (ret) { hid_err(hdev, "hid parse failed with %d\n", ret); return ret; } /* Enable hidraw so existing user-space tools can continue to work */ ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); if (ret) { hid_err(hdev, "hid hw start failed with %d\n", ret); return ret; } ret = hid_hw_open(hdev); if (ret) { hid_err(hdev, "hid hw open failed with %d\n", ret); goto fail_and_stop; } priv->buffer = devm_kzalloc(&hdev->dev, MAX_REPORT_LENGTH, GFP_KERNEL); if (!priv->buffer) { ret = -ENOMEM; goto fail_and_close; } mutex_init(&priv->status_report_request_mutex); mutex_init(&priv->buffer_lock); spin_lock_init(&priv->status_report_request_lock); init_completion(&priv->cooler_status_received); init_completion(&priv->controller_status_received); init_completion(&priv->cooler_duty_received); init_completion(&priv->controller_duty_received); init_completion(&priv->cooler_duty_set); init_completion(&priv->controller_duty_set); priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, "rog_ryujin", priv, &rog_ryujin_chip_info, NULL); if (IS_ERR(priv->hwmon_dev)) { ret = PTR_ERR(priv->hwmon_dev); hid_err(hdev, "hwmon registration failed with %d\n", ret); goto fail_and_close; } return 0; fail_and_close: hid_hw_close(hdev); fail_and_stop: hid_hw_stop(hdev); return ret; } static void rog_ryujin_remove(struct hid_device *hdev) { struct rog_ryujin_data *priv = hid_get_drvdata(hdev); hwmon_device_unregister(priv->hwmon_dev); hid_hw_close(hdev); hid_hw_stop(hdev); } static const struct hid_device_id rog_ryujin_table[] = { { HID_USB_DEVICE(USB_VENDOR_ID_ASUS_ROG, USB_PRODUCT_ID_RYUJIN_AIO) }, { } }; MODULE_DEVICE_TABLE(hid, rog_ryujin_table); static struct hid_driver rog_ryujin_driver = { .name = "rog_ryujin", .id_table = rog_ryujin_table, .probe = rog_ryujin_probe, .remove = rog_ryujin_remove, .raw_event = rog_ryujin_raw_event, }; static int __init rog_ryujin_init(void) { return hid_register_driver(&rog_ryujin_driver); } static void __exit rog_ryujin_exit(void) { hid_unregister_driver(&rog_ryujin_driver); } /* When compiled into the kernel, initialize after the HID bus */ late_initcall(rog_ryujin_init); module_exit(rog_ryujin_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Aleksa Savic "); MODULE_DESCRIPTION("Hwmon driver for Asus ROG Ryujin II 360 AIO cooler");