summaryrefslogtreecommitdiffstats
path: root/drivers/hwmon/nzxt-kraken2.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/hwmon/nzxt-kraken2.c')
-rw-r--r--drivers/hwmon/nzxt-kraken2.c234
1 files changed, 234 insertions, 0 deletions
diff --git a/drivers/hwmon/nzxt-kraken2.c b/drivers/hwmon/nzxt-kraken2.c
new file mode 100644
index 0000000000..7caf387eb1
--- /dev/null
+++ b/drivers/hwmon/nzxt-kraken2.c
@@ -0,0 +1,234 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * nzxt-kraken2.c - hwmon driver for NZXT Kraken X42/X52/X62/X72 coolers
+ *
+ * The device asynchronously sends HID reports (with id 0x04) twice a second to
+ * communicate current fan speed, pump speed and coolant temperature. The
+ * device does not respond to Get_Report requests for this status report.
+ *
+ * Copyright 2019-2021 Jonas Malaco <jonas@protocubo.io>
+ */
+
+#include <asm/unaligned.h>
+#include <linux/hid.h>
+#include <linux/hwmon.h>
+#include <linux/jiffies.h>
+#include <linux/module.h>
+
+#define STATUS_REPORT_ID 0x04
+#define STATUS_VALIDITY 2 /* seconds; equivalent to 4 missed updates */
+
+static const char *const kraken2_temp_label[] = {
+ "Coolant",
+};
+
+static const char *const kraken2_fan_label[] = {
+ "Fan",
+ "Pump",
+};
+
+struct kraken2_priv_data {
+ struct hid_device *hid_dev;
+ struct device *hwmon_dev;
+ s32 temp_input[1];
+ u16 fan_input[2];
+ unsigned long updated; /* jiffies */
+};
+
+static umode_t kraken2_is_visible(const void *data,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel)
+{
+ return 0444;
+}
+
+static int kraken2_read(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, long *val)
+{
+ struct kraken2_priv_data *priv = dev_get_drvdata(dev);
+
+ if (time_after(jiffies, priv->updated + STATUS_VALIDITY * HZ))
+ return -ENODATA;
+
+ switch (type) {
+ case hwmon_temp:
+ *val = priv->temp_input[channel];
+ break;
+ case hwmon_fan:
+ *val = priv->fan_input[channel];
+ break;
+ default:
+ return -EOPNOTSUPP; /* unreachable */
+ }
+
+ return 0;
+}
+
+static int kraken2_read_string(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, const char **str)
+{
+ switch (type) {
+ case hwmon_temp:
+ *str = kraken2_temp_label[channel];
+ break;
+ case hwmon_fan:
+ *str = kraken2_fan_label[channel];
+ break;
+ default:
+ return -EOPNOTSUPP; /* unreachable */
+ }
+ return 0;
+}
+
+static const struct hwmon_ops kraken2_hwmon_ops = {
+ .is_visible = kraken2_is_visible,
+ .read = kraken2_read,
+ .read_string = kraken2_read_string,
+};
+
+static const struct hwmon_channel_info * const kraken2_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),
+ NULL
+};
+
+static const struct hwmon_chip_info kraken2_chip_info = {
+ .ops = &kraken2_hwmon_ops,
+ .info = kraken2_info,
+};
+
+static int kraken2_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size)
+{
+ struct kraken2_priv_data *priv;
+
+ if (size < 7 || report->id != STATUS_REPORT_ID)
+ return 0;
+
+ priv = hid_get_drvdata(hdev);
+
+ /*
+ * The fractional byte of the coolant temperature has been observed to
+ * be in the interval [1,9], but some of these steps are also
+ * consistently skipped for certain integer parts.
+ *
+ * For the lack of a better idea, assume that the resolution is 0.1°C,
+ * and that the missing steps are artifacts of how the firmware
+ * processes the raw sensor data.
+ */
+ priv->temp_input[0] = data[1] * 1000 + data[2] * 100;
+
+ priv->fan_input[0] = get_unaligned_be16(data + 3);
+ priv->fan_input[1] = get_unaligned_be16(data + 5);
+
+ priv->updated = jiffies;
+
+ return 0;
+}
+
+static int kraken2_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ struct kraken2_priv_data *priv;
+ int ret;
+
+ priv = devm_kzalloc(&hdev->dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->hid_dev = hdev;
+ hid_set_drvdata(hdev, priv);
+
+ /*
+ * Initialize ->updated to STATUS_VALIDITY seconds in the past, making
+ * the initial empty data invalid for kraken2_read without the need for
+ * a special case there.
+ */
+ priv->updated = jiffies - STATUS_VALIDITY * HZ;
+
+ 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->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, "kraken2",
+ priv, &kraken2_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 kraken2_remove(struct hid_device *hdev)
+{
+ struct kraken2_priv_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 kraken2_table[] = {
+ { HID_USB_DEVICE(0x1e71, 0x170e) }, /* NZXT Kraken X42/X52/X62/X72 */
+ { }
+};
+
+MODULE_DEVICE_TABLE(hid, kraken2_table);
+
+static struct hid_driver kraken2_driver = {
+ .name = "nzxt-kraken2",
+ .id_table = kraken2_table,
+ .probe = kraken2_probe,
+ .remove = kraken2_remove,
+ .raw_event = kraken2_raw_event,
+};
+
+static int __init kraken2_init(void)
+{
+ return hid_register_driver(&kraken2_driver);
+}
+
+static void __exit kraken2_exit(void)
+{
+ hid_unregister_driver(&kraken2_driver);
+}
+
+/*
+ * When compiled into the kernel, initialize after the hid bus.
+ */
+late_initcall(kraken2_init);
+module_exit(kraken2_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Jonas Malaco <jonas@protocubo.io>");
+MODULE_DESCRIPTION("Hwmon driver for NZXT Kraken X42/X52/X62/X72 coolers");