Contributors: 1
Author Tokens Token Proportion Commits Commit Proportion
Aleksa Savic 2400 100.00% 1 100.00%
Total 2400 1


// SPDX-License-Identifier: GPL-2.0+
/*
 * hwmon driver for Asus ROG Ryujin II 360 AIO cooler.
 *
 * Copyright 2024 Aleksa Savic <savicaleksa83@gmail.com>
 */

#include <linux/debugfs.h>
#include <linux/hid.h>
#include <linux/hwmon.h>
#include <linux/jiffies.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include <asm/unaligned.h>

#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 <savicaleksa83@gmail.com>");
MODULE_DESCRIPTION("Hwmon driver for Asus ROG Ryujin II 360 AIO cooler");