Cougar 500k Gaming Keyboard have some special function keys that make the keyboard stop responding when pressed. Implement the custom vendor interface that deals with the extended keypresses to fix. The bug can be reproduced by plugging in the keyboard, then pressing the rightmost part of the spacebar. Signed-off-by: Daniel M. Lambea <dmlambea@xxxxxxxxx> --- One of those special function keys is just the right-half part of the spacebar, so the chance of hitting it is very high. This is very annoying to the user, since the only way of recovering the device back is to unplug it and to plug it back. The code, built as a DKMS module, has been released and tested by others. For more information, please refer to the bug: https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1511511 diff -uprN -X hid-vanilla/Documentation/dontdiff hid-vanilla/drivers/hid/hid-cougar.c hid/drivers/hid/hid-cougar.c --- hid-vanilla/drivers/hid/hid-cougar.c 1970-01-01 00:00:00.000000000 +0000 +++ hid/drivers/hid/hid-cougar.c 2018-07-09 18:42:42.187292906 +0100 @@ -0,0 +1,313 @@ +/* + * HID driver for Cougar 500k Gaming Keyboard + * + * Copyright (c) 2018 Daniel M. Lambea <dmlambea@xxxxxxxxx> + */ + +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + */ + +#include <linux/hid.h> +#include <linux/module.h> +#include <linux/usb.h> + +#include "hid-ids.h" + +MODULE_AUTHOR("Daniel M. Lambea <dmlambea@xxxxxxxxx>"); +MODULE_DESCRIPTION("Cougar 500k Gaming Keyboard"); +MODULE_LICENSE("GPL"); +MODULE_INFO(key_mappings, "G1-G6 are mapped to F13-F18"); + +static int cougar_g6_is_space = 1; +module_param_named(g6_is_space, cougar_g6_is_space, int, 0600); +MODULE_PARM_DESC(g6_is_space, + "If set, G6 programmable key sends SPACE instead of F18 (0=off, 1=on) (default=1)"); + + +#define COUGAR_KEYB_INTFNO 0 +#define COUGAR_MOUSE_INTFNO 1 +#define COUGAR_RESERVED_INTFNO 2 + +#define COUGAR_RESERVED_FLD_CODE 1 +#define COUGAR_RESERVED_FLD_ACTION 2 + +#define COUGAR_KEY_G1 0x83 +#define COUGAR_KEY_G2 0x84 +#define COUGAR_KEY_G3 0x85 +#define COUGAR_KEY_G4 0x86 +#define COUGAR_KEY_G5 0x87 +#define COUGAR_KEY_G6 0x78 +#define COUGAR_KEY_FN 0x0d +#define COUGAR_KEY_MR 0x6f +#define COUGAR_KEY_M1 0x80 +#define COUGAR_KEY_M2 0x81 +#define COUGAR_KEY_M3 0x82 +#define COUGAR_KEY_LEDS 0x67 +#define COUGAR_KEY_LOCK 0x6e + + +/* Default key mappings */ +static unsigned char cougar_mapping[][2] = { + { COUGAR_KEY_G1, KEY_F13 }, + { COUGAR_KEY_G2, KEY_F14 }, + { COUGAR_KEY_G3, KEY_F15 }, + { COUGAR_KEY_G4, KEY_F16 }, + { COUGAR_KEY_G5, KEY_F17 }, + { COUGAR_KEY_G6, KEY_F18 }, // This is handled individually + { COUGAR_KEY_LOCK, KEY_SCREENLOCK }, + { 0, 0 }, +}; + +struct cougar_kbd_data { + struct hid_device *owner; + struct input_dev *input; +}; + +/* + * Constant-friendly rdesc fixup for mouse interface + */ +static __u8 *cougar_report_fixup(struct hid_device *hdev, __u8 *rdesc, + unsigned int *rsize) +{ + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + + if (intf->cur_altsetting->desc.bInterfaceNumber == COUGAR_MOUSE_INTFNO && + (rdesc[0x73] | rdesc[0x74] << 8) >= HID_MAX_USAGES) { + hid_info(hdev, "usage count exceeds max: fixing up report descriptor"); + rdesc[0x73] = ((HID_MAX_USAGES-1) & 0xff); + rdesc[0x74] = ((HID_MAX_USAGES-1) >> 8); + } + return rdesc; +} + +/* + * Returns a sibling hid_device with the given intf number + */ +static struct hid_device *cougar_get_sibling_hid_device(struct hid_device *hdev, + const __u8 intfno) +{ + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct usb_device *device; + struct usb_host_config *hostcfg; + struct hid_device *siblingHdev; + int i; + + if (intf->cur_altsetting->desc.bInterfaceNumber == intfno) + hid_err(hdev, "returning hid device's data from myself?"); + + device = interface_to_usbdev(intf); + hostcfg = device->config; + siblingHdev = NULL; + for (i = 0; i < hostcfg->desc.bNumInterfaces; i++) { + if (i == intfno) + return usb_get_intfdata(hostcfg->interface[i]); + } + return NULL; +} + +static int cougar_set_drvdata_from_keyb_interface(struct hid_device *hdev) +{ + struct hid_device *sibling; + struct cougar_kbd_data *kbd; + + /* Search for the keyboard */ + sibling = cougar_get_sibling_hid_device(hdev, COUGAR_KEYB_INTFNO); + if (sibling == NULL) { + hid_err(hdev, + "no sibling hid device found for intf %d", COUGAR_KEYB_INTFNO); + return -ENODEV; + } + + kbd = hid_get_drvdata(sibling); + if (kbd == NULL || kbd->input == NULL) { + hid_err(hdev, + "keyboard descriptor not found in intf %d", COUGAR_KEYB_INTFNO); + return -ENODATA; + } + + /* And save its data on reserved, custom vendor intf. device */ + hid_set_drvdata(hdev, kbd); + return 0; +} + +/* + * The probe will save the keyb's input device, so that the + * vendor intf will be able to send keys as if it were the + * keyboard itself. + */ +static int cougar_probe(struct hid_device *hdev, + const struct hid_device_id *id) +{ + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct cougar_kbd_data *kbd = NULL; + unsigned int mask; + int ret; + __u8 intfno; + + intfno = intf->cur_altsetting->desc.bInterfaceNumber; + hid_dbg(hdev, "about to probe intf %d", intfno); + + if (intfno == COUGAR_KEYB_INTFNO) { + kbd = devm_kzalloc(&hdev->dev, sizeof(*kbd), GFP_KERNEL); + if (kbd == NULL) + return -ENOMEM; + + hid_dbg(hdev, "attaching kbd data to intf %d", intfno); + hid_set_drvdata(hdev, kbd); + } + + /* Parse and start hw */ + ret = hid_parse(hdev); + if (ret) + goto err_cleanup; + + /* Reserved, custom vendor interface connects hidraw only */ + mask = intfno == COUGAR_RESERVED_INTFNO ? + HID_CONNECT_HIDRAW : HID_CONNECT_DEFAULT; + ret = hid_hw_start(hdev, mask); + if (ret) + goto err_cleanup; + + if (intfno == COUGAR_RESERVED_INTFNO) { + ret = cougar_set_drvdata_from_keyb_interface(hdev); + if (ret) + goto err_stop_and_cleanup; + + hid_dbg(hdev, "keyboard descriptor attached to intf %d", intfno); + + ret = hid_hw_open(hdev); + if (ret) + goto err_stop_and_cleanup; + } + hid_dbg(hdev, "intf %d probed successfully", intfno); + + return 0; + +err_stop_and_cleanup: + hid_hw_stop(hdev); +err_cleanup: + hid_set_drvdata(hdev, NULL); + if (kbd != NULL && kbd->owner == hdev) + devm_kfree(&hdev->dev, kbd); + return ret; +} + +/* + * Keeps track of the keyboard's hid_input + */ +static int cougar_input_configured(struct hid_device *hdev, struct hid_input *hidinput) +{ + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + __u8 intfno = intf->cur_altsetting->desc.bInterfaceNumber; + struct cougar_kbd_data *kbd; + + if (intfno != COUGAR_KEYB_INTFNO) { + /* Skip interfaces other than COUGAR_KEYB_INTFNO, + * which is the one containing the configured hidinput + */ + hid_dbg(hdev, "input_configured: skipping intf %d", intfno); + return 0; + } + kbd = hid_get_drvdata(hdev); + kbd->owner = hdev; + kbd->input = hidinput->input; + hid_dbg(hdev, "input_configured: intf %d configured", intfno); + return 0; +} + +/* + * Converts events from vendor intf to input key events + */ +static int cougar_raw_event(struct hid_device *hdev, struct hid_report *report, + u8 *data, int size) +{ + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct cougar_kbd_data *kbd; + unsigned char action, code, transcode; + int i; + + if (intf->cur_altsetting->desc.bInterfaceNumber != COUGAR_RESERVED_INTFNO) + return 0; + + // Enable this to see a dump of the data received from reserved interface + //print_hex_dump(KERN_ERR, DRIVER_NAME " data : ", DUMP_PREFIX_OFFSET, 8, 1, data, size, 0); + + code = data[COUGAR_RESERVED_FLD_CODE]; + switch (code) { + case COUGAR_KEY_FN: + hid_dbg(hdev, "FN (special function) key is handled by the hardware itself"); + break; + case COUGAR_KEY_MR: + hid_dbg(hdev, "MR (macro recording) key is handled by the hardware itself"); + break; + case COUGAR_KEY_M1: + hid_dbg(hdev, "M1 (macro set 1) key is handled by the hardware itself"); + break; + case COUGAR_KEY_M2: + hid_dbg(hdev, "M2 (macro set 2) key is handled by the hardware itself"); + break; + case COUGAR_KEY_M3: + hid_dbg(hdev, "M3 (macro set 3) key is handled by the hardware itself"); + break; + case COUGAR_KEY_LEDS: + hid_dbg(hdev, "LED (led set) key is handled by the hardware itself"); + break; + default: + /* Try normal key mappings */ + for (i = 0; cougar_mapping[i][0]; i++) { + if (cougar_mapping[i][0] == code) { + action = data[COUGAR_RESERVED_FLD_ACTION]; + hid_dbg(hdev, "found mapping for code %x", code); + if (code == COUGAR_KEY_G6 && cougar_g6_is_space) + transcode = KEY_SPACE; + else + transcode = cougar_mapping[i][1]; + + kbd = hid_get_drvdata(hdev); + input_event(kbd->input, EV_KEY, transcode, action); + input_sync(kbd->input); + hid_dbg(hdev, "translated to %x", transcode); + return 0; + } + } + hid_warn(hdev, "unmapped key code %d: ignoring", code); + } + return 0; +} + +static void cougar_remove(struct hid_device *hdev) +{ + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct cougar_kbd_data *kbd = hid_get_drvdata(hdev); + + hid_dbg(hdev, "removing %d", intf->cur_altsetting->desc.bInterfaceNumber); + if (intf->cur_altsetting->desc.bInterfaceNumber == COUGAR_RESERVED_INTFNO) + hid_hw_close(hdev); + + hid_hw_stop(hdev); + hid_set_drvdata(hdev, NULL); + if (kbd != NULL && kbd->owner == hdev) + devm_kfree(&hdev->dev, kbd); +} + +static struct hid_device_id cougar_id_table[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_SOLID_YEAR, USB_DEVICE_ID_COUGAR_500K_GAMING_KEYBOARD) }, + {} +}; +MODULE_DEVICE_TABLE(hid, cougar_id_table); + +static struct hid_driver cougar_driver = { + .name = "cougar", + .id_table = cougar_id_table, + .report_fixup = cougar_report_fixup, + .probe = cougar_probe, + .input_configured = cougar_input_configured, + .remove = cougar_remove, + .raw_event = cougar_raw_event, +}; + +module_hid_driver(cougar_driver); diff -uprN -X hid-vanilla/Documentation/dontdiff hid-vanilla/drivers/hid/hid-ids.h hid/drivers/hid/hid-ids.h --- hid-vanilla/drivers/hid/hid-ids.h 2018-07-09 17:48:30.189316299 +0100 +++ hid/drivers/hid/hid-ids.h 2018-07-09 17:54:29.332832182 +0100 @@ -1001,6 +1001,9 @@ #define USB_VENDOR_ID_SINO_LITE 0x1345 #define USB_DEVICE_ID_SINO_LITE_CONTROLLER 0x3008 +#define USB_VENDOR_ID_SOLID_YEAR 0x060b +#define USB_DEVICE_ID_COUGAR_500K_GAMING_KEYBOARD 0x500a + #define USB_VENDOR_ID_SOUNDGRAPH 0x15c2 #define USB_DEVICE_ID_SOUNDGRAPH_IMON_FIRST 0x0034 #define USB_DEVICE_ID_SOUNDGRAPH_IMON_LAST 0x0046 diff -uprN -X hid-vanilla/Documentation/dontdiff hid-vanilla/drivers/hid/hid-quirks.c hid/drivers/hid/hid-quirks.c --- hid-vanilla/drivers/hid/hid-quirks.c 2018-07-09 17:48:30.193316294 +0100 +++ hid/drivers/hid/hid-quirks.c 2018-07-09 17:54:42.708814231 +0100 @@ -610,6 +610,9 @@ static const struct hid_device_id hid_ha { HID_USB_DEVICE(USB_VENDOR_ID_WISEGROUP_LTD, USB_DEVICE_ID_SUPER_DUAL_BOX_PRO) }, { HID_USB_DEVICE(USB_VENDOR_ID_WISEGROUP_LTD, USB_DEVICE_ID_SUPER_JOY_BOX_5_PRO) }, #endif +#if IS_ENABLED(CONFIG_HID_COUGAR) + { HID_USB_DEVICE(USB_VENDOR_ID_SOLID_YEAR, USB_DEVICE_ID_COUGAR_500K_GAMING_KEYBOARD) }, +#endif #if IS_ENABLED(CONFIG_HID_SONY) { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_LOGITECH_HARMONY_PS3) }, { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_SMK, USB_DEVICE_ID_SMK_PS3_BDREMOTE) }, diff -uprN -X hid-vanilla/Documentation/dontdiff hid-vanilla/drivers/hid/Kconfig hid/drivers/hid/Kconfig --- hid-vanilla/drivers/hid/Kconfig 2018-07-09 17:48:30.185316305 +0100 +++ hid/drivers/hid/Kconfig 2018-07-09 18:46:10.075657150 +0100 @@ -207,6 +207,16 @@ config HID_CORSAIR - Vengeance K90 - Scimitar PRO RGB +config HID_COUGAR + tristate "Cougar devices" + depends on HID + help + Support for Cougar devices that are not fully compliant with the + HID standard. + + Supported devices: + - Cougar 500k Gaming Keyboard + config HID_PRODIKEYS tristate "Prodikeys PC-MIDI Keyboard support" depends on HID && SND diff -uprN -X hid-vanilla/Documentation/dontdiff hid-vanilla/drivers/hid/Makefile hid/drivers/hid/Makefile --- hid-vanilla/drivers/hid/Makefile 2018-07-09 17:48:30.185316305 +0100 +++ hid/drivers/hid/Makefile 2018-07-09 17:54:15.140851231 +0100 @@ -35,6 +35,7 @@ obj-$(CONFIG_HID_CHERRY) += hid-cherry.o obj-$(CONFIG_HID_CHICONY) += hid-chicony.o obj-$(CONFIG_HID_CMEDIA) += hid-cmedia.o obj-$(CONFIG_HID_CORSAIR) += hid-corsair.o +obj-$(CONFIG_HID_COUGAR) += hid-cougar.o obj-$(CONFIG_HID_CP2112) += hid-cp2112.o obj-$(CONFIG_HID_CYPRESS) += hid-cypress.o obj-$(CONFIG_HID_DRAGONRISE) += hid-dr.o -- To unsubscribe from this list: send the line "unsubscribe linux-input" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html