|
| 1 | +// SPDX-License-Identifier: GPL-2.0+ |
| 2 | +/* |
| 3 | + * teensyloopctrl.c - hwmon driver for the Teensy Fan Controller |
| 4 | + * |
| 5 | + * Originally: |
| 6 | + * liquidctl.c - hwmon driver for the NZXT Smart Device and Grid+ V3 |
| 7 | + * |
| 8 | + * Copyright 2019 Jonas Malaco <jonas@protocubo.io> |
| 9 | + */ |
| 10 | + |
| 11 | +#include <linux/hid.h> |
| 12 | +#include <linux/hwmon.h> |
| 13 | +#include <linux/kernel.h> |
| 14 | +#include <linux/module.h> |
| 15 | + |
| 16 | +#define DRVNAME "teensyloopctrl" |
| 17 | +#define MAX_REPORT_SIZE 65 |
| 18 | + |
| 19 | +struct tloopctrl_device_data { |
| 20 | + struct hid_device *hid_dev; |
| 21 | + struct device *hwmon_dev; |
| 22 | + |
| 23 | + int temp_count; |
| 24 | + int fan_count; |
| 25 | + int pwm_count; |
| 26 | + const char *const *temp_label; |
| 27 | + const char *const *fan_label; |
| 28 | +// const char *const *pwm_label; |
| 29 | + u64 *temp_input; |
| 30 | + long *fan_input; |
| 31 | + u64 *pwm_input; |
| 32 | + u8 *buf; |
| 33 | +}; |
| 34 | + |
| 35 | +static umode_t tloopctrl_is_visible(const void *data, |
| 36 | + enum hwmon_sensor_types type, |
| 37 | + u32 attr, int channel) { |
| 38 | + struct tloopctrl_device_data *ldata; |
| 39 | + struct hid_device *hdev; |
| 40 | + |
| 41 | + ldata = (struct tloopctrl_device_data *)data; |
| 42 | + hdev = ldata->hid_dev; |
| 43 | + |
| 44 | + if (hdev->dev_rsize < 10 || hdev->dev_rdesc[1] != 0xAB || hdev->dev_rdesc[2] != 0xFF || hdev->dev_rdesc[4] != 0x00 || hdev->dev_rdesc[5] != 0x02) { |
| 45 | +// printk(KERN_WARNING "tloopctrl_is_visible() return: 0"); |
| 46 | + return 0; |
| 47 | + } |
| 48 | + else { |
| 49 | + return S_IRUGO; |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | +static int tloopctrl_read(struct device *dev, enum hwmon_sensor_types type, |
| 54 | + u32 attr, int channel, long *val) { |
| 55 | + struct tloopctrl_device_data *ldata = dev_get_drvdata(dev); |
| 56 | + |
| 57 | + switch (type) { |
| 58 | + case hwmon_temp: |
| 59 | + if (attr != hwmon_temp_input || channel >= ldata->temp_count) |
| 60 | + return -EINVAL; |
| 61 | + *val = (int)ldata->temp_input[channel]; |
| 62 | + break; |
| 63 | + case hwmon_fan: |
| 64 | + if (attr != hwmon_fan_input || channel >= ldata->fan_count) |
| 65 | + return -EINVAL; |
| 66 | + *val = ldata->fan_input[channel]; |
| 67 | + break; |
| 68 | + case hwmon_pwm: |
| 69 | + if (attr != hwmon_pwm_input || channel >= ldata->pwm_count) |
| 70 | + return -EINVAL; |
| 71 | + *val = (int)ldata->pwm_input[channel]; |
| 72 | + break; |
| 73 | + default: |
| 74 | + return -EINVAL; |
| 75 | + } |
| 76 | + return 0; |
| 77 | +} |
| 78 | + |
| 79 | +static int tloopctrl_read_string(struct device *dev, |
| 80 | + enum hwmon_sensor_types type, u32 attr, |
| 81 | + int channel, const char **str) { |
| 82 | + struct tloopctrl_device_data *ldata = dev_get_drvdata(dev); |
| 83 | + |
| 84 | + switch (type) { |
| 85 | + case hwmon_temp: |
| 86 | + if (attr != hwmon_temp_label || channel >= ldata->temp_count || |
| 87 | + !ldata->temp_label[channel]) |
| 88 | + return -EINVAL; |
| 89 | + *str = ldata->temp_label[channel]; |
| 90 | + break; |
| 91 | + case hwmon_fan: |
| 92 | + if (attr != hwmon_fan_label || channel >= ldata->fan_count || |
| 93 | + !ldata->fan_label[channel]) |
| 94 | + return -EINVAL; |
| 95 | + *str = ldata->fan_label[channel]; |
| 96 | + break; |
| 97 | +// case hwmon_pwm: |
| 98 | +// if (attr != hwmon_pwm_label || channel >= ldata->pwm_count || |
| 99 | +// !ldata->pwm_label[channel]) |
| 100 | +// return -EINVAL; |
| 101 | +// *str = ldata->pwm_label[channel]; |
| 102 | +// break; |
| 103 | + default: |
| 104 | + return -EINVAL; |
| 105 | + } |
| 106 | + return 0; |
| 107 | +} |
| 108 | + |
| 109 | +static const struct hwmon_ops tloopctrl_hwmon_ops = { |
| 110 | + .is_visible = tloopctrl_is_visible, |
| 111 | + .read = tloopctrl_read, |
| 112 | + .read_string = tloopctrl_read_string, |
| 113 | +}; |
| 114 | + |
| 115 | +#define DEVNAME_TLOOPCTRL_DEVICE "teensyloopctrl" |
| 116 | +#define TLOOPCTRL_DEVICE_TEMP_COUNT 7 |
| 117 | +#define TLOOPCTRL_DEVICE_FAN_COUNT 6 |
| 118 | +#define TLOOPCTRL_DEVICE_PWM_COUNT 1 |
| 119 | + |
| 120 | +static const u32 tloopctrl_device_fan_config[] = { |
| 121 | + HWMON_F_INPUT, |
| 122 | + HWMON_F_INPUT, |
| 123 | + HWMON_F_INPUT, |
| 124 | + HWMON_F_INPUT, |
| 125 | + HWMON_F_INPUT, |
| 126 | + HWMON_F_INPUT, |
| 127 | + 0 |
| 128 | +}; |
| 129 | + |
| 130 | +static const struct hwmon_channel_info tloopctrl_device_fan = { |
| 131 | + .type = hwmon_fan, |
| 132 | + .config = tloopctrl_device_fan_config, |
| 133 | +}; |
| 134 | + |
| 135 | +static const u32 tloopctrl_device_temp_config[] = { |
| 136 | + HWMON_T_INPUT, |
| 137 | + HWMON_T_INPUT, |
| 138 | + HWMON_T_INPUT, |
| 139 | + HWMON_T_INPUT, |
| 140 | + HWMON_T_INPUT, |
| 141 | + HWMON_T_INPUT, |
| 142 | + HWMON_T_INPUT, |
| 143 | + 0 |
| 144 | +}; |
| 145 | + |
| 146 | +static const struct hwmon_channel_info tloopctrl_device_temp = { |
| 147 | + .type = hwmon_temp, |
| 148 | + .config = tloopctrl_device_temp_config, |
| 149 | +}; |
| 150 | + |
| 151 | +static const u32 tloopctrl_device_pwm_config[] = { |
| 152 | + HWMON_PWM_INPUT, |
| 153 | + 0 |
| 154 | +}; |
| 155 | + |
| 156 | +static const struct hwmon_channel_info tloopctrl_device_pwm = { |
| 157 | + .type = hwmon_pwm, |
| 158 | + .config = tloopctrl_device_pwm_config, |
| 159 | +}; |
| 160 | + |
| 161 | +static const struct hwmon_channel_info *tloopctrl_device_info[] = { |
| 162 | + &tloopctrl_device_fan, |
| 163 | + &tloopctrl_device_temp, |
| 164 | + &tloopctrl_device_pwm, |
| 165 | + NULL |
| 166 | +}; |
| 167 | + |
| 168 | +static const struct hwmon_chip_info tloopctrl_device_chip_info = { |
| 169 | + .ops = &tloopctrl_hwmon_ops, |
| 170 | + .info = tloopctrl_device_info, |
| 171 | +}; |
| 172 | + |
| 173 | +#define USB_VENDOR_ID_ARDUINO 0x16C0 |
| 174 | +#define USB_DEVICE_ID_TLOOPCTRL_DEVICE 0x0486 |
| 175 | + |
| 176 | +// TODO |
| 177 | +#define STATUS_REPORT_ID 4 |
| 178 | +#define STATUS_MIN_BYTES 16 |
| 179 | + |
| 180 | +#define show_ctx() \ |
| 181 | + printk(KERN_DEBUG "%s:%d: irq: %lu, serving_softirq: %lu, nmi: %lu, task: %u\n", \ |
| 182 | + __FUNCTION__, __LINE__, in_irq(), in_serving_softirq(), in_nmi(), in_task()); |
| 183 | + |
| 184 | +static int tloopctrl_raw_event(struct hid_device *hdev, |
| 185 | + struct hid_report *report, u8 *data, int size) { |
| 186 | + struct tloopctrl_device_data *ldata; |
| 187 | + |
| 188 | + /* TODO show_ctx(): in hard irq, how much should we do here? */ |
| 189 | + |
| 190 | + if (size <= 0 || report->maxfield != 1 || report->field[0]->application != 0xFFAB0200) |
| 191 | + return 0; |
| 192 | + |
| 193 | + ldata = hid_get_drvdata(hdev); |
| 194 | + |
| 195 | + /* TODO reads don't need the latest data, but each store must be atomic */ |
| 196 | + switch (hdev->product) { |
| 197 | + case USB_DEVICE_ID_TLOOPCTRL_DEVICE: |
| 198 | + if (data[0] == 0xDA) { |
| 199 | + ldata->temp_input[0] = le64_to_cpup((__le64 *) (data + 2)); // supplyTemp |
| 200 | + ldata->temp_input[1] = le64_to_cpup((__le64 *) (data + 6)); // returnTemp |
| 201 | + ldata->temp_input[2] = le64_to_cpup((__le64 *) (data + 10)); // caseTemp |
| 202 | + ldata->temp_input[3] = le64_to_cpup((__le64 *) (data + 14)); // aux1Temp |
| 203 | + ldata->temp_input[4] = le64_to_cpup((__le64 *) (data + 18)); // aux2Temp |
| 204 | + ldata->temp_input[5] = le64_to_cpup((__le64 *) (data + 22)); // deltaT |
| 205 | +// ldata->temp_input[5] = le64_to_cpup((__le64 *) (data + 26)); // |
| 206 | + ldata->temp_input[6] = le64_to_cpup((__le64 *) (data + 30)); // setpoint |
| 207 | + |
| 208 | + ldata->fan_input[0] = le16_to_cpup((__le16 *) (data + 34)); // rpm1 |
| 209 | + ldata->fan_input[1] = le16_to_cpup((__le16 *) (data + 36)); // rpm2 |
| 210 | + ldata->fan_input[2] = le16_to_cpup((__le16 *) (data + 38)); // rpm3 |
| 211 | + ldata->fan_input[3] = le16_to_cpup((__le16 *) (data + 40)); // rpm4 |
| 212 | + ldata->fan_input[4] = le16_to_cpup((__le16 *) (data + 42)); // rpm5 |
| 213 | + ldata->fan_input[5] = le16_to_cpup((__le16 *) (data + 44)); // rpm6 |
| 214 | + |
| 215 | + ldata->pwm_input[0] = 0; |
| 216 | +// ldata->pwm_input[0] = le64_to_cpup((__le64 *) (data + 26)); // fan % (PID) |
| 217 | +// ldata->pwm_input[0] = le64_to_cpup((__le64 *) (data + 26)); // fan % (%-tbl) |
| 218 | + } |
| 219 | + break; |
| 220 | + default: |
| 221 | + return 0; |
| 222 | + } |
| 223 | + return 0; |
| 224 | +} |
| 225 | + |
| 226 | +static int tloopctrl_initialize(struct tloopctrl_device_data *ldata) { |
| 227 | + // TODO |
| 228 | + int ret; |
| 229 | + memset(ldata->buf, '\0', MAX_REPORT_SIZE); |
| 230 | + *(ldata->buf) = 0; // report id ? |
| 231 | + *(ldata->buf + 1) = 0; //0xC0; |
| 232 | + *(ldata->buf + 2) = 0; //0xFF; |
| 233 | + ret = hid_hw_output_report(ldata->hid_dev, ldata->buf, MAX_REPORT_SIZE); |
| 234 | + //printk(KERN_WARNING "tloopctrl_initialize() ret: %d", ret); |
| 235 | + if (ret < 0) |
| 236 | + return ret; |
| 237 | + if (ret != MAX_REPORT_SIZE) |
| 238 | + return -EINVAL; |
| 239 | + |
| 240 | + return 0; |
| 241 | +} |
| 242 | + |
| 243 | +static const struct hid_device_id tloopctrl_table[] = { |
| 244 | + {HID_USB_DEVICE(USB_VENDOR_ID_ARDUINO, USB_DEVICE_ID_TLOOPCTRL_DEVICE)}, |
| 245 | + {} |
| 246 | +}; |
| 247 | + |
| 248 | +MODULE_DEVICE_TABLE(hid, tloopctrl_table |
| 249 | +); |
| 250 | + |
| 251 | +static int tloopctrl_probe(struct hid_device *hdev, |
| 252 | + const struct hid_device_id *id) { |
| 253 | + struct tloopctrl_device_data *ldata; |
| 254 | + struct device *hwmon_dev; |
| 255 | + const struct hwmon_chip_info *chip_info; |
| 256 | + char *chip_name; |
| 257 | + int ret; |
| 258 | + |
| 259 | + ldata = devm_kzalloc(&hdev->dev, sizeof(*ldata), GFP_KERNEL); |
| 260 | + if (!ldata) |
| 261 | + return -ENOMEM; |
| 262 | + |
| 263 | + switch (hdev->product) { |
| 264 | + case USB_DEVICE_ID_TLOOPCTRL_DEVICE: |
| 265 | + chip_name = DEVNAME_TLOOPCTRL_DEVICE; |
| 266 | + ldata->temp_count = TLOOPCTRL_DEVICE_TEMP_COUNT; |
| 267 | + ldata->fan_count = TLOOPCTRL_DEVICE_FAN_COUNT; |
| 268 | + ldata->pwm_count = TLOOPCTRL_DEVICE_PWM_COUNT; |
| 269 | + ldata->temp_label = NULL; |
| 270 | + ldata->fan_label = NULL; |
| 271 | +// ldata->pwm_label = NULL; |
| 272 | + chip_info = &tloopctrl_device_chip_info; |
| 273 | + break; |
| 274 | + default: |
| 275 | + return -EINVAL; |
| 276 | + } |
| 277 | + hid_info(hdev, "device: %s\n", chip_name); |
| 278 | + |
| 279 | + ldata->buf = devm_kmalloc(&hdev->dev, MAX_REPORT_SIZE, GFP_KERNEL); |
| 280 | + if (!ldata->buf) |
| 281 | + return -ENOMEM; |
| 282 | + |
| 283 | + ldata->temp_input = devm_kcalloc(&hdev->dev, ldata->temp_count, |
| 284 | + sizeof(*ldata->temp_input), GFP_KERNEL); |
| 285 | + if (!ldata->temp_input) |
| 286 | + return -ENOMEM; |
| 287 | + |
| 288 | + ldata->fan_input = devm_kcalloc(&hdev->dev, ldata->fan_count, |
| 289 | + sizeof(*ldata->fan_input), GFP_KERNEL); |
| 290 | + if (!ldata->fan_input) |
| 291 | + return -ENOMEM; |
| 292 | + |
| 293 | + ldata->pwm_input = devm_kcalloc(&hdev->dev, ldata->pwm_count, |
| 294 | + sizeof(*ldata->pwm_input), GFP_KERNEL); |
| 295 | + if (!ldata->pwm_input) |
| 296 | + return -ENOMEM; |
| 297 | + |
| 298 | + ldata->hid_dev = hdev; |
| 299 | + hid_set_drvdata(hdev, ldata); |
| 300 | + |
| 301 | + ret = hid_parse(hdev); |
| 302 | + if (ret) { |
| 303 | + hid_err(hdev, "hid_parse failed with %d\n", ret); |
| 304 | + return ret; |
| 305 | + } |
| 306 | + |
| 307 | + /* keep hidraw so user-space can (easily) take care of the other |
| 308 | + * features of the device (e.g. LEDs) */ |
| 309 | + ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); |
| 310 | + if (ret) { |
| 311 | + hid_err(hdev, "hid_hw_start failed with %d\n", ret); |
| 312 | + goto rec_stop_hid; |
| 313 | + } |
| 314 | + |
| 315 | + ret = hid_hw_open(hdev); |
| 316 | + if (ret) { |
| 317 | + hid_err(hdev, "hid_hw_open failed with %d\n", ret); |
| 318 | + goto rec_close_hid; |
| 319 | + } |
| 320 | + |
| 321 | + hwmon_dev = devm_hwmon_device_register_with_info(&hdev->dev, chip_name, |
| 322 | + ldata, chip_info, |
| 323 | + NULL); |
| 324 | + if (IS_ERR(hwmon_dev)) { |
| 325 | + hid_err(hdev, "failed to register hwmon device\n"); |
| 326 | + ret = PTR_ERR(hwmon_dev); |
| 327 | + goto rec_close_hid; |
| 328 | + } |
| 329 | + ldata->hwmon_dev = hwmon_dev; |
| 330 | + |
| 331 | + ret = tloopctrl_initialize(ldata); |
| 332 | + if (ret) { |
| 333 | + hid_err(hdev, "failed to initialize device"); |
| 334 | + goto rec_close_hid; |
| 335 | + } |
| 336 | + |
| 337 | + hid_info(hdev, "probing successful\n"); |
| 338 | + return 0; |
| 339 | + |
| 340 | + rec_close_hid: |
| 341 | + hid_hw_close(hdev); |
| 342 | + rec_stop_hid: |
| 343 | + hid_hw_stop(hdev); |
| 344 | + return ret; |
| 345 | +} |
| 346 | + |
| 347 | +static void tloopctrl_remove(struct hid_device *hdev) { |
| 348 | + hid_hw_close(hdev); |
| 349 | + hid_hw_stop(hdev); |
| 350 | +} |
| 351 | + |
| 352 | +static struct hid_driver tloopctrl_driver = { |
| 353 | + .name = DRVNAME, |
| 354 | + .id_table = tloopctrl_table, |
| 355 | + .probe = tloopctrl_probe, |
| 356 | + .remove = tloopctrl_remove, |
| 357 | + .raw_event = tloopctrl_raw_event, |
| 358 | +}; |
| 359 | + |
| 360 | +module_hid_driver(tloopctrl_driver); |
| 361 | + |
| 362 | +MODULE_LICENSE("GPL"); |
| 363 | +MODULE_AUTHOR("Jonas Malaco <jonas@protocubo.io>"); |
| 364 | +MODULE_AUTHOR("Jack Davis <c@jtd.me>"); |
| 365 | +MODULE_DESCRIPTION("Hwmon driver for Teensy Fan Controller"); |
0 commit comments