Commit | Line | Data |
---|---|---|
0e35f63f AS |
1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* | |
2fd3eec1 | 3 | * hwmon driver for Aquacomputer devices (D5 Next, Farbwerk 360) |
0e35f63f | 4 | * |
2fd3eec1 AS |
5 | * Aquacomputer devices send HID reports (with ID 0x01) every second to report |
6 | * sensor values. | |
0e35f63f AS |
7 | * |
8 | * Copyright 2021 Aleksa Savic <savicaleksa83@gmail.com> | |
9 | */ | |
10 | ||
0e35f63f AS |
11 | #include <linux/debugfs.h> |
12 | #include <linux/hid.h> | |
13 | #include <linux/hwmon.h> | |
14 | #include <linux/jiffies.h> | |
15 | #include <linux/module.h> | |
16 | #include <linux/seq_file.h> | |
2fd3eec1 | 17 | #include <asm/unaligned.h> |
0e35f63f | 18 | |
2fd3eec1 AS |
19 | #define USB_VENDOR_ID_AQUACOMPUTER 0x0c70 |
20 | #define USB_PRODUCT_ID_D5NEXT 0xf00e | |
21 | #define USB_PRODUCT_ID_FARBWERK360 0xf010 | |
0e35f63f | 22 | |
2fd3eec1 | 23 | enum kinds { d5next, farbwerk360 }; |
0e35f63f | 24 | |
2fd3eec1 AS |
25 | static const char *const aqc_device_names[] = { |
26 | [d5next] = "d5next", | |
27 | [farbwerk360] = "farbwerk360" | |
28 | }; | |
0e35f63f | 29 | |
2fd3eec1 AS |
30 | #define DRIVER_NAME "aquacomputer_d5next" |
31 | ||
32 | #define STATUS_REPORT_ID 0x01 | |
33 | #define STATUS_UPDATE_INTERVAL (2 * HZ) /* In seconds */ | |
34 | #define SERIAL_FIRST_PART 3 | |
35 | #define SERIAL_SECOND_PART 5 | |
36 | #define FIRMWARE_VERSION 13 | |
37 | ||
38 | /* Register offsets for the D5 Next pump */ | |
0e35f63f AS |
39 | #define D5NEXT_POWER_CYCLES 24 |
40 | ||
41 | #define D5NEXT_COOLANT_TEMP 87 | |
42 | ||
43 | #define D5NEXT_PUMP_SPEED 116 | |
44 | #define D5NEXT_FAN_SPEED 103 | |
45 | ||
46 | #define D5NEXT_PUMP_POWER 114 | |
47 | #define D5NEXT_FAN_POWER 101 | |
48 | ||
49 | #define D5NEXT_PUMP_VOLTAGE 110 | |
50 | #define D5NEXT_FAN_VOLTAGE 97 | |
51 | #define D5NEXT_5V_VOLTAGE 57 | |
52 | ||
53 | #define D5NEXT_PUMP_CURRENT 112 | |
54 | #define D5NEXT_FAN_CURRENT 99 | |
55 | ||
2fd3eec1 AS |
56 | /* Register offsets for the Farbwerk 360 RGB controller */ |
57 | #define FARBWERK360_NUM_SENSORS 4 | |
58 | #define FARBWERK360_SENSOR_START 0x32 | |
59 | #define FARBWERK360_SENSOR_SIZE 0x02 | |
60 | #define FARBWERK360_SENSOR_DISCONNECTED 0x7FFF | |
0e35f63f | 61 | |
2fd3eec1 AS |
62 | /* Labels for D5 Next */ |
63 | #define L_D5NEXT_COOLANT_TEMP "Coolant temp" | |
0e35f63f | 64 | |
2fd3eec1 AS |
65 | static const char *const label_d5next_speeds[] = { |
66 | "Pump speed", | |
67 | "Fan speed" | |
68 | }; | |
0e35f63f | 69 | |
2fd3eec1 AS |
70 | static const char *const label_d5next_power[] = { |
71 | "Pump power", | |
72 | "Fan power" | |
0e35f63f AS |
73 | }; |
74 | ||
2fd3eec1 AS |
75 | static const char *const label_d5next_voltages[] = { |
76 | "Pump voltage", | |
77 | "Fan voltage", | |
78 | "+5V voltage" | |
0e35f63f AS |
79 | }; |
80 | ||
2fd3eec1 AS |
81 | static const char *const label_d5next_current[] = { |
82 | "Pump current", | |
83 | "Fan current" | |
0e35f63f AS |
84 | }; |
85 | ||
2fd3eec1 AS |
86 | /* Labels for Farbwerk 360 temperature sensors */ |
87 | static const char *const label_temp_sensors[] = { | |
88 | "Sensor 1", | |
89 | "Sensor 2", | |
90 | "Sensor 3", | |
91 | "Sensor 4" | |
0e35f63f AS |
92 | }; |
93 | ||
2fd3eec1 | 94 | struct aqc_data { |
0e35f63f AS |
95 | struct hid_device *hdev; |
96 | struct device *hwmon_dev; | |
97 | struct dentry *debugfs; | |
2fd3eec1 AS |
98 | enum kinds kind; |
99 | const char *name; | |
100 | ||
101 | /* General info, same across all devices */ | |
102 | u32 serial_number[2]; | |
103 | u16 firmware_version; | |
104 | ||
105 | /* D5 Next specific - how many times the device was powered on */ | |
106 | u32 power_cycles; | |
107 | ||
108 | /* Sensor values */ | |
109 | s32 temp_input[4]; | |
0e35f63f AS |
110 | u16 speed_input[2]; |
111 | u32 power_input[2]; | |
112 | u16 voltage_input[3]; | |
113 | u16 current_input[2]; | |
2fd3eec1 | 114 | |
0e35f63f AS |
115 | unsigned long updated; |
116 | }; | |
117 | ||
2fd3eec1 AS |
118 | static umode_t aqc_is_visible(const void *data, enum hwmon_sensor_types type, u32 attr, |
119 | int channel) | |
0e35f63f | 120 | { |
2fd3eec1 AS |
121 | const struct aqc_data *priv = data; |
122 | ||
123 | switch (type) { | |
124 | case hwmon_temp: | |
125 | switch (priv->kind) { | |
126 | case d5next: | |
127 | if (channel == 0) | |
128 | return 0444; | |
129 | break; | |
130 | case farbwerk360: | |
131 | return 0444; | |
132 | default: | |
133 | break; | |
134 | } | |
135 | break; | |
136 | case hwmon_fan: | |
137 | case hwmon_power: | |
138 | case hwmon_in: | |
139 | case hwmon_curr: | |
140 | switch (priv->kind) { | |
141 | case d5next: | |
142 | return 0444; | |
143 | default: | |
144 | break; | |
145 | } | |
146 | break; | |
147 | default: | |
148 | break; | |
149 | } | |
150 | ||
151 | return 0; | |
0e35f63f AS |
152 | } |
153 | ||
2fd3eec1 AS |
154 | static int aqc_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, |
155 | int channel, long *val) | |
0e35f63f | 156 | { |
2fd3eec1 | 157 | struct aqc_data *priv = dev_get_drvdata(dev); |
0e35f63f | 158 | |
2fd3eec1 | 159 | if (time_after(jiffies, priv->updated + STATUS_UPDATE_INTERVAL)) |
0e35f63f AS |
160 | return -ENODATA; |
161 | ||
162 | switch (type) { | |
163 | case hwmon_temp: | |
2fd3eec1 AS |
164 | if (priv->temp_input[channel] == -ENODATA) |
165 | return -ENODATA; | |
166 | ||
167 | *val = priv->temp_input[channel]; | |
0e35f63f AS |
168 | break; |
169 | case hwmon_fan: | |
170 | *val = priv->speed_input[channel]; | |
171 | break; | |
172 | case hwmon_power: | |
173 | *val = priv->power_input[channel]; | |
174 | break; | |
175 | case hwmon_in: | |
176 | *val = priv->voltage_input[channel]; | |
177 | break; | |
178 | case hwmon_curr: | |
179 | *val = priv->current_input[channel]; | |
180 | break; | |
181 | default: | |
182 | return -EOPNOTSUPP; | |
183 | } | |
184 | ||
185 | return 0; | |
186 | } | |
187 | ||
2fd3eec1 AS |
188 | static int aqc_read_string(struct device *dev, enum hwmon_sensor_types type, u32 attr, |
189 | int channel, const char **str) | |
0e35f63f | 190 | { |
2fd3eec1 AS |
191 | struct aqc_data *priv = dev_get_drvdata(dev); |
192 | ||
0e35f63f AS |
193 | switch (type) { |
194 | case hwmon_temp: | |
2fd3eec1 AS |
195 | switch (priv->kind) { |
196 | case d5next: | |
197 | *str = L_D5NEXT_COOLANT_TEMP; | |
198 | break; | |
199 | case farbwerk360: | |
200 | *str = label_temp_sensors[channel]; | |
201 | break; | |
202 | default: | |
203 | break; | |
204 | } | |
0e35f63f AS |
205 | break; |
206 | case hwmon_fan: | |
2fd3eec1 AS |
207 | switch (priv->kind) { |
208 | case d5next: | |
209 | *str = label_d5next_speeds[channel]; | |
210 | break; | |
211 | default: | |
212 | break; | |
213 | } | |
0e35f63f AS |
214 | break; |
215 | case hwmon_power: | |
2fd3eec1 AS |
216 | switch (priv->kind) { |
217 | case d5next: | |
218 | *str = label_d5next_power[channel]; | |
219 | break; | |
220 | default: | |
221 | break; | |
222 | } | |
0e35f63f AS |
223 | break; |
224 | case hwmon_in: | |
2fd3eec1 AS |
225 | switch (priv->kind) { |
226 | case d5next: | |
227 | *str = label_d5next_voltages[channel]; | |
228 | break; | |
229 | default: | |
230 | break; | |
231 | } | |
0e35f63f AS |
232 | break; |
233 | case hwmon_curr: | |
2fd3eec1 AS |
234 | switch (priv->kind) { |
235 | case d5next: | |
236 | *str = label_d5next_current[channel]; | |
237 | break; | |
238 | default: | |
239 | break; | |
240 | } | |
0e35f63f AS |
241 | break; |
242 | default: | |
243 | return -EOPNOTSUPP; | |
244 | } | |
245 | ||
246 | return 0; | |
247 | } | |
248 | ||
2fd3eec1 AS |
249 | static const struct hwmon_ops aqc_hwmon_ops = { |
250 | .is_visible = aqc_is_visible, | |
251 | .read = aqc_read, | |
252 | .read_string = aqc_read_string, | |
0e35f63f AS |
253 | }; |
254 | ||
2fd3eec1 AS |
255 | static const struct hwmon_channel_info *aqc_info[] = { |
256 | HWMON_CHANNEL_INFO(temp, | |
257 | HWMON_T_INPUT | HWMON_T_LABEL, | |
258 | HWMON_T_INPUT | HWMON_T_LABEL, | |
259 | HWMON_T_INPUT | HWMON_T_LABEL, | |
260 | HWMON_T_INPUT | HWMON_T_LABEL), | |
261 | HWMON_CHANNEL_INFO(fan, | |
262 | HWMON_F_INPUT | HWMON_F_LABEL, | |
263 | HWMON_F_INPUT | HWMON_F_LABEL), | |
264 | HWMON_CHANNEL_INFO(power, | |
265 | HWMON_P_INPUT | HWMON_P_LABEL, | |
266 | HWMON_P_INPUT | HWMON_P_LABEL), | |
267 | HWMON_CHANNEL_INFO(in, | |
268 | HWMON_I_INPUT | HWMON_I_LABEL, | |
269 | HWMON_I_INPUT | HWMON_I_LABEL, | |
0e35f63f | 270 | HWMON_I_INPUT | HWMON_I_LABEL), |
2fd3eec1 AS |
271 | HWMON_CHANNEL_INFO(curr, |
272 | HWMON_C_INPUT | HWMON_C_LABEL, | |
273 | HWMON_C_INPUT | HWMON_C_LABEL), | |
0e35f63f AS |
274 | NULL |
275 | }; | |
276 | ||
2fd3eec1 AS |
277 | static const struct hwmon_chip_info aqc_chip_info = { |
278 | .ops = &aqc_hwmon_ops, | |
279 | .info = aqc_info, | |
0e35f63f AS |
280 | }; |
281 | ||
2fd3eec1 AS |
282 | static int aqc_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, |
283 | int size) | |
0e35f63f | 284 | { |
2fd3eec1 AS |
285 | int i, sensor_value; |
286 | struct aqc_data *priv; | |
0e35f63f | 287 | |
2fd3eec1 | 288 | if (report->id != STATUS_REPORT_ID) |
0e35f63f AS |
289 | return 0; |
290 | ||
291 | priv = hid_get_drvdata(hdev); | |
292 | ||
293 | /* Info provided with every report */ | |
2fd3eec1 AS |
294 | priv->serial_number[0] = get_unaligned_be16(data + SERIAL_FIRST_PART); |
295 | priv->serial_number[1] = get_unaligned_be16(data + SERIAL_SECOND_PART); | |
296 | priv->firmware_version = get_unaligned_be16(data + FIRMWARE_VERSION); | |
0e35f63f AS |
297 | |
298 | /* Sensor readings */ | |
2fd3eec1 AS |
299 | switch (priv->kind) { |
300 | case d5next: | |
301 | priv->power_cycles = get_unaligned_be32(data + D5NEXT_POWER_CYCLES); | |
0e35f63f | 302 | |
2fd3eec1 | 303 | priv->temp_input[0] = get_unaligned_be16(data + D5NEXT_COOLANT_TEMP) * 10; |
0e35f63f | 304 | |
2fd3eec1 AS |
305 | priv->speed_input[0] = get_unaligned_be16(data + D5NEXT_PUMP_SPEED); |
306 | priv->speed_input[1] = get_unaligned_be16(data + D5NEXT_FAN_SPEED); | |
0e35f63f | 307 | |
2fd3eec1 AS |
308 | priv->power_input[0] = get_unaligned_be16(data + D5NEXT_PUMP_POWER) * 10000; |
309 | priv->power_input[1] = get_unaligned_be16(data + D5NEXT_FAN_POWER) * 10000; | |
0e35f63f | 310 | |
2fd3eec1 AS |
311 | priv->voltage_input[0] = get_unaligned_be16(data + D5NEXT_PUMP_VOLTAGE) * 10; |
312 | priv->voltage_input[1] = get_unaligned_be16(data + D5NEXT_FAN_VOLTAGE) * 10; | |
313 | priv->voltage_input[2] = get_unaligned_be16(data + D5NEXT_5V_VOLTAGE) * 10; | |
0e35f63f | 314 | |
2fd3eec1 AS |
315 | priv->current_input[0] = get_unaligned_be16(data + D5NEXT_PUMP_CURRENT); |
316 | priv->current_input[1] = get_unaligned_be16(data + D5NEXT_FAN_CURRENT); | |
317 | break; | |
318 | case farbwerk360: | |
319 | /* Temperature sensor readings */ | |
320 | for (i = 0; i < FARBWERK360_NUM_SENSORS; i++) { | |
321 | sensor_value = get_unaligned_be16(data + FARBWERK360_SENSOR_START + | |
322 | i * FARBWERK360_SENSOR_SIZE); | |
323 | if (sensor_value == FARBWERK360_SENSOR_DISCONNECTED) | |
324 | priv->temp_input[i] = -ENODATA; | |
325 | else | |
326 | priv->temp_input[i] = sensor_value * 10; | |
327 | } | |
328 | break; | |
329 | default: | |
330 | break; | |
331 | } | |
0e35f63f AS |
332 | |
333 | priv->updated = jiffies; | |
334 | ||
335 | return 0; | |
336 | } | |
337 | ||
338 | #ifdef CONFIG_DEBUG_FS | |
339 | ||
340 | static int serial_number_show(struct seq_file *seqf, void *unused) | |
341 | { | |
2fd3eec1 | 342 | struct aqc_data *priv = seqf->private; |
0e35f63f AS |
343 | |
344 | seq_printf(seqf, "%05u-%05u\n", priv->serial_number[0], priv->serial_number[1]); | |
345 | ||
346 | return 0; | |
347 | } | |
348 | DEFINE_SHOW_ATTRIBUTE(serial_number); | |
349 | ||
350 | static int firmware_version_show(struct seq_file *seqf, void *unused) | |
351 | { | |
2fd3eec1 | 352 | struct aqc_data *priv = seqf->private; |
0e35f63f AS |
353 | |
354 | seq_printf(seqf, "%u\n", priv->firmware_version); | |
355 | ||
356 | return 0; | |
357 | } | |
358 | DEFINE_SHOW_ATTRIBUTE(firmware_version); | |
359 | ||
360 | static int power_cycles_show(struct seq_file *seqf, void *unused) | |
361 | { | |
2fd3eec1 | 362 | struct aqc_data *priv = seqf->private; |
0e35f63f AS |
363 | |
364 | seq_printf(seqf, "%u\n", priv->power_cycles); | |
365 | ||
366 | return 0; | |
367 | } | |
368 | DEFINE_SHOW_ATTRIBUTE(power_cycles); | |
369 | ||
2fd3eec1 | 370 | static void aqc_debugfs_init(struct aqc_data *priv) |
0e35f63f | 371 | { |
2fd3eec1 | 372 | char name[64]; |
0e35f63f | 373 | |
2fd3eec1 AS |
374 | scnprintf(name, sizeof(name), "%s_%s-%s", "aquacomputer", priv->name, |
375 | dev_name(&priv->hdev->dev)); | |
0e35f63f AS |
376 | |
377 | priv->debugfs = debugfs_create_dir(name, NULL); | |
378 | debugfs_create_file("serial_number", 0444, priv->debugfs, priv, &serial_number_fops); | |
379 | debugfs_create_file("firmware_version", 0444, priv->debugfs, priv, &firmware_version_fops); | |
2fd3eec1 AS |
380 | |
381 | if (priv->kind == d5next) | |
382 | debugfs_create_file("power_cycles", 0444, priv->debugfs, priv, &power_cycles_fops); | |
0e35f63f AS |
383 | } |
384 | ||
385 | #else | |
386 | ||
2fd3eec1 | 387 | static void aqc_debugfs_init(struct aqc_data *priv) |
0e35f63f AS |
388 | { |
389 | } | |
390 | ||
391 | #endif | |
392 | ||
2fd3eec1 | 393 | static int aqc_probe(struct hid_device *hdev, const struct hid_device_id *id) |
0e35f63f | 394 | { |
2fd3eec1 | 395 | struct aqc_data *priv; |
0e35f63f AS |
396 | int ret; |
397 | ||
398 | priv = devm_kzalloc(&hdev->dev, sizeof(*priv), GFP_KERNEL); | |
399 | if (!priv) | |
400 | return -ENOMEM; | |
401 | ||
402 | priv->hdev = hdev; | |
403 | hid_set_drvdata(hdev, priv); | |
404 | ||
2fd3eec1 | 405 | priv->updated = jiffies - STATUS_UPDATE_INTERVAL; |
0e35f63f AS |
406 | |
407 | ret = hid_parse(hdev); | |
408 | if (ret) | |
409 | return ret; | |
410 | ||
411 | ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); | |
412 | if (ret) | |
413 | return ret; | |
414 | ||
415 | ret = hid_hw_open(hdev); | |
416 | if (ret) | |
417 | goto fail_and_stop; | |
418 | ||
2fd3eec1 AS |
419 | switch (hdev->product) { |
420 | case USB_PRODUCT_ID_D5NEXT: | |
421 | priv->kind = d5next; | |
422 | break; | |
423 | case USB_PRODUCT_ID_FARBWERK360: | |
424 | priv->kind = farbwerk360; | |
425 | break; | |
426 | default: | |
427 | break; | |
428 | } | |
429 | ||
430 | priv->name = aqc_device_names[priv->kind]; | |
431 | ||
432 | priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, priv->name, priv, | |
433 | &aqc_chip_info, NULL); | |
0e35f63f AS |
434 | |
435 | if (IS_ERR(priv->hwmon_dev)) { | |
436 | ret = PTR_ERR(priv->hwmon_dev); | |
437 | goto fail_and_close; | |
438 | } | |
439 | ||
2fd3eec1 | 440 | aqc_debugfs_init(priv); |
0e35f63f AS |
441 | |
442 | return 0; | |
443 | ||
444 | fail_and_close: | |
445 | hid_hw_close(hdev); | |
446 | fail_and_stop: | |
447 | hid_hw_stop(hdev); | |
448 | return ret; | |
449 | } | |
450 | ||
2fd3eec1 | 451 | static void aqc_remove(struct hid_device *hdev) |
0e35f63f | 452 | { |
2fd3eec1 | 453 | struct aqc_data *priv = hid_get_drvdata(hdev); |
0e35f63f AS |
454 | |
455 | debugfs_remove_recursive(priv->debugfs); | |
456 | hwmon_device_unregister(priv->hwmon_dev); | |
457 | ||
458 | hid_hw_close(hdev); | |
459 | hid_hw_stop(hdev); | |
460 | } | |
461 | ||
2fd3eec1 AS |
462 | static const struct hid_device_id aqc_table[] = { |
463 | { HID_USB_DEVICE(USB_VENDOR_ID_AQUACOMPUTER, USB_PRODUCT_ID_D5NEXT) }, | |
464 | { HID_USB_DEVICE(USB_VENDOR_ID_AQUACOMPUTER, USB_PRODUCT_ID_FARBWERK360) }, | |
465 | { } | |
0e35f63f AS |
466 | }; |
467 | ||
2fd3eec1 | 468 | MODULE_DEVICE_TABLE(hid, aqc_table); |
0e35f63f | 469 | |
2fd3eec1 | 470 | static struct hid_driver aqc_driver = { |
0e35f63f | 471 | .name = DRIVER_NAME, |
2fd3eec1 AS |
472 | .id_table = aqc_table, |
473 | .probe = aqc_probe, | |
474 | .remove = aqc_remove, | |
475 | .raw_event = aqc_raw_event, | |
0e35f63f AS |
476 | }; |
477 | ||
2fd3eec1 | 478 | static int __init aqc_init(void) |
0e35f63f | 479 | { |
2fd3eec1 | 480 | return hid_register_driver(&aqc_driver); |
0e35f63f AS |
481 | } |
482 | ||
2fd3eec1 | 483 | static void __exit aqc_exit(void) |
0e35f63f | 484 | { |
2fd3eec1 | 485 | hid_unregister_driver(&aqc_driver); |
0e35f63f AS |
486 | } |
487 | ||
488 | /* Request to initialize after the HID bus to ensure it's not being loaded before */ | |
2fd3eec1 AS |
489 | late_initcall(aqc_init); |
490 | module_exit(aqc_exit); | |
0e35f63f AS |
491 | |
492 | MODULE_LICENSE("GPL"); | |
493 | MODULE_AUTHOR("Aleksa Savic <savicaleksa83@gmail.com>"); | |
2fd3eec1 | 494 | MODULE_DESCRIPTION("Hwmon driver for Aquacomputer devices"); |