Commit | Line | Data |
---|---|---|
82e3430d JM |
1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* | |
3 | * nzxt-kraken2.c - hwmon driver for NZXT Kraken X42/X52/X62/X72 coolers | |
4 | * | |
5 | * The device asynchronously sends HID reports (with id 0x04) twice a second to | |
6 | * communicate current fan speed, pump speed and coolant temperature. The | |
7 | * device does not respond to Get_Report requests for this status report. | |
8 | * | |
9 | * Copyright 2019-2021 Jonas Malaco <jonas@protocubo.io> | |
10 | */ | |
11 | ||
12 | #include <asm/unaligned.h> | |
13 | #include <linux/hid.h> | |
14 | #include <linux/hwmon.h> | |
15 | #include <linux/jiffies.h> | |
16 | #include <linux/module.h> | |
17 | ||
18 | #define STATUS_REPORT_ID 0x04 | |
19 | #define STATUS_VALIDITY 2 /* seconds; equivalent to 4 missed updates */ | |
20 | ||
21 | static const char *const kraken2_temp_label[] = { | |
22 | "Coolant", | |
23 | }; | |
24 | ||
25 | static const char *const kraken2_fan_label[] = { | |
26 | "Fan", | |
27 | "Pump", | |
28 | }; | |
29 | ||
30 | struct kraken2_priv_data { | |
31 | struct hid_device *hid_dev; | |
32 | struct device *hwmon_dev; | |
33 | s32 temp_input[1]; | |
34 | u16 fan_input[2]; | |
35 | unsigned long updated; /* jiffies */ | |
36 | }; | |
37 | ||
38 | static umode_t kraken2_is_visible(const void *data, | |
39 | enum hwmon_sensor_types type, | |
40 | u32 attr, int channel) | |
41 | { | |
42 | return 0444; | |
43 | } | |
44 | ||
45 | static int kraken2_read(struct device *dev, enum hwmon_sensor_types type, | |
46 | u32 attr, int channel, long *val) | |
47 | { | |
48 | struct kraken2_priv_data *priv = dev_get_drvdata(dev); | |
49 | ||
50 | if (time_after(jiffies, priv->updated + STATUS_VALIDITY * HZ)) | |
51 | return -ENODATA; | |
52 | ||
53 | switch (type) { | |
54 | case hwmon_temp: | |
55 | *val = priv->temp_input[channel]; | |
56 | break; | |
57 | case hwmon_fan: | |
58 | *val = priv->fan_input[channel]; | |
59 | break; | |
60 | default: | |
61 | return -EOPNOTSUPP; /* unreachable */ | |
62 | } | |
63 | ||
64 | return 0; | |
65 | } | |
66 | ||
67 | static int kraken2_read_string(struct device *dev, enum hwmon_sensor_types type, | |
68 | u32 attr, int channel, const char **str) | |
69 | { | |
70 | switch (type) { | |
71 | case hwmon_temp: | |
72 | *str = kraken2_temp_label[channel]; | |
73 | break; | |
74 | case hwmon_fan: | |
75 | *str = kraken2_fan_label[channel]; | |
76 | break; | |
77 | default: | |
78 | return -EOPNOTSUPP; /* unreachable */ | |
79 | } | |
80 | return 0; | |
81 | } | |
82 | ||
83 | static const struct hwmon_ops kraken2_hwmon_ops = { | |
84 | .is_visible = kraken2_is_visible, | |
85 | .read = kraken2_read, | |
86 | .read_string = kraken2_read_string, | |
87 | }; | |
88 | ||
89 | static const struct hwmon_channel_info *kraken2_info[] = { | |
90 | HWMON_CHANNEL_INFO(temp, | |
91 | HWMON_T_INPUT | HWMON_T_LABEL), | |
92 | HWMON_CHANNEL_INFO(fan, | |
93 | HWMON_F_INPUT | HWMON_F_LABEL, | |
94 | HWMON_F_INPUT | HWMON_F_LABEL), | |
95 | NULL | |
96 | }; | |
97 | ||
98 | static const struct hwmon_chip_info kraken2_chip_info = { | |
99 | .ops = &kraken2_hwmon_ops, | |
100 | .info = kraken2_info, | |
101 | }; | |
102 | ||
103 | static int kraken2_raw_event(struct hid_device *hdev, | |
104 | struct hid_report *report, u8 *data, int size) | |
105 | { | |
106 | struct kraken2_priv_data *priv; | |
107 | ||
108 | if (size < 7 || report->id != STATUS_REPORT_ID) | |
109 | return 0; | |
110 | ||
111 | priv = hid_get_drvdata(hdev); | |
112 | ||
113 | /* | |
114 | * The fractional byte of the coolant temperature has been observed to | |
115 | * be in the interval [1,9], but some of these steps are also | |
116 | * consistently skipped for certain integer parts. | |
117 | * | |
118 | * For the lack of a better idea, assume that the resolution is 0.1°C, | |
119 | * and that the missing steps are artifacts of how the firmware | |
120 | * processes the raw sensor data. | |
121 | */ | |
122 | priv->temp_input[0] = data[1] * 1000 + data[2] * 100; | |
123 | ||
124 | priv->fan_input[0] = get_unaligned_be16(data + 3); | |
125 | priv->fan_input[1] = get_unaligned_be16(data + 5); | |
126 | ||
127 | priv->updated = jiffies; | |
128 | ||
129 | return 0; | |
130 | } | |
131 | ||
132 | static int kraken2_probe(struct hid_device *hdev, | |
133 | const struct hid_device_id *id) | |
134 | { | |
135 | struct kraken2_priv_data *priv; | |
136 | int ret; | |
137 | ||
138 | priv = devm_kzalloc(&hdev->dev, sizeof(*priv), GFP_KERNEL); | |
139 | if (!priv) | |
140 | return -ENOMEM; | |
141 | ||
142 | priv->hid_dev = hdev; | |
143 | hid_set_drvdata(hdev, priv); | |
144 | ||
145 | /* | |
146 | * Initialize ->updated to STATUS_VALIDITY seconds in the past, making | |
147 | * the initial empty data invalid for kraken2_read without the need for | |
148 | * a special case there. | |
149 | */ | |
150 | priv->updated = jiffies - STATUS_VALIDITY * HZ; | |
151 | ||
152 | ret = hid_parse(hdev); | |
153 | if (ret) { | |
154 | hid_err(hdev, "hid parse failed with %d\n", ret); | |
155 | return ret; | |
156 | } | |
157 | ||
158 | /* | |
159 | * Enable hidraw so existing user-space tools can continue to work. | |
160 | */ | |
161 | ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); | |
162 | if (ret) { | |
163 | hid_err(hdev, "hid hw start failed with %d\n", ret); | |
164 | goto fail_and_stop; | |
165 | } | |
166 | ||
167 | ret = hid_hw_open(hdev); | |
168 | if (ret) { | |
169 | hid_err(hdev, "hid hw open failed with %d\n", ret); | |
170 | goto fail_and_close; | |
171 | } | |
172 | ||
173 | priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, "kraken2", | |
174 | priv, &kraken2_chip_info, | |
175 | NULL); | |
176 | if (IS_ERR(priv->hwmon_dev)) { | |
177 | ret = PTR_ERR(priv->hwmon_dev); | |
178 | hid_err(hdev, "hwmon registration failed with %d\n", ret); | |
179 | goto fail_and_close; | |
180 | } | |
181 | ||
182 | return 0; | |
183 | ||
184 | fail_and_close: | |
185 | hid_hw_close(hdev); | |
186 | fail_and_stop: | |
187 | hid_hw_stop(hdev); | |
188 | return ret; | |
189 | } | |
190 | ||
191 | static void kraken2_remove(struct hid_device *hdev) | |
192 | { | |
193 | struct kraken2_priv_data *priv = hid_get_drvdata(hdev); | |
194 | ||
195 | hwmon_device_unregister(priv->hwmon_dev); | |
196 | ||
197 | hid_hw_close(hdev); | |
198 | hid_hw_stop(hdev); | |
199 | } | |
200 | ||
201 | static const struct hid_device_id kraken2_table[] = { | |
202 | { HID_USB_DEVICE(0x1e71, 0x170e) }, /* NZXT Kraken X42/X52/X62/X72 */ | |
203 | { } | |
204 | }; | |
205 | ||
206 | MODULE_DEVICE_TABLE(hid, kraken2_table); | |
207 | ||
208 | static struct hid_driver kraken2_driver = { | |
209 | .name = "nzxt-kraken2", | |
210 | .id_table = kraken2_table, | |
211 | .probe = kraken2_probe, | |
212 | .remove = kraken2_remove, | |
213 | .raw_event = kraken2_raw_event, | |
214 | }; | |
215 | ||
216 | static int __init kraken2_init(void) | |
217 | { | |
218 | return hid_register_driver(&kraken2_driver); | |
219 | } | |
220 | ||
221 | static void __exit kraken2_exit(void) | |
222 | { | |
223 | hid_unregister_driver(&kraken2_driver); | |
224 | } | |
225 | ||
226 | /* | |
227 | * When compiled into the kernel, initialize after the hid bus. | |
228 | */ | |
229 | late_initcall(kraken2_init); | |
230 | module_exit(kraken2_exit); | |
231 | ||
232 | MODULE_LICENSE("GPL"); | |
233 | MODULE_AUTHOR("Jonas Malaco <jonas@protocubo.io>"); | |
234 | MODULE_DESCRIPTION("Hwmon driver for NZXT Kraken X42/X52/X62/X72 coolers"); |