HID: cougar: Add support for the Cougar 500k Gaming Keyboard
authorDaniel M. Lambea <dmlambea@gmail.com>
Tue, 17 Jul 2018 21:35:37 +0000 (22:35 +0100)
committerJiri Kosina <jkosina@suse.cz>
Mon, 23 Jul 2018 09:35:05 +0000 (11:35 +0200)
Cougar 500k Gaming Keyboard have some special function keys that
make the keyboard stop responding once 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@gmail.com>
Reviewed-by: Benjamin Tissoires <benjamin.tissoires@redhat.com>
Signed-off-by: Jiri Kosina <jkosina@suse.cz>
drivers/hid/Kconfig
drivers/hid/Makefile
drivers/hid/hid-cougar.c [new file with mode: 0644]
drivers/hid/hid-ids.h

index a49a104..61e1953 100644 (file)
@@ -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
index 511e1cb..bd7ac53 100644 (file)
@@ -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
diff --git a/drivers/hid/hid-cougar.c b/drivers/hid/hid-cougar.c
new file mode 100644 (file)
index 0000000..ad2e87d
--- /dev/null
@@ -0,0 +1,312 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ *  HID driver for Cougar 500k Gaming Keyboard
+ *
+ *  Copyright (c) 2018 Daniel M. Lambea <dmlambea@gmail.com>
+ */
+
+#include <linux/hid.h>
+#include <linux/module.h>
+
+#include "hid-ids.h"
+
+MODULE_AUTHOR("Daniel M. Lambea <dmlambea@gmail.com>");
+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_VENDOR_USAGE    0xff00ff00
+
+#define COUGAR_FIELD_CODE      1
+#define COUGAR_FIELD_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. The special key COUGAR_KEY_G6 is defined first
+ * because it is more frequent to use the spacebar rather than any other
+ * special keys. Depending on the value of the parameter 'g6_is_space',
+ * the mapping will be updated in the probe function.
+ */
+static unsigned char cougar_mapping[][2] = {
+       { COUGAR_KEY_G6,   KEY_SPACE },
+       { 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_LOCK, KEY_SCREENLOCK },
+/* The following keys are handled by the hardware itself, so no special
+ * treatment is required:
+       { COUGAR_KEY_FN, KEY_RESERVED },
+       { COUGAR_KEY_MR, KEY_RESERVED },
+       { COUGAR_KEY_M1, KEY_RESERVED },
+       { COUGAR_KEY_M2, KEY_RESERVED },
+       { COUGAR_KEY_M3, KEY_RESERVED },
+       { COUGAR_KEY_LEDS, KEY_RESERVED },
+*/
+       { 0, 0 },
+};
+
+struct cougar_shared {
+       struct list_head list;
+       struct kref kref;
+       bool enabled;
+       struct hid_device *dev;
+       struct input_dev *input;
+};
+
+struct cougar {
+       bool special_intf;
+       struct cougar_shared *shared;
+};
+
+static LIST_HEAD(cougar_udev_list);
+static DEFINE_MUTEX(cougar_udev_list_lock);
+
+static void cougar_fix_g6_mapping(struct hid_device *hdev)
+{
+       int i;
+
+       for (i = 0; cougar_mapping[i][0]; i++) {
+               if (cougar_mapping[i][0] == COUGAR_KEY_G6) {
+                       cougar_mapping[i][1] =
+                               cougar_g6_is_space ? KEY_SPACE : KEY_F18;
+                       hid_info(hdev, "G6 mapped to %s\n",
+                                cougar_g6_is_space ? "space" : "F18");
+                       return;
+               }
+       }
+       hid_warn(hdev, "no mapping defined for G6/spacebar");
+}
+
+/*
+ * Constant-friendly rdesc fixup for mouse interface
+ */
+static __u8 *cougar_report_fixup(struct hid_device *hdev, __u8 *rdesc,
+                                unsigned int *rsize)
+{
+       if (rdesc[2] == 0x09 && rdesc[3] == 0x02 &&
+           (rdesc[115] | rdesc[116] << 8) >= HID_MAX_USAGES) {
+               hid_info(hdev,
+                       "usage count exceeds max: fixing up report descriptor\n");
+               rdesc[115] = ((HID_MAX_USAGES-1) & 0xff);
+               rdesc[116] = ((HID_MAX_USAGES-1) >> 8);
+       }
+       return rdesc;
+}
+
+static struct cougar_shared *cougar_get_shared_data(struct hid_device *hdev)
+{
+       struct cougar_shared *shared;
+
+       /* Try to find an already-probed interface from the same device */
+       list_for_each_entry(shared, &cougar_udev_list, list) {
+               if (hid_compare_device_paths(hdev, shared->dev, '/')) {
+                       kref_get(&shared->kref);
+                       return shared;
+               }
+       }
+       return NULL;
+}
+
+static void cougar_release_shared_data(struct kref *kref)
+{
+       struct cougar_shared *shared = container_of(kref,
+                                                   struct cougar_shared, kref);
+
+       mutex_lock(&cougar_udev_list_lock);
+       list_del(&shared->list);
+       mutex_unlock(&cougar_udev_list_lock);
+
+       kfree(shared);
+}
+
+static void cougar_remove_shared_data(void *resource)
+{
+       struct cougar *cougar = resource;
+
+       if (cougar->shared) {
+               kref_put(&cougar->shared->kref, cougar_release_shared_data);
+               cougar->shared = NULL;
+       }
+}
+
+/*
+ * Bind the device group's shared data to this cougar struct.
+ * If no shared data exists for this group, create and initialize it.
+ */
+static int cougar_bind_shared_data(struct hid_device *hdev, struct cougar *cougar)
+{
+       struct cougar_shared *shared;
+       int error = 0;
+
+       mutex_lock(&cougar_udev_list_lock);
+
+       shared = cougar_get_shared_data(hdev);
+       if (!shared) {
+               shared = kzalloc(sizeof(*shared), GFP_KERNEL);
+               if (!shared) {
+                       error = -ENOMEM;
+                       goto out;
+               }
+
+               kref_init(&shared->kref);
+               shared->dev = hdev;
+               list_add_tail(&shared->list, &cougar_udev_list);
+       }
+
+       cougar->shared = shared;
+
+       error = devm_add_action(&hdev->dev, cougar_remove_shared_data, cougar);
+       if (error) {
+               mutex_unlock(&cougar_udev_list_lock);
+               cougar_remove_shared_data(cougar);
+               return error;
+       }
+
+out:
+       mutex_unlock(&cougar_udev_list_lock);
+       return error;
+}
+
+static int cougar_probe(struct hid_device *hdev,
+                       const struct hid_device_id *id)
+{
+       struct cougar *cougar;
+       struct hid_input *next, *hidinput = NULL;
+       unsigned int connect_mask;
+       int error;
+
+       cougar = devm_kzalloc(&hdev->dev, sizeof(*cougar), GFP_KERNEL);
+       if (!cougar)
+               return -ENOMEM;
+       hid_set_drvdata(hdev, cougar);
+
+       error = hid_parse(hdev);
+       if (error) {
+               hid_err(hdev, "parse failed\n");
+               goto fail;
+       }
+
+       if (hdev->collection->usage == COUGAR_VENDOR_USAGE) {
+               cougar->special_intf = true;
+               connect_mask = HID_CONNECT_HIDRAW;
+       } else
+               connect_mask = HID_CONNECT_DEFAULT;
+
+       error = hid_hw_start(hdev, connect_mask);
+       if (error) {
+               hid_err(hdev, "hw start failed\n");
+               goto fail;
+       }
+
+       error = cougar_bind_shared_data(hdev, cougar);
+       if (error)
+               goto fail_stop_and_cleanup;
+
+       /* The custom vendor interface will use the hid_input registered
+        * for the keyboard interface, in order to send translated key codes
+        * to it.
+        */
+       if (hdev->collection->usage == HID_GD_KEYBOARD) {
+               cougar_fix_g6_mapping(hdev);
+               list_for_each_entry_safe(hidinput, next, &hdev->inputs, list) {
+                       if (hidinput->registered && hidinput->input != NULL) {
+                               cougar->shared->input = hidinput->input;
+                               cougar->shared->enabled = true;
+                               break;
+                       }
+               }
+       } else if (hdev->collection->usage == COUGAR_VENDOR_USAGE) {
+               error = hid_hw_open(hdev);
+               if (error)
+                       goto fail_stop_and_cleanup;
+       }
+       return 0;
+
+fail_stop_and_cleanup:
+       hid_hw_stop(hdev);
+fail:
+       hid_set_drvdata(hdev, NULL);
+       return error;
+}
+
+/*
+ * Convert 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 cougar *cougar;
+       unsigned char code, action;
+       int i;
+
+       cougar = hid_get_drvdata(hdev);
+       if (!cougar->special_intf || !cougar->shared ||
+           !cougar->shared->input || !cougar->shared->enabled)
+               return 0;
+
+       code = data[COUGAR_FIELD_CODE];
+       action = data[COUGAR_FIELD_ACTION];
+       for (i = 0; cougar_mapping[i][0]; i++) {
+               if (code == cougar_mapping[i][0]) {
+                       input_event(cougar->shared->input, EV_KEY,
+                                   cougar_mapping[i][1], action);
+                       input_sync(cougar->shared->input);
+                       return 0;
+               }
+       }
+       hid_warn(hdev, "unmapped special key code %x: ignoring\n", code);
+       return 0;
+}
+
+static void cougar_remove(struct hid_device *hdev)
+{
+       struct cougar *cougar = hid_get_drvdata(hdev);
+
+       if (cougar) {
+               /* Stop the vendor intf to process more events */
+               if (cougar->shared)
+                       cougar->shared->enabled = false;
+               if (cougar->special_intf)
+                       hid_hw_close(hdev);
+       }
+       hid_hw_stop(hdev);
+}
+
+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,
+       .remove                 = cougar_remove,
+       .raw_event              = cougar_raw_event,
+};
+
+module_hid_driver(cougar_driver);
index c7981dd..a1ee8f6 100644 (file)
 #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