| // SPDX-License-Identifier: GPL-2.0+ |
| /* |
| * hwmon driver for Gigabyte AORUS Waterforce AIO CPU coolers: X240, X280 and X360. |
| * |
| * Copyright 2023 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 "gigabyte_waterforce" |
| |
| #define USB_VENDOR_ID_GIGABYTE 0x1044 |
| #define USB_PRODUCT_ID_WATERFORCE 0x7a4d /* Gigabyte AORUS WATERFORCE X240, X280 and X360 */ |
| |
| #define STATUS_VALIDITY (2 * 1000) /* ms */ |
| #define MAX_REPORT_LENGTH 6144 |
| |
| #define WATERFORCE_TEMP_SENSOR 0xD |
| #define WATERFORCE_FAN_SPEED 0x02 |
| #define WATERFORCE_PUMP_SPEED 0x05 |
| #define WATERFORCE_FAN_DUTY 0x08 |
| #define WATERFORCE_PUMP_DUTY 0x09 |
| |
| /* Control commands, inner offsets and lengths */ |
| static const u8 get_status_cmd[] = { 0x99, 0xDA }; |
| |
| #define FIRMWARE_VER_START_OFFSET_1 2 |
| #define FIRMWARE_VER_START_OFFSET_2 3 |
| static const u8 get_firmware_ver_cmd[] = { 0x99, 0xD6 }; |
| |
| /* Command lengths */ |
| #define GET_STATUS_CMD_LENGTH 2 |
| #define GET_FIRMWARE_VER_CMD_LENGTH 2 |
| |
| static const char *const waterforce_temp_label[] = { |
| "Coolant temp" |
| }; |
| |
| static const char *const waterforce_speed_label[] = { |
| "Fan speed", |
| "Pump speed" |
| }; |
| |
| struct waterforce_data { |
| struct hid_device *hdev; |
| struct device *hwmon_dev; |
| struct dentry *debugfs; |
| /* For locking access to buffer */ |
| struct mutex buffer_lock; |
| /* For queueing multiple readers */ |
| struct mutex status_report_request_mutex; |
| /* For reinitializing the completion below */ |
| spinlock_t status_report_request_lock; |
| struct completion status_report_received; |
| struct completion fw_version_processed; |
| |
| /* Sensor data */ |
| s32 temp_input[1]; |
| u16 speed_input[2]; /* Fan and pump speed in RPM */ |
| u8 duty_input[2]; /* Fan and pump duty in 0-100% */ |
| |
| u8 *buffer; |
| int firmware_version; |
| unsigned long updated; /* jiffies */ |
| }; |
| |
| static umode_t waterforce_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 0444; |
| default: |
| break; |
| } |
| break; |
| default: |
| break; |
| } |
| |
| return 0; |
| } |
| |
| /* Writes the command to the device with the rest of the report filled with zeroes */ |
| static int waterforce_write_expanded(struct waterforce_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; |
| } |
| |
| static int waterforce_get_status(struct waterforce_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; |
| } |
| |
| /* |
| * 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 priv->status_report_received |
| * completion as done. |
| */ |
| spin_lock_bh(&priv->status_report_request_lock); |
| reinit_completion(&priv->status_report_received); |
| spin_unlock_bh(&priv->status_report_request_lock); |
| |
| /* Send command for getting status */ |
| ret = waterforce_write_expanded(priv, get_status_cmd, GET_STATUS_CMD_LENGTH); |
| if (ret < 0) |
| goto unlock_and_return; |
| |
| ret = wait_for_completion_interruptible_timeout(&priv->status_report_received, |
| msecs_to_jiffies(STATUS_VALIDITY)); |
| if (ret == 0) |
| ret = -ETIMEDOUT; |
| |
| unlock_and_return: |
| mutex_unlock(&priv->status_report_request_mutex); |
| if (ret < 0) |
| return ret; |
| |
| return 0; |
| } |
| |
| static int waterforce_read(struct device *dev, enum hwmon_sensor_types type, |
| u32 attr, int channel, long *val) |
| { |
| struct waterforce_data *priv = dev_get_drvdata(dev); |
| int ret = waterforce_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 = DIV_ROUND_CLOSEST(priv->duty_input[channel] * 255, 100); |
| break; |
| default: |
| return -EOPNOTSUPP; |
| } |
| break; |
| default: |
| return -EOPNOTSUPP; /* unreachable */ |
| } |
| |
| return 0; |
| } |
| |
| static int waterforce_read_string(struct device *dev, enum hwmon_sensor_types type, |
| u32 attr, int channel, const char **str) |
| { |
| switch (type) { |
| case hwmon_temp: |
| *str = waterforce_temp_label[channel]; |
| break; |
| case hwmon_fan: |
| *str = waterforce_speed_label[channel]; |
| break; |
| default: |
| return -EOPNOTSUPP; /* unreachable */ |
| } |
| |
| return 0; |
| } |
| |
| static int waterforce_get_fw_ver(struct hid_device *hdev) |
| { |
| struct waterforce_data *priv = hid_get_drvdata(hdev); |
| int ret; |
| |
| ret = waterforce_write_expanded(priv, get_firmware_ver_cmd, GET_FIRMWARE_VER_CMD_LENGTH); |
| if (ret < 0) |
| return ret; |
| |
| ret = wait_for_completion_interruptible_timeout(&priv->fw_version_processed, |
| msecs_to_jiffies(STATUS_VALIDITY)); |
| if (ret == 0) |
| return -ETIMEDOUT; |
| else if (ret < 0) |
| return ret; |
| |
| return 0; |
| } |
| |
| static const struct hwmon_ops waterforce_hwmon_ops = { |
| .is_visible = waterforce_is_visible, |
| .read = waterforce_read, |
| .read_string = waterforce_read_string |
| }; |
| |
| static const struct hwmon_channel_info *waterforce_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_CHANNEL_INFO(pwm, |
| HWMON_PWM_INPUT, |
| HWMON_PWM_INPUT), |
| NULL |
| }; |
| |
| static const struct hwmon_chip_info waterforce_chip_info = { |
| .ops = &waterforce_hwmon_ops, |
| .info = waterforce_info, |
| }; |
| |
| static int waterforce_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, |
| int size) |
| { |
| struct waterforce_data *priv = hid_get_drvdata(hdev); |
| |
| if (data[0] == get_firmware_ver_cmd[0] && data[1] == get_firmware_ver_cmd[1]) { |
| /* Received a firmware version report */ |
| priv->firmware_version = |
| data[FIRMWARE_VER_START_OFFSET_1] * 10 + data[FIRMWARE_VER_START_OFFSET_2]; |
| |
| if (!completion_done(&priv->fw_version_processed)) |
| complete_all(&priv->fw_version_processed); |
| return 0; |
| } |
| |
| if (data[0] != get_status_cmd[0] || data[1] != get_status_cmd[1]) |
| return 0; |
| |
| priv->temp_input[0] = data[WATERFORCE_TEMP_SENSOR] * 1000; |
| priv->speed_input[0] = get_unaligned_le16(data + WATERFORCE_FAN_SPEED); |
| priv->speed_input[1] = get_unaligned_le16(data + WATERFORCE_PUMP_SPEED); |
| priv->duty_input[0] = data[WATERFORCE_FAN_DUTY]; |
| priv->duty_input[1] = data[WATERFORCE_PUMP_DUTY]; |
| |
| spin_lock(&priv->status_report_request_lock); |
| if (!completion_done(&priv->status_report_received)) |
| complete_all(&priv->status_report_received); |
| spin_unlock(&priv->status_report_request_lock); |
| |
| priv->updated = jiffies; |
| |
| return 0; |
| } |
| |
| static int firmware_version_show(struct seq_file *seqf, void *unused) |
| { |
| struct waterforce_data *priv = seqf->private; |
| |
| seq_printf(seqf, "%u\n", priv->firmware_version); |
| |
| return 0; |
| } |
| DEFINE_SHOW_ATTRIBUTE(firmware_version); |
| |
| static void waterforce_debugfs_init(struct waterforce_data *priv) |
| { |
| char name[64]; |
| |
| if (!priv->firmware_version) |
| return; /* There's nothing to show in debugfs */ |
| |
| scnprintf(name, sizeof(name), "%s-%s", DRIVER_NAME, 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 int waterforce_probe(struct hid_device *hdev, const struct hid_device_id *id) |
| { |
| struct waterforce_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 waterforce_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->status_report_received); |
| init_completion(&priv->fw_version_processed); |
| |
| hid_device_io_start(hdev); |
| ret = waterforce_get_fw_ver(hdev); |
| if (ret < 0) |
| hid_warn(hdev, "fw version request failed with %d\n", ret); |
| |
| priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, "waterforce", |
| priv, &waterforce_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; |
| } |
| |
| waterforce_debugfs_init(priv); |
| |
| return 0; |
| |
| fail_and_close: |
| hid_hw_close(hdev); |
| fail_and_stop: |
| hid_hw_stop(hdev); |
| return ret; |
| } |
| |
| static void waterforce_remove(struct hid_device *hdev) |
| { |
| struct waterforce_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); |
| } |
| |
| static const struct hid_device_id waterforce_table[] = { |
| { HID_USB_DEVICE(USB_VENDOR_ID_GIGABYTE, USB_PRODUCT_ID_WATERFORCE) }, |
| { } |
| }; |
| |
| MODULE_DEVICE_TABLE(hid, waterforce_table); |
| |
| static struct hid_driver waterforce_driver = { |
| .name = "waterforce", |
| .id_table = waterforce_table, |
| .probe = waterforce_probe, |
| .remove = waterforce_remove, |
| .raw_event = waterforce_raw_event, |
| }; |
| |
| static int __init waterforce_init(void) |
| { |
| return hid_register_driver(&waterforce_driver); |
| } |
| |
| static void __exit waterforce_exit(void) |
| { |
| hid_unregister_driver(&waterforce_driver); |
| } |
| |
| /* When compiled into the kernel, initialize after the HID bus */ |
| late_initcall(waterforce_init); |
| module_exit(waterforce_exit); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_AUTHOR("Aleksa Savic <savicaleksa83@gmail.com>"); |
| MODULE_DESCRIPTION("Hwmon driver for Gigabyte AORUS Waterforce AIO coolers"); |