// SPDX-License-Identifier: GPL-2.0+ /* * hwmon driver for Aquacomputer devices (D5 Next, Farbwerk 360) * * Aquacomputer devices send HID reports (with ID 0x01) every second to report * sensor values. * * Copyright 2021 Aleksa Savic */ #include #include #include #include #include #include #include #define USB_VENDOR_ID_AQUACOMPUTER 0x0c70 #define USB_PRODUCT_ID_D5NEXT 0xf00e #define USB_PRODUCT_ID_FARBWERK360 0xf010 enum kinds { d5next, farbwerk360 }; static const char *const aqc_device_names[] = { [d5next] = "d5next", [farbwerk360] = "farbwerk360" }; #define DRIVER_NAME "aquacomputer_d5next" #define STATUS_REPORT_ID 0x01 #define STATUS_UPDATE_INTERVAL (2 * HZ) /* In seconds */ #define SERIAL_FIRST_PART 3 #define SERIAL_SECOND_PART 5 #define FIRMWARE_VERSION 13 /* Register offsets for the D5 Next pump */ #define D5NEXT_POWER_CYCLES 24 #define D5NEXT_COOLANT_TEMP 87 #define D5NEXT_PUMP_SPEED 116 #define D5NEXT_FAN_SPEED 103 #define D5NEXT_PUMP_POWER 114 #define D5NEXT_FAN_POWER 101 #define D5NEXT_PUMP_VOLTAGE 110 #define D5NEXT_FAN_VOLTAGE 97 #define D5NEXT_5V_VOLTAGE 57 #define D5NEXT_PUMP_CURRENT 112 #define D5NEXT_FAN_CURRENT 99 /* Register offsets for the Farbwerk 360 RGB controller */ #define FARBWERK360_NUM_SENSORS 4 #define FARBWERK360_SENSOR_START 0x32 #define FARBWERK360_SENSOR_SIZE 0x02 #define FARBWERK360_SENSOR_DISCONNECTED 0x7FFF /* Labels for D5 Next */ #define L_D5NEXT_COOLANT_TEMP "Coolant temp" static const char *const label_d5next_speeds[] = { "Pump speed", "Fan speed" }; static const char *const label_d5next_power[] = { "Pump power", "Fan power" }; static const char *const label_d5next_voltages[] = { "Pump voltage", "Fan voltage", "+5V voltage" }; static const char *const label_d5next_current[] = { "Pump current", "Fan current" }; /* Labels for Farbwerk 360 temperature sensors */ static const char *const label_temp_sensors[] = { "Sensor 1", "Sensor 2", "Sensor 3", "Sensor 4" }; struct aqc_data { struct hid_device *hdev; struct device *hwmon_dev; struct dentry *debugfs; enum kinds kind; const char *name; /* General info, same across all devices */ u32 serial_number[2]; u16 firmware_version; /* D5 Next specific - how many times the device was powered on */ u32 power_cycles; /* Sensor values */ s32 temp_input[4]; u16 speed_input[2]; u32 power_input[2]; u16 voltage_input[3]; u16 current_input[2]; unsigned long updated; }; static umode_t aqc_is_visible(const void *data, enum hwmon_sensor_types type, u32 attr, int channel) { const struct aqc_data *priv = data; switch (type) { case hwmon_temp: switch (priv->kind) { case d5next: if (channel == 0) return 0444; break; case farbwerk360: return 0444; default: break; } break; case hwmon_fan: case hwmon_power: case hwmon_in: case hwmon_curr: switch (priv->kind) { case d5next: return 0444; default: break; } break; default: break; } return 0; } static int aqc_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, long *val) { struct aqc_data *priv = dev_get_drvdata(dev); if (time_after(jiffies, priv->updated + STATUS_UPDATE_INTERVAL)) return -ENODATA; switch (type) { case hwmon_temp: if (priv->temp_input[channel] == -ENODATA) return -ENODATA; *val = priv->temp_input[channel]; break; case hwmon_fan: *val = priv->speed_input[channel]; break; case hwmon_power: *val = priv->power_input[channel]; break; case hwmon_in: *val = priv->voltage_input[channel]; break; case hwmon_curr: *val = priv->current_input[channel]; break; default: return -EOPNOTSUPP; } return 0; } static int aqc_read_string(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel, const char **str) { struct aqc_data *priv = dev_get_drvdata(dev); switch (type) { case hwmon_temp: switch (priv->kind) { case d5next: *str = L_D5NEXT_COOLANT_TEMP; break; case farbwerk360: *str = label_temp_sensors[channel]; break; default: break; } break; case hwmon_fan: switch (priv->kind) { case d5next: *str = label_d5next_speeds[channel]; break; default: break; } break; case hwmon_power: switch (priv->kind) { case d5next: *str = label_d5next_power[channel]; break; default: break; } break; case hwmon_in: switch (priv->kind) { case d5next: *str = label_d5next_voltages[channel]; break; default: break; } break; case hwmon_curr: switch (priv->kind) { case d5next: *str = label_d5next_current[channel]; break; default: break; } break; default: return -EOPNOTSUPP; } return 0; } static const struct hwmon_ops aqc_hwmon_ops = { .is_visible = aqc_is_visible, .read = aqc_read, .read_string = aqc_read_string, }; static const struct hwmon_channel_info *aqc_info[] = { HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT | HWMON_T_LABEL, HWMON_T_INPUT | HWMON_T_LABEL, HWMON_T_INPUT | HWMON_T_LABEL, 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(power, HWMON_P_INPUT | HWMON_P_LABEL, HWMON_P_INPUT | HWMON_P_LABEL), HWMON_CHANNEL_INFO(in, HWMON_I_INPUT | HWMON_I_LABEL, HWMON_I_INPUT | HWMON_I_LABEL, HWMON_I_INPUT | HWMON_I_LABEL), HWMON_CHANNEL_INFO(curr, HWMON_C_INPUT | HWMON_C_LABEL, HWMON_C_INPUT | HWMON_C_LABEL), NULL }; static const struct hwmon_chip_info aqc_chip_info = { .ops = &aqc_hwmon_ops, .info = aqc_info, }; static int aqc_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) { int i, sensor_value; struct aqc_data *priv; if (report->id != STATUS_REPORT_ID) return 0; priv = hid_get_drvdata(hdev); /* Info provided with every report */ priv->serial_number[0] = get_unaligned_be16(data + SERIAL_FIRST_PART); priv->serial_number[1] = get_unaligned_be16(data + SERIAL_SECOND_PART); priv->firmware_version = get_unaligned_be16(data + FIRMWARE_VERSION); /* Sensor readings */ switch (priv->kind) { case d5next: priv->power_cycles = get_unaligned_be32(data + D5NEXT_POWER_CYCLES); priv->temp_input[0] = get_unaligned_be16(data + D5NEXT_COOLANT_TEMP) * 10; priv->speed_input[0] = get_unaligned_be16(data + D5NEXT_PUMP_SPEED); priv->speed_input[1] = get_unaligned_be16(data + D5NEXT_FAN_SPEED); priv->power_input[0] = get_unaligned_be16(data + D5NEXT_PUMP_POWER) * 10000; priv->power_input[1] = get_unaligned_be16(data + D5NEXT_FAN_POWER) * 10000; priv->voltage_input[0] = get_unaligned_be16(data + D5NEXT_PUMP_VOLTAGE) * 10; priv->voltage_input[1] = get_unaligned_be16(data + D5NEXT_FAN_VOLTAGE) * 10; priv->voltage_input[2] = get_unaligned_be16(data + D5NEXT_5V_VOLTAGE) * 10; priv->current_input[0] = get_unaligned_be16(data + D5NEXT_PUMP_CURRENT); priv->current_input[1] = get_unaligned_be16(data + D5NEXT_FAN_CURRENT); break; case farbwerk360: /* Temperature sensor readings */ for (i = 0; i < FARBWERK360_NUM_SENSORS; i++) { sensor_value = get_unaligned_be16(data + FARBWERK360_SENSOR_START + i * FARBWERK360_SENSOR_SIZE); if (sensor_value == FARBWERK360_SENSOR_DISCONNECTED) priv->temp_input[i] = -ENODATA; else priv->temp_input[i] = sensor_value * 10; } break; default: break; } priv->updated = jiffies; return 0; } #ifdef CONFIG_DEBUG_FS static int serial_number_show(struct seq_file *seqf, void *unused) { struct aqc_data *priv = seqf->private; seq_printf(seqf, "%05u-%05u\n", priv->serial_number[0], priv->serial_number[1]); return 0; } DEFINE_SHOW_ATTRIBUTE(serial_number); static int firmware_version_show(struct seq_file *seqf, void *unused) { struct aqc_data *priv = seqf->private; seq_printf(seqf, "%u\n", priv->firmware_version); return 0; } DEFINE_SHOW_ATTRIBUTE(firmware_version); static int power_cycles_show(struct seq_file *seqf, void *unused) { struct aqc_data *priv = seqf->private; seq_printf(seqf, "%u\n", priv->power_cycles); return 0; } DEFINE_SHOW_ATTRIBUTE(power_cycles); static void aqc_debugfs_init(struct aqc_data *priv) { char name[64]; scnprintf(name, sizeof(name), "%s_%s-%s", "aquacomputer", priv->name, dev_name(&priv->hdev->dev)); priv->debugfs = debugfs_create_dir(name, NULL); debugfs_create_file("serial_number", 0444, priv->debugfs, priv, &serial_number_fops); debugfs_create_file("firmware_version", 0444, priv->debugfs, priv, &firmware_version_fops); if (priv->kind == d5next) debugfs_create_file("power_cycles", 0444, priv->debugfs, priv, &power_cycles_fops); } #else static void aqc_debugfs_init(struct aqc_data *priv) { } #endif static int aqc_probe(struct hid_device *hdev, const struct hid_device_id *id) { struct aqc_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); priv->updated = jiffies - STATUS_UPDATE_INTERVAL; ret = hid_parse(hdev); if (ret) return ret; ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); if (ret) return ret; ret = hid_hw_open(hdev); if (ret) goto fail_and_stop; switch (hdev->product) { case USB_PRODUCT_ID_D5NEXT: priv->kind = d5next; break; case USB_PRODUCT_ID_FARBWERK360: priv->kind = farbwerk360; break; default: break; } priv->name = aqc_device_names[priv->kind]; priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, priv->name, priv, &aqc_chip_info, NULL); if (IS_ERR(priv->hwmon_dev)) { ret = PTR_ERR(priv->hwmon_dev); goto fail_and_close; } aqc_debugfs_init(priv); return 0; fail_and_close: hid_hw_close(hdev); fail_and_stop: hid_hw_stop(hdev); return ret; } static void aqc_remove(struct hid_device *hdev) { struct aqc_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 aqc_table[] = { { HID_USB_DEVICE(USB_VENDOR_ID_AQUACOMPUTER, USB_PRODUCT_ID_D5NEXT) }, { HID_USB_DEVICE(USB_VENDOR_ID_AQUACOMPUTER, USB_PRODUCT_ID_FARBWERK360) }, { } }; MODULE_DEVICE_TABLE(hid, aqc_table); static struct hid_driver aqc_driver = { .name = DRIVER_NAME, .id_table = aqc_table, .probe = aqc_probe, .remove = aqc_remove, .raw_event = aqc_raw_event, }; static int __init aqc_init(void) { return hid_register_driver(&aqc_driver); } static void __exit aqc_exit(void) { hid_unregister_driver(&aqc_driver); } /* Request to initialize after the HID bus to ensure it's not being loaded before */ late_initcall(aqc_init); module_exit(aqc_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Aleksa Savic "); MODULE_DESCRIPTION("Hwmon driver for Aquacomputer devices");