root/drivers/hwmon/asus_rog_ryujin.c
// 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 <linux/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 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)
{
        memcpy_and_pad(priv->buffer, MAX_REPORT_LENGTH, cmd, cmd_length, 0x00);
        return hid_hw_output_report(priv->hdev, priv->buffer, MAX_REPORT_LENGTH);
}

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;

        if (!time_after(jiffies, priv->updated + msecs_to_jiffies(STATUS_VALIDITY))) {
                /* Data is up to date */
                return 0;
        }

        /* Retrieve cooler status */
        ret =
            rog_ryujin_execute_cmd(priv, get_cooler_status_cmd, GET_CMD_LENGTH,
                                   &priv->cooler_status_received);
        if (ret < 0)
                return ret;

        /* Retrieve controller status (speeds) */
        ret =
            rog_ryujin_execute_cmd(priv, get_controller_speed_cmd, GET_CMD_LENGTH,
                                   &priv->controller_status_received);
        if (ret < 0)
                return ret;

        /* Retrieve cooler duty */
        ret =
            rog_ryujin_execute_cmd(priv, get_cooler_duty_cmd, GET_CMD_LENGTH,
                                   &priv->cooler_duty_received);
        if (ret < 0)
                return ret;

        /* Retrieve controller duty */
        ret =
            rog_ryujin_execute_cmd(priv, get_controller_duty_cmd, GET_CMD_LENGTH,
                                   &priv->controller_duty_received);
        if (ret < 0)
                return ret;

        priv->updated = jiffies;
        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 =
                    rog_ryujin_execute_cmd(priv, get_cooler_duty_cmd, GET_CMD_LENGTH,
                                           &priv->cooler_duty_received);
                if (ret < 0)
                        return ret;

                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;
                }

                return rog_ryujin_execute_cmd(priv, set_cmd, SET_CMD_LENGTH, &priv->cooler_duty_set);
        } 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;
        }

        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");