Simon Wood | 75dbb95 | 2013-01-31 08:07:07 -0700 | [diff] [blame] | 1 | /* |
| 2 | * HID driver for Steelseries SRW-S1 |
| 3 | * |
| 4 | * Copyright (c) 2013 Simon Wood |
| 5 | */ |
| 6 | |
| 7 | /* |
| 8 | * This program is free software; you can redistribute it and/or modify it |
| 9 | * under the terms of the GNU General Public License as published by the Free |
| 10 | * Software Foundation; either version 2 of the License, or (at your option) |
| 11 | * any later version. |
| 12 | */ |
| 13 | |
| 14 | #include <linux/device.h> |
| 15 | #include <linux/hid.h> |
| 16 | #include <linux/module.h> |
| 17 | |
| 18 | #include "hid-ids.h" |
| 19 | |
Simon Wood | b52b506 | 2013-05-02 19:43:14 -0600 | [diff] [blame] | 20 | #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \ |
| 21 | (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES)) |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 22 | #define SRWS1_NUMBER_LEDS 15 |
| 23 | struct steelseries_srws1_data { |
| 24 | __u16 led_state; |
Jiri Kosina | 7e41576 | 2013-01-31 16:51:47 +0100 | [diff] [blame] | 25 | /* the last element is used for setting all leds simultaneously */ |
| 26 | struct led_classdev *led[SRWS1_NUMBER_LEDS + 1]; |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 27 | }; |
| 28 | #endif |
| 29 | |
Simon Wood | 5492606 | 2013-01-31 08:07:08 -0700 | [diff] [blame] | 30 | /* Fixed report descriptor for Steelseries SRW-S1 wheel controller |
| 31 | * |
| 32 | * The original descriptor hides the sensitivity and assists dials |
| 33 | * a custom vendor usage page. This inserts a patch to make them |
| 34 | * appear in the 'Generic Desktop' usage. |
| 35 | */ |
| 36 | |
| 37 | static __u8 steelseries_srws1_rdesc_fixed[] = { |
| 38 | 0x05, 0x01, /* Usage Page (Desktop) */ |
| 39 | 0x09, 0x08, /* Usage (MultiAxis), Changed */ |
| 40 | 0xA1, 0x01, /* Collection (Application), */ |
| 41 | 0xA1, 0x02, /* Collection (Logical), */ |
| 42 | 0x95, 0x01, /* Report Count (1), */ |
| 43 | 0x05, 0x01, /* Changed Usage Page (Desktop), */ |
| 44 | 0x09, 0x30, /* Changed Usage (X), */ |
| 45 | 0x16, 0xF8, 0xF8, /* Logical Minimum (-1800), */ |
| 46 | 0x26, 0x08, 0x07, /* Logical Maximum (1800), */ |
| 47 | 0x65, 0x14, /* Unit (Degrees), */ |
| 48 | 0x55, 0x0F, /* Unit Exponent (15), */ |
| 49 | 0x75, 0x10, /* Report Size (16), */ |
| 50 | 0x81, 0x02, /* Input (Variable), */ |
| 51 | 0x09, 0x31, /* Changed Usage (Y), */ |
| 52 | 0x15, 0x00, /* Logical Minimum (0), */ |
| 53 | 0x26, 0xFF, 0x03, /* Logical Maximum (1023), */ |
| 54 | 0x75, 0x0C, /* Report Size (12), */ |
| 55 | 0x81, 0x02, /* Input (Variable), */ |
| 56 | 0x09, 0x32, /* Changed Usage (Z), */ |
| 57 | 0x15, 0x00, /* Logical Minimum (0), */ |
| 58 | 0x26, 0xFF, 0x03, /* Logical Maximum (1023), */ |
| 59 | 0x75, 0x0C, /* Report Size (12), */ |
| 60 | 0x81, 0x02, /* Input (Variable), */ |
| 61 | 0x05, 0x01, /* Usage Page (Desktop), */ |
| 62 | 0x09, 0x39, /* Usage (Hat Switch), */ |
| 63 | 0x25, 0x07, /* Logical Maximum (7), */ |
| 64 | 0x35, 0x00, /* Physical Minimum (0), */ |
| 65 | 0x46, 0x3B, 0x01, /* Physical Maximum (315), */ |
| 66 | 0x65, 0x14, /* Unit (Degrees), */ |
| 67 | 0x75, 0x04, /* Report Size (4), */ |
| 68 | 0x95, 0x01, /* Report Count (1), */ |
| 69 | 0x81, 0x02, /* Input (Variable), */ |
| 70 | 0x25, 0x01, /* Logical Maximum (1), */ |
| 71 | 0x45, 0x01, /* Physical Maximum (1), */ |
| 72 | 0x65, 0x00, /* Unit, */ |
| 73 | 0x75, 0x01, /* Report Size (1), */ |
| 74 | 0x95, 0x03, /* Report Count (3), */ |
| 75 | 0x81, 0x01, /* Input (Constant), */ |
| 76 | 0x05, 0x09, /* Usage Page (Button), */ |
| 77 | 0x19, 0x01, /* Usage Minimum (01h), */ |
| 78 | 0x29, 0x11, /* Usage Maximum (11h), */ |
| 79 | 0x95, 0x11, /* Report Count (17), */ |
| 80 | 0x81, 0x02, /* Input (Variable), */ |
| 81 | /* ---- Dial patch starts here ---- */ |
| 82 | 0x05, 0x01, /* Usage Page (Desktop), */ |
| 83 | 0x09, 0x33, /* Usage (RX), */ |
| 84 | 0x75, 0x04, /* Report Size (4), */ |
| 85 | 0x95, 0x02, /* Report Count (2), */ |
| 86 | 0x15, 0x00, /* Logical Minimum (0), */ |
| 87 | 0x25, 0x0b, /* Logical Maximum (b), */ |
| 88 | 0x81, 0x02, /* Input (Variable), */ |
| 89 | 0x09, 0x35, /* Usage (RZ), */ |
| 90 | 0x75, 0x04, /* Report Size (4), */ |
| 91 | 0x95, 0x01, /* Report Count (1), */ |
| 92 | 0x25, 0x03, /* Logical Maximum (3), */ |
| 93 | 0x81, 0x02, /* Input (Variable), */ |
| 94 | /* ---- Dial patch ends here ---- */ |
| 95 | 0x06, 0x00, 0xFF, /* Usage Page (FF00h), */ |
| 96 | 0x09, 0x01, /* Usage (01h), */ |
| 97 | 0x75, 0x04, /* Changed Report Size (4), */ |
| 98 | 0x95, 0x0D, /* Changed Report Count (13), */ |
| 99 | 0x81, 0x02, /* Input (Variable), */ |
| 100 | 0xC0, /* End Collection, */ |
| 101 | 0xA1, 0x02, /* Collection (Logical), */ |
| 102 | 0x09, 0x02, /* Usage (02h), */ |
| 103 | 0x75, 0x08, /* Report Size (8), */ |
| 104 | 0x95, 0x10, /* Report Count (16), */ |
| 105 | 0x91, 0x02, /* Output (Variable), */ |
| 106 | 0xC0, /* End Collection, */ |
| 107 | 0xC0 /* End Collection */ |
| 108 | }; |
| 109 | |
Simon Wood | b52b506 | 2013-05-02 19:43:14 -0600 | [diff] [blame] | 110 | #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \ |
| 111 | (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES)) |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 112 | static void steelseries_srws1_set_leds(struct hid_device *hdev, __u16 leds) |
| 113 | { |
| 114 | struct list_head *report_list = &hdev->report_enum[HID_OUTPUT_REPORT].report_list; |
| 115 | struct hid_report *report = list_entry(report_list->next, struct hid_report, list); |
| 116 | __s32 *value = report->field[0]->value; |
| 117 | |
| 118 | value[0] = 0x40; |
| 119 | value[1] = leds & 0xFF; |
| 120 | value[2] = leds >> 8; |
| 121 | value[3] = 0x00; |
| 122 | value[4] = 0x00; |
| 123 | value[5] = 0x00; |
| 124 | value[6] = 0x00; |
| 125 | value[7] = 0x00; |
| 126 | value[8] = 0x00; |
| 127 | value[9] = 0x00; |
| 128 | value[10] = 0x00; |
| 129 | value[11] = 0x00; |
| 130 | value[12] = 0x00; |
| 131 | value[13] = 0x00; |
| 132 | value[14] = 0x00; |
| 133 | value[15] = 0x00; |
| 134 | |
Benjamin Tissoires | d8814272 | 2013-02-25 11:31:46 +0100 | [diff] [blame] | 135 | hid_hw_request(hdev, report, HID_REQ_SET_REPORT); |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 136 | |
| 137 | /* Note: LED change does not show on device until the device is read/polled */ |
| 138 | } |
| 139 | |
Simon Wood | e25d780 | 2013-01-31 08:07:10 -0700 | [diff] [blame] | 140 | static void steelseries_srws1_led_all_set_brightness(struct led_classdev *led_cdev, |
| 141 | enum led_brightness value) |
| 142 | { |
| 143 | struct device *dev = led_cdev->dev->parent; |
Geliang Tang | ee79a8f | 2015-12-27 17:25:21 +0800 | [diff] [blame] | 144 | struct hid_device *hid = to_hid_device(dev); |
Simon Wood | e25d780 | 2013-01-31 08:07:10 -0700 | [diff] [blame] | 145 | struct steelseries_srws1_data *drv_data = hid_get_drvdata(hid); |
| 146 | |
| 147 | if (!drv_data) { |
| 148 | hid_err(hid, "Device data not found."); |
| 149 | return; |
| 150 | } |
| 151 | |
| 152 | if (value == LED_OFF) |
| 153 | drv_data->led_state = 0; |
| 154 | else |
| 155 | drv_data->led_state = (1 << (SRWS1_NUMBER_LEDS + 1)) - 1; |
| 156 | |
| 157 | steelseries_srws1_set_leds(hid, drv_data->led_state); |
| 158 | } |
| 159 | |
| 160 | static enum led_brightness steelseries_srws1_led_all_get_brightness(struct led_classdev *led_cdev) |
| 161 | { |
| 162 | struct device *dev = led_cdev->dev->parent; |
Geliang Tang | ee79a8f | 2015-12-27 17:25:21 +0800 | [diff] [blame] | 163 | struct hid_device *hid = to_hid_device(dev); |
Simon Wood | e25d780 | 2013-01-31 08:07:10 -0700 | [diff] [blame] | 164 | struct steelseries_srws1_data *drv_data; |
| 165 | |
| 166 | drv_data = hid_get_drvdata(hid); |
| 167 | |
| 168 | if (!drv_data) { |
| 169 | hid_err(hid, "Device data not found."); |
| 170 | return LED_OFF; |
| 171 | } |
| 172 | |
| 173 | return (drv_data->led_state >> SRWS1_NUMBER_LEDS) ? LED_FULL : LED_OFF; |
| 174 | } |
| 175 | |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 176 | static void steelseries_srws1_led_set_brightness(struct led_classdev *led_cdev, |
| 177 | enum led_brightness value) |
| 178 | { |
| 179 | struct device *dev = led_cdev->dev->parent; |
Geliang Tang | ee79a8f | 2015-12-27 17:25:21 +0800 | [diff] [blame] | 180 | struct hid_device *hid = to_hid_device(dev); |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 181 | struct steelseries_srws1_data *drv_data = hid_get_drvdata(hid); |
| 182 | int i, state = 0; |
| 183 | |
| 184 | if (!drv_data) { |
| 185 | hid_err(hid, "Device data not found."); |
| 186 | return; |
| 187 | } |
| 188 | |
| 189 | for (i = 0; i < SRWS1_NUMBER_LEDS; i++) { |
| 190 | if (led_cdev != drv_data->led[i]) |
| 191 | continue; |
| 192 | |
| 193 | state = (drv_data->led_state >> i) & 1; |
| 194 | if (value == LED_OFF && state) { |
| 195 | drv_data->led_state &= ~(1 << i); |
| 196 | steelseries_srws1_set_leds(hid, drv_data->led_state); |
| 197 | } else if (value != LED_OFF && !state) { |
| 198 | drv_data->led_state |= 1 << i; |
| 199 | steelseries_srws1_set_leds(hid, drv_data->led_state); |
| 200 | } |
| 201 | break; |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | static enum led_brightness steelseries_srws1_led_get_brightness(struct led_classdev *led_cdev) |
| 206 | { |
| 207 | struct device *dev = led_cdev->dev->parent; |
Geliang Tang | ee79a8f | 2015-12-27 17:25:21 +0800 | [diff] [blame] | 208 | struct hid_device *hid = to_hid_device(dev); |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 209 | struct steelseries_srws1_data *drv_data; |
| 210 | int i, value = 0; |
| 211 | |
| 212 | drv_data = hid_get_drvdata(hid); |
| 213 | |
| 214 | if (!drv_data) { |
| 215 | hid_err(hid, "Device data not found."); |
| 216 | return LED_OFF; |
| 217 | } |
| 218 | |
| 219 | for (i = 0; i < SRWS1_NUMBER_LEDS; i++) |
| 220 | if (led_cdev == drv_data->led[i]) { |
| 221 | value = (drv_data->led_state >> i) & 1; |
| 222 | break; |
| 223 | } |
| 224 | |
| 225 | return value ? LED_FULL : LED_OFF; |
| 226 | } |
| 227 | |
| 228 | static int steelseries_srws1_probe(struct hid_device *hdev, |
| 229 | const struct hid_device_id *id) |
| 230 | { |
| 231 | int ret, i; |
| 232 | struct led_classdev *led; |
| 233 | size_t name_sz; |
| 234 | char *name; |
| 235 | |
| 236 | struct steelseries_srws1_data *drv_data = kzalloc(sizeof(*drv_data), GFP_KERNEL); |
| 237 | |
| 238 | if (drv_data == NULL) { |
| 239 | hid_err(hdev, "can't alloc SRW-S1 memory\n"); |
| 240 | return -ENOMEM; |
| 241 | } |
| 242 | |
| 243 | hid_set_drvdata(hdev, drv_data); |
| 244 | |
| 245 | ret = hid_parse(hdev); |
| 246 | if (ret) { |
| 247 | hid_err(hdev, "parse failed\n"); |
| 248 | goto err_free; |
| 249 | } |
| 250 | |
Kees Cook | 41df7f6 | 2013-09-11 21:56:53 +0200 | [diff] [blame] | 251 | if (!hid_validate_values(hdev, HID_OUTPUT_REPORT, 0, 0, 16)) { |
| 252 | ret = -ENODEV; |
| 253 | goto err_free; |
| 254 | } |
| 255 | |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 256 | ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); |
| 257 | if (ret) { |
| 258 | hid_err(hdev, "hw start failed\n"); |
| 259 | goto err_free; |
| 260 | } |
| 261 | |
| 262 | /* register led subsystem */ |
| 263 | drv_data->led_state = 0; |
Simon Wood | e25d780 | 2013-01-31 08:07:10 -0700 | [diff] [blame] | 264 | for (i = 0; i < SRWS1_NUMBER_LEDS + 1; i++) |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 265 | drv_data->led[i] = NULL; |
| 266 | |
| 267 | steelseries_srws1_set_leds(hdev, 0); |
| 268 | |
Simon Wood | e25d780 | 2013-01-31 08:07:10 -0700 | [diff] [blame] | 269 | name_sz = strlen(hdev->uniq) + 16; |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 270 | |
Simon Wood | e25d780 | 2013-01-31 08:07:10 -0700 | [diff] [blame] | 271 | /* 'ALL', for setting all LEDs simultaneously */ |
| 272 | led = kzalloc(sizeof(struct led_classdev)+name_sz, GFP_KERNEL); |
| 273 | if (!led) { |
| 274 | hid_err(hdev, "can't allocate memory for LED ALL\n"); |
| 275 | goto err_led; |
| 276 | } |
| 277 | |
| 278 | name = (void *)(&led[1]); |
| 279 | snprintf(name, name_sz, "SRWS1::%s::RPMALL", hdev->uniq); |
| 280 | led->name = name; |
| 281 | led->brightness = 0; |
| 282 | led->max_brightness = 1; |
| 283 | led->brightness_get = steelseries_srws1_led_all_get_brightness; |
| 284 | led->brightness_set = steelseries_srws1_led_all_set_brightness; |
| 285 | |
| 286 | drv_data->led[SRWS1_NUMBER_LEDS] = led; |
| 287 | ret = led_classdev_register(&hdev->dev, led); |
| 288 | if (ret) |
| 289 | goto err_led; |
| 290 | |
| 291 | /* Each individual LED */ |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 292 | for (i = 0; i < SRWS1_NUMBER_LEDS; i++) { |
| 293 | led = kzalloc(sizeof(struct led_classdev)+name_sz, GFP_KERNEL); |
| 294 | if (!led) { |
| 295 | hid_err(hdev, "can't allocate memory for LED %d\n", i); |
| 296 | goto err_led; |
| 297 | } |
| 298 | |
| 299 | name = (void *)(&led[1]); |
| 300 | snprintf(name, name_sz, "SRWS1::%s::RPM%d", hdev->uniq, i+1); |
| 301 | led->name = name; |
| 302 | led->brightness = 0; |
| 303 | led->max_brightness = 1; |
| 304 | led->brightness_get = steelseries_srws1_led_get_brightness; |
| 305 | led->brightness_set = steelseries_srws1_led_set_brightness; |
| 306 | |
| 307 | drv_data->led[i] = led; |
| 308 | ret = led_classdev_register(&hdev->dev, led); |
| 309 | |
| 310 | if (ret) { |
| 311 | hid_err(hdev, "failed to register LED %d. Aborting.\n", i); |
| 312 | err_led: |
| 313 | /* Deregister all LEDs (if any) */ |
Simon Wood | e25d780 | 2013-01-31 08:07:10 -0700 | [diff] [blame] | 314 | for (i = 0; i < SRWS1_NUMBER_LEDS + 1; i++) { |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 315 | led = drv_data->led[i]; |
| 316 | drv_data->led[i] = NULL; |
| 317 | if (!led) |
| 318 | continue; |
| 319 | led_classdev_unregister(led); |
| 320 | kfree(led); |
| 321 | } |
| 322 | goto out; /* but let the driver continue without LEDs */ |
| 323 | } |
| 324 | } |
| 325 | out: |
| 326 | return 0; |
| 327 | err_free: |
| 328 | kfree(drv_data); |
| 329 | return ret; |
| 330 | } |
| 331 | |
| 332 | static void steelseries_srws1_remove(struct hid_device *hdev) |
| 333 | { |
| 334 | int i; |
| 335 | struct led_classdev *led; |
| 336 | |
| 337 | struct steelseries_srws1_data *drv_data = hid_get_drvdata(hdev); |
| 338 | |
| 339 | if (drv_data) { |
| 340 | /* Deregister LEDs (if any) */ |
Simon Wood | e25d780 | 2013-01-31 08:07:10 -0700 | [diff] [blame] | 341 | for (i = 0; i < SRWS1_NUMBER_LEDS + 1; i++) { |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 342 | led = drv_data->led[i]; |
| 343 | drv_data->led[i] = NULL; |
| 344 | if (!led) |
| 345 | continue; |
| 346 | led_classdev_unregister(led); |
| 347 | kfree(led); |
| 348 | } |
| 349 | |
| 350 | } |
| 351 | |
| 352 | hid_hw_stop(hdev); |
| 353 | kfree(drv_data); |
| 354 | return; |
| 355 | } |
| 356 | #endif |
| 357 | |
Simon Wood | 75dbb95 | 2013-01-31 08:07:07 -0700 | [diff] [blame] | 358 | static __u8 *steelseries_srws1_report_fixup(struct hid_device *hdev, __u8 *rdesc, |
| 359 | unsigned int *rsize) |
| 360 | { |
| 361 | if (*rsize >= 115 && rdesc[11] == 0x02 && rdesc[13] == 0xc8 |
| 362 | && rdesc[29] == 0xbb && rdesc[40] == 0xc5) { |
| 363 | hid_info(hdev, "Fixing up Steelseries SRW-S1 report descriptor\n"); |
Simon Wood | 5492606 | 2013-01-31 08:07:08 -0700 | [diff] [blame] | 364 | rdesc = steelseries_srws1_rdesc_fixed; |
| 365 | *rsize = sizeof(steelseries_srws1_rdesc_fixed); |
Simon Wood | 75dbb95 | 2013-01-31 08:07:07 -0700 | [diff] [blame] | 366 | } |
| 367 | return rdesc; |
| 368 | } |
| 369 | |
| 370 | static const struct hid_device_id steelseries_srws1_devices[] = { |
| 371 | { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1) }, |
| 372 | { } |
| 373 | }; |
| 374 | MODULE_DEVICE_TABLE(hid, steelseries_srws1_devices); |
| 375 | |
| 376 | static struct hid_driver steelseries_srws1_driver = { |
| 377 | .name = "steelseries_srws1", |
| 378 | .id_table = steelseries_srws1_devices, |
Simon Wood | b52b506 | 2013-05-02 19:43:14 -0600 | [diff] [blame] | 379 | #if IS_BUILTIN(CONFIG_LEDS_CLASS) || \ |
| 380 | (IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES)) |
Simon Wood | 2e2daff | 2013-01-31 08:07:09 -0700 | [diff] [blame] | 381 | .probe = steelseries_srws1_probe, |
| 382 | .remove = steelseries_srws1_remove, |
| 383 | #endif |
Simon Wood | 75dbb95 | 2013-01-31 08:07:07 -0700 | [diff] [blame] | 384 | .report_fixup = steelseries_srws1_report_fixup |
| 385 | }; |
| 386 | |
Wei Yongjun | bb64469 | 2013-03-15 23:07:02 +0800 | [diff] [blame] | 387 | module_hid_driver(steelseries_srws1_driver); |
Simon Wood | 75dbb95 | 2013-01-31 08:07:07 -0700 | [diff] [blame] | 388 | MODULE_LICENSE("GPL"); |