Commit | Line | Data |
---|---|---|
a10e763b | 1 | // SPDX-License-Identifier: GPL-2.0-only |
6c7ad07e HK |
2 | /* |
3 | * Simple USB RGB LED driver | |
4 | * | |
5 | * Copyright 2016 Heiner Kallweit <hkallweit1@gmail.com> | |
6 | * Based on drivers/hid/hid-thingm.c and | |
7 | * drivers/usb/misc/usbled.c | |
6c7ad07e HK |
8 | */ |
9 | ||
10 | #include <linux/hid.h> | |
11 | #include <linux/hidraw.h> | |
12 | #include <linux/leds.h> | |
13 | #include <linux/module.h> | |
14 | #include <linux/mutex.h> | |
15 | ||
16 | #include "hid-ids.h" | |
17 | ||
18 | enum hidled_report_type { | |
19 | RAW_REQUEST, | |
20 | OUTPUT_REPORT | |
21 | }; | |
22 | ||
23 | enum hidled_type { | |
24 | RISO_KAGAKU, | |
25 | DREAM_CHEEKY, | |
007414e8 | 26 | THINGM, |
de908650 | 27 | DELCOM, |
9d1e048c | 28 | LUXAFOR, |
6c7ad07e HK |
29 | }; |
30 | ||
31 | static unsigned const char riso_kagaku_tbl[] = { | |
32 | /* R+2G+4B -> riso kagaku color index */ | |
33 | [0] = 0, /* black */ | |
34 | [1] = 2, /* red */ | |
35 | [2] = 1, /* green */ | |
36 | [3] = 5, /* yellow */ | |
37 | [4] = 3, /* blue */ | |
38 | [5] = 6, /* magenta */ | |
39 | [6] = 4, /* cyan */ | |
40 | [7] = 7 /* white */ | |
41 | }; | |
42 | ||
43 | #define RISO_KAGAKU_IX(r, g, b) riso_kagaku_tbl[((r)?1:0)+((g)?2:0)+((b)?4:0)] | |
44 | ||
de908650 HK |
45 | union delcom_packet { |
46 | __u8 data[8]; | |
47 | struct { | |
48 | __u8 major_cmd; | |
49 | __u8 minor_cmd; | |
50 | __u8 data_lsb; | |
51 | __u8 data_msb; | |
52 | } tx; | |
53 | struct { | |
54 | __u8 cmd; | |
55 | } rx; | |
56 | struct { | |
57 | __le16 family_code; | |
58 | __le16 security_code; | |
59 | __u8 fw_version; | |
60 | } fw; | |
61 | }; | |
62 | ||
63 | #define DELCOM_GREEN_LED 0 | |
64 | #define DELCOM_RED_LED 1 | |
65 | #define DELCOM_BLUE_LED 2 | |
66 | ||
6c7ad07e | 67 | struct hidled_device; |
5bc83936 | 68 | struct hidled_rgb; |
6c7ad07e HK |
69 | |
70 | struct hidled_config { | |
71 | enum hidled_type type; | |
72 | const char *name; | |
73 | const char *short_name; | |
74 | enum led_brightness max_brightness; | |
5bc83936 | 75 | int num_leds; |
6c7ad07e HK |
76 | size_t report_size; |
77 | enum hidled_report_type report_type; | |
6c7ad07e HK |
78 | int (*init)(struct hidled_device *ldev); |
79 | int (*write)(struct led_classdev *cdev, enum led_brightness br); | |
80 | }; | |
81 | ||
82 | struct hidled_led { | |
83 | struct led_classdev cdev; | |
5bc83936 | 84 | struct hidled_rgb *rgb; |
6c7ad07e HK |
85 | char name[32]; |
86 | }; | |
87 | ||
5bc83936 HK |
88 | struct hidled_rgb { |
89 | struct hidled_device *ldev; | |
6c7ad07e HK |
90 | struct hidled_led red; |
91 | struct hidled_led green; | |
92 | struct hidled_led blue; | |
5bc83936 HK |
93 | u8 num; |
94 | }; | |
95 | ||
96 | struct hidled_device { | |
97 | const struct hidled_config *config; | |
6c7ad07e | 98 | struct hid_device *hdev; |
5bc83936 | 99 | struct hidled_rgb *rgb; |
3d1355b3 | 100 | u8 *buf; |
6c7ad07e HK |
101 | struct mutex lock; |
102 | }; | |
103 | ||
104 | #define MAX_REPORT_SIZE 16 | |
105 | ||
106 | #define to_hidled_led(arg) container_of(arg, struct hidled_led, cdev) | |
107 | ||
108 | static bool riso_kagaku_switch_green_blue; | |
109 | module_param(riso_kagaku_switch_green_blue, bool, S_IRUGO | S_IWUSR); | |
110 | MODULE_PARM_DESC(riso_kagaku_switch_green_blue, | |
111 | "switch green and blue RGB component for Riso Kagaku devices"); | |
112 | ||
113 | static int hidled_send(struct hidled_device *ldev, __u8 *buf) | |
114 | { | |
115 | int ret; | |
116 | ||
6c7ad07e HK |
117 | mutex_lock(&ldev->lock); |
118 | ||
3d1355b3 HK |
119 | /* |
120 | * buffer provided to hid_hw_raw_request must not be on the stack | |
121 | * and must not be part of a data structure | |
122 | */ | |
123 | memcpy(ldev->buf, buf, ldev->config->report_size); | |
124 | ||
6c7ad07e | 125 | if (ldev->config->report_type == RAW_REQUEST) |
3d1355b3 | 126 | ret = hid_hw_raw_request(ldev->hdev, buf[0], ldev->buf, |
6c7ad07e HK |
127 | ldev->config->report_size, |
128 | HID_FEATURE_REPORT, | |
129 | HID_REQ_SET_REPORT); | |
130 | else if (ldev->config->report_type == OUTPUT_REPORT) | |
3d1355b3 | 131 | ret = hid_hw_output_report(ldev->hdev, ldev->buf, |
6c7ad07e HK |
132 | ldev->config->report_size); |
133 | else | |
134 | ret = -EINVAL; | |
135 | ||
136 | mutex_unlock(&ldev->lock); | |
137 | ||
138 | if (ret < 0) | |
139 | return ret; | |
140 | ||
141 | return ret == ldev->config->report_size ? 0 : -EMSGSIZE; | |
142 | } | |
143 | ||
43745730 HK |
144 | /* reading data is supported for report type RAW_REQUEST only */ |
145 | static int hidled_recv(struct hidled_device *ldev, __u8 *buf) | |
146 | { | |
147 | int ret; | |
148 | ||
149 | if (ldev->config->report_type != RAW_REQUEST) | |
150 | return -EINVAL; | |
151 | ||
43745730 HK |
152 | mutex_lock(&ldev->lock); |
153 | ||
3d1355b3 HK |
154 | memcpy(ldev->buf, buf, ldev->config->report_size); |
155 | ||
156 | ret = hid_hw_raw_request(ldev->hdev, buf[0], ldev->buf, | |
43745730 HK |
157 | ldev->config->report_size, |
158 | HID_FEATURE_REPORT, | |
159 | HID_REQ_SET_REPORT); | |
160 | if (ret < 0) | |
161 | goto err; | |
162 | ||
3d1355b3 | 163 | ret = hid_hw_raw_request(ldev->hdev, buf[0], ldev->buf, |
43745730 HK |
164 | ldev->config->report_size, |
165 | HID_FEATURE_REPORT, | |
166 | HID_REQ_GET_REPORT); | |
3d1355b3 HK |
167 | |
168 | memcpy(buf, ldev->buf, ldev->config->report_size); | |
43745730 HK |
169 | err: |
170 | mutex_unlock(&ldev->lock); | |
171 | ||
172 | return ret < 0 ? ret : 0; | |
173 | } | |
174 | ||
5bc83936 | 175 | static u8 riso_kagaku_index(struct hidled_rgb *rgb) |
6c7ad07e HK |
176 | { |
177 | enum led_brightness r, g, b; | |
178 | ||
5bc83936 HK |
179 | r = rgb->red.cdev.brightness; |
180 | g = rgb->green.cdev.brightness; | |
181 | b = rgb->blue.cdev.brightness; | |
6c7ad07e HK |
182 | |
183 | if (riso_kagaku_switch_green_blue) | |
184 | return RISO_KAGAKU_IX(r, b, g); | |
185 | else | |
186 | return RISO_KAGAKU_IX(r, g, b); | |
187 | } | |
188 | ||
189 | static int riso_kagaku_write(struct led_classdev *cdev, enum led_brightness br) | |
190 | { | |
191 | struct hidled_led *led = to_hidled_led(cdev); | |
5bc83936 | 192 | struct hidled_rgb *rgb = led->rgb; |
6c7ad07e HK |
193 | __u8 buf[MAX_REPORT_SIZE] = {}; |
194 | ||
5bc83936 | 195 | buf[1] = riso_kagaku_index(rgb); |
6c7ad07e | 196 | |
5bc83936 | 197 | return hidled_send(rgb->ldev, buf); |
6c7ad07e HK |
198 | } |
199 | ||
200 | static int dream_cheeky_write(struct led_classdev *cdev, enum led_brightness br) | |
201 | { | |
202 | struct hidled_led *led = to_hidled_led(cdev); | |
5bc83936 | 203 | struct hidled_rgb *rgb = led->rgb; |
6c7ad07e HK |
204 | __u8 buf[MAX_REPORT_SIZE] = {}; |
205 | ||
5bc83936 HK |
206 | buf[1] = rgb->red.cdev.brightness; |
207 | buf[2] = rgb->green.cdev.brightness; | |
208 | buf[3] = rgb->blue.cdev.brightness; | |
6c7ad07e HK |
209 | buf[7] = 0x1a; |
210 | buf[8] = 0x05; | |
211 | ||
5bc83936 | 212 | return hidled_send(rgb->ldev, buf); |
6c7ad07e HK |
213 | } |
214 | ||
215 | static int dream_cheeky_init(struct hidled_device *ldev) | |
216 | { | |
217 | __u8 buf[MAX_REPORT_SIZE] = {}; | |
218 | ||
219 | /* Dream Cheeky magic */ | |
220 | buf[1] = 0x1f; | |
221 | buf[2] = 0x02; | |
222 | buf[4] = 0x5f; | |
223 | buf[7] = 0x1a; | |
224 | buf[8] = 0x03; | |
225 | ||
226 | return hidled_send(ldev, buf); | |
227 | } | |
228 | ||
007414e8 HK |
229 | static int _thingm_write(struct led_classdev *cdev, enum led_brightness br, |
230 | u8 offset) | |
231 | { | |
232 | struct hidled_led *led = to_hidled_led(cdev); | |
34d9810b | 233 | __u8 buf[MAX_REPORT_SIZE] = { 1, 'c' }; |
007414e8 HK |
234 | |
235 | buf[2] = led->rgb->red.cdev.brightness; | |
236 | buf[3] = led->rgb->green.cdev.brightness; | |
237 | buf[4] = led->rgb->blue.cdev.brightness; | |
238 | buf[7] = led->rgb->num + offset; | |
239 | ||
240 | return hidled_send(led->rgb->ldev, buf); | |
241 | } | |
242 | ||
243 | static int thingm_write_v1(struct led_classdev *cdev, enum led_brightness br) | |
244 | { | |
245 | return _thingm_write(cdev, br, 0); | |
246 | } | |
247 | ||
248 | static int thingm_write(struct led_classdev *cdev, enum led_brightness br) | |
249 | { | |
250 | return _thingm_write(cdev, br, 1); | |
251 | } | |
252 | ||
253 | static const struct hidled_config hidled_config_thingm_v1 = { | |
254 | .name = "ThingM blink(1) v1", | |
255 | .short_name = "thingm", | |
256 | .max_brightness = 255, | |
257 | .num_leds = 1, | |
258 | .report_size = 9, | |
259 | .report_type = RAW_REQUEST, | |
007414e8 HK |
260 | .write = thingm_write_v1, |
261 | }; | |
262 | ||
263 | static int thingm_init(struct hidled_device *ldev) | |
264 | { | |
34d9810b | 265 | __u8 buf[MAX_REPORT_SIZE] = { 1, 'v' }; |
007414e8 HK |
266 | int ret; |
267 | ||
268 | ret = hidled_recv(ldev, buf); | |
269 | if (ret) | |
270 | return ret; | |
271 | ||
272 | /* Check for firmware major version 1 */ | |
273 | if (buf[3] == '1') | |
274 | ldev->config = &hidled_config_thingm_v1; | |
275 | ||
276 | return 0; | |
277 | } | |
278 | ||
de908650 HK |
279 | static inline int delcom_get_lednum(const struct hidled_led *led) |
280 | { | |
281 | if (led == &led->rgb->red) | |
282 | return DELCOM_RED_LED; | |
283 | else if (led == &led->rgb->green) | |
284 | return DELCOM_GREEN_LED; | |
285 | else | |
286 | return DELCOM_BLUE_LED; | |
287 | } | |
288 | ||
289 | static int delcom_enable_led(struct hidled_led *led) | |
290 | { | |
291 | union delcom_packet dp = { .tx.major_cmd = 101, .tx.minor_cmd = 12 }; | |
292 | ||
293 | dp.tx.data_lsb = 1 << delcom_get_lednum(led); | |
294 | dp.tx.data_msb = 0; | |
295 | ||
296 | return hidled_send(led->rgb->ldev, dp.data); | |
297 | } | |
298 | ||
299 | static int delcom_set_pwm(struct hidled_led *led) | |
300 | { | |
301 | union delcom_packet dp = { .tx.major_cmd = 101, .tx.minor_cmd = 34 }; | |
302 | ||
303 | dp.tx.data_lsb = delcom_get_lednum(led); | |
304 | dp.tx.data_msb = led->cdev.brightness; | |
305 | ||
306 | return hidled_send(led->rgb->ldev, dp.data); | |
307 | } | |
308 | ||
309 | static int delcom_write(struct led_classdev *cdev, enum led_brightness br) | |
310 | { | |
311 | struct hidled_led *led = to_hidled_led(cdev); | |
312 | int ret; | |
313 | ||
314 | /* | |
315 | * enable LED | |
316 | * We can't do this in the init function already because the device | |
317 | * is internally reset later. | |
318 | */ | |
319 | ret = delcom_enable_led(led); | |
320 | if (ret) | |
321 | return ret; | |
322 | ||
323 | return delcom_set_pwm(led); | |
324 | } | |
325 | ||
326 | static int delcom_init(struct hidled_device *ldev) | |
327 | { | |
328 | union delcom_packet dp = { .rx.cmd = 104 }; | |
329 | int ret; | |
330 | ||
331 | ret = hidled_recv(ldev, dp.data); | |
332 | if (ret) | |
333 | return ret; | |
334 | /* | |
335 | * Several Delcom devices share the same USB VID/PID | |
336 | * Check for family id 2 for Visual Signal Indicator | |
337 | */ | |
f4c109b6 | 338 | return le16_to_cpu(dp.fw.family_code) == 2 ? 0 : -ENODEV; |
de908650 HK |
339 | } |
340 | ||
9d1e048c HK |
341 | static int luxafor_write(struct led_classdev *cdev, enum led_brightness br) |
342 | { | |
343 | struct hidled_led *led = to_hidled_led(cdev); | |
344 | __u8 buf[MAX_REPORT_SIZE] = { [1] = 1 }; | |
345 | ||
346 | buf[2] = led->rgb->num + 1; | |
347 | buf[3] = led->rgb->red.cdev.brightness; | |
348 | buf[4] = led->rgb->green.cdev.brightness; | |
349 | buf[5] = led->rgb->blue.cdev.brightness; | |
350 | ||
351 | return hidled_send(led->rgb->ldev, buf); | |
352 | } | |
353 | ||
6c7ad07e HK |
354 | static const struct hidled_config hidled_configs[] = { |
355 | { | |
356 | .type = RISO_KAGAKU, | |
357 | .name = "Riso Kagaku Webmail Notifier", | |
358 | .short_name = "riso_kagaku", | |
359 | .max_brightness = 1, | |
5bc83936 | 360 | .num_leds = 1, |
6c7ad07e HK |
361 | .report_size = 6, |
362 | .report_type = OUTPUT_REPORT, | |
6c7ad07e HK |
363 | .write = riso_kagaku_write, |
364 | }, | |
365 | { | |
366 | .type = DREAM_CHEEKY, | |
367 | .name = "Dream Cheeky Webmail Notifier", | |
368 | .short_name = "dream_cheeky", | |
116c3f4a | 369 | .max_brightness = 63, |
5bc83936 | 370 | .num_leds = 1, |
6c7ad07e HK |
371 | .report_size = 9, |
372 | .report_type = RAW_REQUEST, | |
6c7ad07e HK |
373 | .init = dream_cheeky_init, |
374 | .write = dream_cheeky_write, | |
375 | }, | |
007414e8 HK |
376 | { |
377 | .type = THINGM, | |
378 | .name = "ThingM blink(1)", | |
379 | .short_name = "thingm", | |
380 | .max_brightness = 255, | |
381 | .num_leds = 2, | |
382 | .report_size = 9, | |
383 | .report_type = RAW_REQUEST, | |
007414e8 HK |
384 | .init = thingm_init, |
385 | .write = thingm_write, | |
386 | }, | |
de908650 HK |
387 | { |
388 | .type = DELCOM, | |
389 | .name = "Delcom Visual Signal Indicator G2", | |
390 | .short_name = "delcom", | |
391 | .max_brightness = 100, | |
392 | .num_leds = 1, | |
393 | .report_size = 8, | |
394 | .report_type = RAW_REQUEST, | |
395 | .init = delcom_init, | |
396 | .write = delcom_write, | |
397 | }, | |
9d1e048c HK |
398 | { |
399 | .type = LUXAFOR, | |
400 | .name = "Greynut Luxafor", | |
401 | .short_name = "luxafor", | |
402 | .max_brightness = 255, | |
403 | .num_leds = 6, | |
404 | .report_size = 9, | |
405 | .report_type = OUTPUT_REPORT, | |
406 | .write = luxafor_write, | |
407 | }, | |
6c7ad07e HK |
408 | }; |
409 | ||
410 | static int hidled_init_led(struct hidled_led *led, const char *color_name, | |
5bc83936 | 411 | struct hidled_rgb *rgb, unsigned int minor) |
6c7ad07e | 412 | { |
5bc83936 HK |
413 | const struct hidled_config *config = rgb->ldev->config; |
414 | ||
415 | if (config->num_leds > 1) | |
416 | snprintf(led->name, sizeof(led->name), "%s%u:%s:led%u", | |
417 | config->short_name, minor, color_name, rgb->num); | |
418 | else | |
419 | snprintf(led->name, sizeof(led->name), "%s%u:%s", | |
420 | config->short_name, minor, color_name); | |
6c7ad07e | 421 | led->cdev.name = led->name; |
5bc83936 HK |
422 | led->cdev.max_brightness = config->max_brightness; |
423 | led->cdev.brightness_set_blocking = config->write; | |
6c7ad07e | 424 | led->cdev.flags = LED_HW_PLUGGABLE; |
5bc83936 | 425 | led->rgb = rgb; |
6c7ad07e | 426 | |
5bc83936 | 427 | return devm_led_classdev_register(&rgb->ldev->hdev->dev, &led->cdev); |
6c7ad07e HK |
428 | } |
429 | ||
5bc83936 | 430 | static int hidled_init_rgb(struct hidled_rgb *rgb, unsigned int minor) |
6c7ad07e HK |
431 | { |
432 | int ret; | |
433 | ||
434 | /* Register the red diode */ | |
5bc83936 | 435 | ret = hidled_init_led(&rgb->red, "red", rgb, minor); |
6c7ad07e HK |
436 | if (ret) |
437 | return ret; | |
438 | ||
439 | /* Register the green diode */ | |
5bc83936 | 440 | ret = hidled_init_led(&rgb->green, "green", rgb, minor); |
6c7ad07e HK |
441 | if (ret) |
442 | return ret; | |
443 | ||
444 | /* Register the blue diode */ | |
5bc83936 | 445 | return hidled_init_led(&rgb->blue, "blue", rgb, minor); |
6c7ad07e HK |
446 | } |
447 | ||
448 | static int hidled_probe(struct hid_device *hdev, const struct hid_device_id *id) | |
449 | { | |
450 | struct hidled_device *ldev; | |
451 | unsigned int minor; | |
452 | int ret, i; | |
453 | ||
454 | ldev = devm_kzalloc(&hdev->dev, sizeof(*ldev), GFP_KERNEL); | |
455 | if (!ldev) | |
456 | return -ENOMEM; | |
457 | ||
3d1355b3 HK |
458 | ldev->buf = devm_kmalloc(&hdev->dev, MAX_REPORT_SIZE, GFP_KERNEL); |
459 | if (!ldev->buf) | |
460 | return -ENOMEM; | |
461 | ||
6c7ad07e HK |
462 | ret = hid_parse(hdev); |
463 | if (ret) | |
464 | return ret; | |
465 | ||
466 | ldev->hdev = hdev; | |
467 | mutex_init(&ldev->lock); | |
468 | ||
469 | for (i = 0; !ldev->config && i < ARRAY_SIZE(hidled_configs); i++) | |
470 | if (hidled_configs[i].type == id->driver_data) | |
471 | ldev->config = &hidled_configs[i]; | |
472 | ||
473 | if (!ldev->config) | |
474 | return -EINVAL; | |
475 | ||
476 | if (ldev->config->init) { | |
477 | ret = ldev->config->init(ldev); | |
478 | if (ret) | |
479 | return ret; | |
480 | } | |
481 | ||
5bc83936 HK |
482 | ldev->rgb = devm_kcalloc(&hdev->dev, ldev->config->num_leds, |
483 | sizeof(struct hidled_rgb), GFP_KERNEL); | |
484 | if (!ldev->rgb) | |
485 | return -ENOMEM; | |
486 | ||
6c7ad07e HK |
487 | ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); |
488 | if (ret) | |
489 | return ret; | |
490 | ||
491 | minor = ((struct hidraw *) hdev->hidraw)->minor; | |
492 | ||
5bc83936 HK |
493 | for (i = 0; i < ldev->config->num_leds; i++) { |
494 | ldev->rgb[i].ldev = ldev; | |
495 | ldev->rgb[i].num = i; | |
496 | ret = hidled_init_rgb(&ldev->rgb[i], minor); | |
497 | if (ret) { | |
498 | hid_hw_stop(hdev); | |
499 | return ret; | |
500 | } | |
6c7ad07e HK |
501 | } |
502 | ||
503 | hid_info(hdev, "%s initialized\n", ldev->config->name); | |
504 | ||
505 | return 0; | |
506 | } | |
507 | ||
508 | static const struct hid_device_id hidled_table[] = { | |
509 | { HID_USB_DEVICE(USB_VENDOR_ID_RISO_KAGAKU, | |
510 | USB_DEVICE_ID_RI_KA_WEBMAIL), .driver_data = RISO_KAGAKU }, | |
511 | { HID_USB_DEVICE(USB_VENDOR_ID_DREAM_CHEEKY, | |
512 | USB_DEVICE_ID_DREAM_CHEEKY_WN), .driver_data = DREAM_CHEEKY }, | |
513 | { HID_USB_DEVICE(USB_VENDOR_ID_DREAM_CHEEKY, | |
514 | USB_DEVICE_ID_DREAM_CHEEKY_FA), .driver_data = DREAM_CHEEKY }, | |
007414e8 HK |
515 | { HID_USB_DEVICE(USB_VENDOR_ID_THINGM, |
516 | USB_DEVICE_ID_BLINK1), .driver_data = THINGM }, | |
de908650 HK |
517 | { HID_USB_DEVICE(USB_VENDOR_ID_DELCOM, |
518 | USB_DEVICE_ID_DELCOM_VISUAL_IND), .driver_data = DELCOM }, | |
9d1e048c HK |
519 | { HID_USB_DEVICE(USB_VENDOR_ID_MICROCHIP, |
520 | USB_DEVICE_ID_LUXAFOR), .driver_data = LUXAFOR }, | |
6c7ad07e HK |
521 | { } |
522 | }; | |
523 | MODULE_DEVICE_TABLE(hid, hidled_table); | |
524 | ||
525 | static struct hid_driver hidled_driver = { | |
526 | .name = "hid-led", | |
527 | .probe = hidled_probe, | |
528 | .id_table = hidled_table, | |
529 | }; | |
530 | ||
531 | module_hid_driver(hidled_driver); | |
532 | ||
533 | MODULE_LICENSE("GPL"); | |
534 | MODULE_AUTHOR("Heiner Kallweit <hkallweit1@gmail.com>"); | |
535 | MODULE_DESCRIPTION("Simple USB RGB LED driver"); |