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