aboutsummaryrefslogtreecommitdiffstats
path: root/src/hid-lg-g710-plus.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/hid-lg-g710-plus.c')
-rw-r--r--src/hid-lg-g710-plus.c394
1 files changed, 394 insertions, 0 deletions
diff --git a/src/hid-lg-g710-plus.c b/src/hid-lg-g710-plus.c
new file mode 100644
index 0000000..abccd6c
--- /dev/null
+++ b/src/hid-lg-g710-plus.c
@@ -0,0 +1,394 @@
+/*
+ * Logitech G710+ Keyboard Input Driver
+ *
+ * Driver generates additional key events for the keys M1-MR, G1-G6
+ * and supports setting the backlight levels of the keyboard
+ *
+ * Copyright (c) 2013 Filip Wieladke <Wattos@gmail.com>
+ */
+
+/*
+ * 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.
+ */
+
+
+/*
+ * This version of the G710 driver utilizes the M keys to alter how the G keys
+ * function, resulting in a total of 24 'inputs'. These are mapped to Fn keys,
+ * from F1 to F24.
+ */
+
+
+#include <linux/hid.h>
+#include <linux/input.h>
+#include <linux/device.h>
+#include <linux/module.h>
+#include <linux/usb.h>
+#include <linux/version.h>
+
+#include "hid-ids.h"
+#include "usbhid/usbhid.h"
+
+#define USB_DEVICE_ID_LOGITECH_KEYBOARD_G710_PLUS 0xc24d
+
+// 20 seconds timeout
+#define WAIT_TIME_OUT 20000
+
+#define LOGITECH_KEY_MAP_SIZE 16
+
+static const u8 g710_plus_key_map[LOGITECH_KEY_MAP_SIZE] = {
+ 0, /* unused */
+ 0, /* unused */
+ 0, /* unused */
+ 0, /* unused */
+
+ // These values are converted to Fn keys in extra_key_event()
+
+ 1, /* M1 */
+ 2, /* M2 */
+ 3, /* M3 */
+ 4, /* MR */
+ 5, /* G1 */
+ 6, /* G2 */
+ 7, /* G3 */
+ 8, /* G4 */
+ 9, /* G5 */
+ 10, /* G6 */
+
+ 0, /* unused */
+ 0, /* unused */
+};
+
+// Assume these values aren't in a straight line
+static const u8 f_button_map[24] = {
+ KEY_F1, KEY_F2, KEY_F3, KEY_F4,
+ KEY_F5, KEY_F6, KEY_F7, KEY_F8,
+ KEY_F9, KEY_F10, KEY_F11, KEY_F12,
+ KEY_F13, KEY_F14, KEY_F15, KEY_F16,
+ KEY_F17, KEY_F18, KEY_F19, KEY_F20,
+ KEY_F21, KEY_F22, KEY_F23, KEY_F24,
+};
+
+/* Convenience macros */
+#define lg_g710_plus_get_data(hdev) \
+ ((struct lg_g710_plus_data *)(hid_get_drvdata(hdev)))
+
+#define BIT_AT(var,pos) ((var) & (1<<(pos)))
+
+struct lg_g710_plus_data {
+ struct hid_report *g_mr_buttons_support_report; /* Needs to be written to enable G1-G6 and M1-MR keys */
+ struct hid_report *mr_buttons_led_report; /* Controls the backlight of M1-MR buttons */
+ struct hid_report *other_buttons_led_report; /* Controls the backlight of other buttons */
+ struct hid_report *gamemode_report; /* Controls the backlight of other buttons */
+
+ u16 macro_button_state; /* Holds the last state of the G1-G6, M1-MR buttons. Required to know which buttons were pressed and which were released */
+ struct hid_device *hdev;
+ struct input_dev *input_dev;
+ struct attribute_group attr_group;
+
+ u8 led_macro; /* state of the M1-MR macro leds as returned by the keyboard ==> binary coded 0 -> 0xF*/
+ u8 led_keys; /* state of the WASD key leds as returned by the keyboard ==> 0 -> 4 */
+
+ spinlock_t lock; /* lock for communication with user space */
+ struct completion ready; /* ready indicator */
+};
+
+static ssize_t lg_g710_plus_show_led_macro(struct device *device, struct device_attribute *attr, char *buf);
+static ssize_t lg_g710_plus_store_led_macro(struct device *device, struct device_attribute *attr, const char *buf, size_t count);
+static ssize_t lg_g710_plus_show_led_keys(struct device *device, struct device_attribute *attr, char *buf);
+static ssize_t lg_g710_plus_store_led_keys(struct device *device, struct device_attribute *attr, const char *buf, size_t count);
+
+static DEVICE_ATTR(led_macro, 0660, lg_g710_plus_show_led_macro, lg_g710_plus_store_led_macro);
+static DEVICE_ATTR(led_keys, 0660, lg_g710_plus_show_led_keys, lg_g710_plus_store_led_keys);
+
+static struct attribute *lg_g710_plus_attrs[] = {
+ &dev_attr_led_macro.attr,
+ &dev_attr_led_keys.attr,
+ NULL,
+};
+
+static unsigned int lg_g710_plus_m_state = 0;
+
+static int lg_g710_plus_extra_key_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) {
+ u8 i;
+ u16 keys_pressed;
+ char macro_buf[2] = {'0','\0'};
+ struct lg_g710_plus_data* g710_data = lg_g710_plus_get_data(hdev);
+ if (g710_data == NULL || size < 3 || data[0] != 3) {
+ return 1; /* cannot handle the event */
+ }
+
+ keys_pressed = data[1] << 8 | data[2];
+ for (i = 0; i < LOGITECH_KEY_MAP_SIZE; i++) {
+ if (g710_plus_key_map[i] != 0 && (BIT_AT(keys_pressed, i) != BIT_AT(g710_data->macro_button_state, i))) {
+ if (g710_plus_key_map[i] < 5) { /* handle M keys */
+ if (BIT_AT(keys_pressed, i)) { /* only handle key presses */
+ lg_g710_plus_m_state = g710_plus_key_map[i] - 1;
+ if (lg_g710_plus_m_state == 3) { /* have MR act like M1, but clear leds */
+ lg_g710_plus_m_state = 0;
+ macro_buf[0] = '0';
+ } else
+ macro_buf[0] = (1 << lg_g710_plus_m_state) + '0';
+ lg_g710_plus_store_led_macro(&hdev->dev,(struct device_attribute *)NULL,macro_buf,2);
+ }
+ } else
+ input_report_key(g710_data->input_dev, f_button_map[ (g710_plus_key_map[i] - 5) + 6 * lg_g710_plus_m_state ], BIT_AT(keys_pressed, i) != 0);
+ }
+ }
+ input_sync(g710_data->input_dev);
+ g710_data->macro_button_state= keys_pressed;
+ return 1;
+}
+
+static int lg_g710_plus_extra_led_mr_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) {
+ struct lg_g710_plus_data* g710_data = lg_g710_plus_get_data(hdev);
+ g710_data->led_macro= (data[1] >> 4) & 0xF;
+ complete_all(&g710_data->ready);
+ return 1;
+}
+
+static int lg_g710_plus_extra_led_keys_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size) {
+ struct lg_g710_plus_data* g710_data = lg_g710_plus_get_data(hdev);
+ g710_data->led_keys= data[1] << 4 | data[2];
+ complete_all(&g710_data->ready);
+ return 1;
+}
+
+static int lg_g710_plus_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size)
+{
+ switch(report->id) {
+ case 3: return lg_g710_plus_extra_key_event(hdev, report, data, size);
+ case 6: return lg_g710_plus_extra_led_mr_event(hdev, report, data, size);
+ case 8: return lg_g710_plus_extra_led_keys_event(hdev, report, data, size);
+ default: return 0;
+ }
+}
+
+static int lg_g710_plus_input_mapping(struct hid_device *hdev, struct hid_input *hi, struct hid_field *field, struct hid_usage *usage, unsigned long **bit, int *max)
+{
+ struct lg_g710_plus_data* data = lg_g710_plus_get_data(hdev);
+ if (data != NULL && data->input_dev == NULL) {
+ data->input_dev= hi->input;
+ }
+ return 0;
+}
+
+enum req_type {
+ REQTYPE_READ,
+ REQTYPE_WRITE
+};
+
+static void hidhw_request(struct hid_device *hdev, struct hid_report *report, enum req_type reqtype) {
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,10,0)
+ hid_hw_request(hdev, report, reqtype == REQTYPE_READ ? HID_REQ_GET_REPORT : HID_REQ_SET_REPORT);
+#else
+ usbhid_submit_report(hdev, report, reqtype == REQTYPE_READ ? USB_DIR_IN : USB_DIR_OUT);
+#endif
+}
+
+static int lg_g710_plus_initialize(struct hid_device *hdev) {
+ int ret = 0;
+ struct lg_g710_plus_data *data;
+ struct list_head *feature_report_list = &hdev->report_enum[HID_FEATURE_REPORT].report_list;
+ struct hid_report *report;
+
+ if (list_empty(feature_report_list)) {
+ return 0; /* Currently, the keyboard registers as two different devices */
+ }
+
+ data = lg_g710_plus_get_data(hdev);
+ list_for_each_entry(report, feature_report_list, list) {
+ switch(report->id) {
+ case 6: data->mr_buttons_led_report= report; break;
+ case 8: data->other_buttons_led_report= report; break;
+ case 9:
+ data->g_mr_buttons_support_report= report;
+ hidhw_request(hdev, report, REQTYPE_WRITE);
+ break;
+ }
+ }
+
+ ret= sysfs_create_group(&hdev->dev.kobj, &data->attr_group);
+ return ret;
+}
+
+static struct lg_g710_plus_data* lg_g710_plus_create(struct hid_device *hdev)
+{
+ struct lg_g710_plus_data* data;
+ data= kzalloc(sizeof(struct lg_g710_plus_data), GFP_KERNEL);
+ if (data == NULL) {
+ return NULL;
+ }
+
+ data->attr_group.name= "logitech-g710";
+ data->attr_group.attrs= lg_g710_plus_attrs;
+ data->hdev= hdev;
+
+ spin_lock_init(&data->lock);
+ init_completion(&data->ready);
+ return data;
+}
+
+static int lg_g710_plus_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ int ret;
+ struct lg_g710_plus_data *data;
+
+ data = lg_g710_plus_create(hdev);
+ if (data == NULL) {
+ dev_err(&hdev->dev, "can't allocate space for Logitech G710+ device attributes\n");
+ ret= -ENOMEM;
+ goto err_free;
+ }
+ hid_set_drvdata(hdev, data);
+
+ /*
+ * Without this, the device would send a first report with a key down event for
+ * certain buttons, but never the key up event
+ */
+ hdev->quirks |= HID_QUIRK_NOGET;
+
+ ret = hid_parse(hdev);
+ if (ret) {
+ hid_err(hdev, "parse failed\n");
+ goto err_free;
+ }
+
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret) {
+ hid_err(hdev, "hw start failed\n");
+ goto err_free;
+ }
+
+ ret= lg_g710_plus_initialize(hdev);
+ if (ret) {
+ ret = -ret;
+ hid_hw_stop(hdev);
+ goto err_free;
+ }
+
+ return 0;
+
+err_free:
+ if (data != NULL) {
+ kfree(data);
+ }
+ return ret;
+}
+
+static void lg_g710_plus_remove(struct hid_device *hdev)
+{
+ struct lg_g710_plus_data* data = lg_g710_plus_get_data(hdev);
+ struct list_head *feature_report_list = &hdev->report_enum[HID_FEATURE_REPORT].report_list;
+
+ if (data != NULL && !list_empty(feature_report_list))
+ sysfs_remove_group(&hdev->dev.kobj, &data->attr_group);
+
+ hid_hw_stop(hdev);
+ if (data != NULL) {
+ kfree(data);
+ }
+}
+
+static ssize_t lg_g710_plus_show_led_macro(struct device *device, struct device_attribute *attr, char *buf)
+{
+ struct lg_g710_plus_data* data = hid_get_drvdata(dev_get_drvdata(device->parent));
+ if (data != NULL) {
+ spin_lock(&data->lock);
+ init_completion(&data->ready);
+ hidhw_request(data->hdev, data->mr_buttons_led_report, REQTYPE_READ);
+ wait_for_completion_timeout(&data->ready, WAIT_TIME_OUT);
+ spin_unlock(&data->lock);
+ return sprintf(buf, "%d\n", data->led_macro);
+ }
+ return 0;
+}
+
+static ssize_t lg_g710_plus_show_led_keys(struct device *device, struct device_attribute *attr, char *buf)
+{
+ struct lg_g710_plus_data* data = hid_get_drvdata(dev_get_drvdata(device->parent));
+ if (data != NULL) {
+ spin_lock(&data->lock);
+ init_completion(&data->ready);
+ hidhw_request(data->hdev, data->other_buttons_led_report, REQTYPE_READ);
+ wait_for_completion_timeout(&data->ready, WAIT_TIME_OUT);
+ spin_unlock(&data->lock);
+ return sprintf(buf, "%d\n", data->led_keys);
+ }
+ return 0;
+}
+
+static ssize_t lg_g710_plus_store_led_macro(struct device *device, struct device_attribute *attr, const char *buf, size_t count)
+{
+ unsigned long key_mask;
+ int retval;
+ struct lg_g710_plus_data* data = hid_get_drvdata(dev_get_drvdata(device->parent));
+ retval = kstrtoul(buf, 10, &key_mask);
+ if (retval)
+ return retval;
+
+ spin_lock(&data->lock);
+ data->mr_buttons_led_report->field[0]->value[0]= (key_mask & 0xF) << 4;
+ hidhw_request(data->hdev, data->mr_buttons_led_report, REQTYPE_WRITE);
+ spin_unlock(&data->lock);
+ return count;
+}
+
+static ssize_t lg_g710_plus_store_led_keys(struct device *device, struct device_attribute *attr, const char *buf, size_t count)
+{
+ int retval;
+ unsigned long key_mask;
+ u8 wasd_mask, keys_mask;
+ struct lg_g710_plus_data* data = hid_get_drvdata(dev_get_drvdata(device->parent));
+ retval = kstrtoul(buf, 10, &key_mask);
+ if (retval)
+ return retval;
+
+ wasd_mask= (key_mask >> 4) & 0xF;
+ keys_mask= (key_mask) & 0xF;
+
+ wasd_mask= wasd_mask > 4 ? 4 : wasd_mask;
+ keys_mask= keys_mask > 4 ? 4 : keys_mask;
+
+ spin_lock(&data->lock);
+ data->other_buttons_led_report->field[0]->value[0]= wasd_mask;
+ data->other_buttons_led_report->field[0]->value[1]= keys_mask;
+ hidhw_request(data->hdev, data->other_buttons_led_report, REQTYPE_WRITE);
+ spin_unlock(&data->lock);
+ return count;
+}
+
+static const struct hid_device_id lg_g710_plus_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_LOGITECH_KEYBOARD_G710_PLUS) },
+ { }
+};
+
+MODULE_DEVICE_TABLE(hid, lg_g710_plus_devices);
+static struct hid_driver lg_g710_plus_driver = {
+ .name = "hid-lg-g710-plus",
+ .id_table = lg_g710_plus_devices,
+ .raw_event = lg_g710_plus_raw_event,
+ .input_mapping = lg_g710_plus_input_mapping,
+ .probe= lg_g710_plus_probe,
+ .remove= lg_g710_plus_remove,
+};
+
+static int __init lg_g710_plus_init(void)
+{
+ return hid_register_driver(&lg_g710_plus_driver);
+}
+
+static void __exit lg_g710_plus_exit(void)
+{
+ hid_unregister_driver(&lg_g710_plus_driver);
+}
+
+module_init(lg_g710_plus_init);
+module_exit(lg_g710_plus_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Filip Wieladek <Wattos@gmail.com>");
+MODULE_DESCRIPTION("Logitech G710+ driver");