// SPDX-License-Identifier: GPL-2.0+ /* * hwmon driver for Gigabyte AORUS Waterforce AIO CPU coolers: X240, X280 and X360. * * Copyright 2023 Aleksa Savic */ #include #include #include #include #include #include #include #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 "); MODULE_DESCRIPTION("Hwmon driver for Gigabyte AORUS Waterforce AIO coolers");