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