HID: Driver for Lenovo Keyboard with Trackpoint
authorBernhard Seibold <mail@bernhard-seibold.de>
Wed, 15 Feb 2012 12:40:43 +0000 (13:40 +0100)
committerJiri Kosina <jkosina@suse.cz>
Fri, 8 Jun 2012 08:44:10 +0000 (10:44 +0200)
This driver for the "Lenovo ThinkPad USB Keyboard with Trackpoint" supports
setting various device attributes, controlling mute and microphone mute
LEDs and enables use of the microphone mute key.

Signed-off-by: Bernhard Seibold <mail@bernhard-seibold.de>
Signed-off-by: Jiri Kosina <jkosina@suse.cz>
Documentation/ABI/testing/sysfs-driver-hid-lenovo-tpkbd [new file with mode: 0644]
drivers/hid/Kconfig
drivers/hid/Makefile
drivers/hid/hid-core.c
drivers/hid/hid-ids.h
drivers/hid/hid-lenovo-tpkbd.c [new file with mode: 0644]

diff --git a/Documentation/ABI/testing/sysfs-driver-hid-lenovo-tpkbd b/Documentation/ABI/testing/sysfs-driver-hid-lenovo-tpkbd
new file mode 100644 (file)
index 0000000..57b92cb
--- /dev/null
@@ -0,0 +1,38 @@
+What:          /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/press_to_select
+Date:          July 2011
+Contact:       linux-input@vger.kernel.org
+Description:   This controls if mouse clicks should be generated if the trackpoint is quickly pressed. How fast this press has to be
+               is being controlled by press_speed.
+               Values are 0 or 1.
+
+What:          /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/dragging
+Date:          July 2011
+Contact:       linux-input@vger.kernel.org
+Description:   If this setting is enabled, it is possible to do dragging by pressing the trackpoint. This requires press_to_select to be enabled.
+               Values are 0 or 1.
+
+What:          /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/release_to_select
+Date:          July 2011
+Contact:       linux-input@vger.kernel.org
+Description:   For details regarding this setting please refer to http://www.pc.ibm.com/ww/healthycomputing/trkpntb.html
+               Values are 0 or 1.
+
+What:          /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/select_right
+Date:          July 2011
+Contact:       linux-input@vger.kernel.org
+Description:   This setting controls if the mouse click events generated by pressing the trackpoint (if press_to_select is enabled) generate
+               a left or right mouse button click.
+               Values are 0 or 1.
+
+What:          /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/sensitivity
+Date:          July 2011
+Contact:       linux-input@vger.kernel.org
+Description:   This file contains the trackpoint sensitivity.
+               Values are decimal integers from 1 (lowest sensitivity) to 255 (highest sensitivity).
+
+What:          /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/press_speed
+Date:          July 2011
+Contact:       linux-input@vger.kernel.org
+Description:   This setting controls how fast the trackpoint needs to be pressed to generate a mouse click if press_to_select is enabled.
+               Values are decimal integers from 1 (slowest) to 255 (fastest).
+
index e9c68fedfcff0b90222cf6a48886fe5229f53d94..ca7e76cab83d699f5b90d1f0b35180a49efd51e6 100644 (file)
@@ -268,6 +268,18 @@ config HID_LCPOWER
        ---help---
        Support for LC-Power RC1000MCE RF remote control.
 
+config HID_LENOVO_TPKBD
+       tristate "Lenovo ThinkPad USB Keyboard with TrackPoint"
+       depends on USB_HID
+       select LEDS_CLASS
+       ---help---
+       Support for the Lenovo ThinkPad USB Keyboard with TrackPoint.
+
+       Say Y here if you have a Lenovo ThinkPad USB Keyboard with TrackPoint
+       and would like to use device-specific features like changing the
+       sensitivity of the trackpoint, using the microphone mute button or
+       controlling the mute and microphone mute LEDs.
+
 config HID_LOGITECH
        tristate "Logitech devices" if EXPERT
        depends on USB_HID
index ca6cc9f0485c90d22fb11d0c6deeb6560ac36158..05913c533ec569cfe3ad1e9836d699506a10dbe4 100644 (file)
@@ -54,6 +54,7 @@ obj-$(CONFIG_HID_KENSINGTON)  += hid-kensington.o
 obj-$(CONFIG_HID_KEYTOUCH)     += hid-keytouch.o
 obj-$(CONFIG_HID_KYE)          += hid-kye.o
 obj-$(CONFIG_HID_LCPOWER)       += hid-lcpower.o
+obj-$(CONFIG_HID_LENOVO_TPKBD) += hid-lenovo-tpkbd.o
 obj-$(CONFIG_HID_LOGITECH)     += hid-logitech.o
 obj-$(CONFIG_HID_LOGITECH_DJ)  += hid-logitech-dj.o
 obj-$(CONFIG_HID_MAGICMOUSE)    += hid-magicmouse.o
index 8e3a6b261477d9866ec45c83df4b2766098d281d..f69568032c04bd8e707fd8f2a51b3a9644c12931 100644 (file)
@@ -1544,6 +1544,7 @@ static const struct hid_device_id hid_have_special_driver[] = {
        { HID_USB_DEVICE(USB_VENDOR_ID_KYE, USB_DEVICE_ID_KYE_EASYPEN_M610X) },
        { HID_USB_DEVICE(USB_VENDOR_ID_LABTEC, USB_DEVICE_ID_LABTEC_WIRELESS_KEYBOARD) },
        { HID_USB_DEVICE(USB_VENDOR_ID_LCPOWER, USB_DEVICE_ID_LCPOWER_LC1000 ) },
+       { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO, USB_DEVICE_ID_LENOVO_TPKBD) },
        { HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_MX3000_RECEIVER) },
        { HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_S510_RECEIVER) },
        { HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_S510_RECEIVER_2) },
index 9373f535dfe9908c14b8ff04afbe90a981693a54..c607e6f33953f8838532d28a6d6d5106330640e2 100644 (file)
 #define USB_DEVICE_ID_LD_HYBRID                0x2090
 #define USB_DEVICE_ID_LD_HEATCONTROL   0x20A0
 
+#define USB_VENDOR_ID_LENOVO           0x17ef
+#define USB_DEVICE_ID_LENOVO_TPKBD     0x6009
+
 #define USB_VENDOR_ID_LG               0x1fd2
 #define USB_DEVICE_ID_LG_MULTITOUCH    0x0064
 
diff --git a/drivers/hid/hid-lenovo-tpkbd.c b/drivers/hid/hid-lenovo-tpkbd.c
new file mode 100644 (file)
index 0000000..77d2df0
--- /dev/null
@@ -0,0 +1,564 @@
+/*
+ *  HID driver for Lenovo ThinkPad USB Keyboard with TrackPoint
+ *
+ *  Copyright (c) 2012 Bernhard Seibold
+ */
+
+/*
+ * 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/module.h>
+#include <linux/sysfs.h>
+#include <linux/device.h>
+#include <linux/usb.h>
+#include <linux/hid.h>
+#include <linux/input.h>
+#include <linux/leds.h>
+#include "usbhid/usbhid.h"
+
+#include "hid-ids.h"
+
+/* This is only used for the trackpoint part of the driver, hence _tp */
+struct tpkbd_data_pointer {
+       int led_state;
+       struct led_classdev led_mute;
+       struct led_classdev led_micmute;
+       int press_to_select;
+       int dragging;
+       int release_to_select;
+       int select_right;
+       int sensitivity;
+       int press_speed;
+};
+
+#define map_key_clear(c) hid_map_usage_clear(hi, usage, bit, max, EV_KEY, (c))
+
+static int tpkbd_input_mapping(struct hid_device *hdev,
+               struct hid_input *hi, struct hid_field *field,
+               struct hid_usage *usage, unsigned long **bit, int *max)
+{
+       struct usbhid_device *uhdev;
+
+       uhdev = (struct usbhid_device *) hdev->driver_data;
+       if (uhdev->ifnum == 1 && usage->hid == (HID_UP_BUTTON | 0x0010)) {
+               map_key_clear(KEY_MICMUTE);
+               return 1;
+       }
+       return 0;
+}
+
+#undef map_key_clear
+
+static int tpkbd_features_set(struct hid_device *hdev)
+{
+       struct hid_report *report;
+       struct tpkbd_data_pointer *data_pointer;
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+       report = hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[4];
+
+       report->field[0]->value[0]  = data_pointer->press_to_select   ? 0x01 : 0x02;
+       report->field[0]->value[0] |= data_pointer->dragging          ? 0x04 : 0x08;
+       report->field[0]->value[0] |= data_pointer->release_to_select ? 0x10 : 0x20;
+       report->field[0]->value[0] |= data_pointer->select_right      ? 0x80 : 0x40;
+       report->field[1]->value[0] = 0x03; // unknown setting, imitate windows driver
+       report->field[2]->value[0] = data_pointer->sensitivity;
+       report->field[3]->value[0] = data_pointer->press_speed;
+
+       usbhid_submit_report(hdev, report, USB_DIR_OUT);
+       return 0;
+}
+
+static ssize_t pointer_press_to_select_show(struct device *dev,
+               struct device_attribute *attr,
+               char *buf)
+{
+       struct hid_device *hdev;
+       struct tpkbd_data_pointer *data_pointer;
+
+       hdev = container_of(dev, struct hid_device, dev);
+       if (hdev == NULL)
+               return -ENODEV;
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       return snprintf(buf, PAGE_SIZE, "%u\n", data_pointer->press_to_select);
+}
+
+static ssize_t pointer_press_to_select_store(struct device *dev,
+               struct device_attribute *attr,
+               const char *buf,
+               size_t count)
+{
+       struct hid_device *hdev;
+       struct tpkbd_data_pointer *data_pointer;
+       int value;
+
+       hdev = container_of(dev, struct hid_device, dev);
+       if (hdev == NULL)
+               return -ENODEV;
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       if (kstrtoint(buf, 10, &value))
+               return -EINVAL;
+       if (value < 0 || value > 1)
+               return -EINVAL;
+
+       data_pointer->press_to_select = value;
+       tpkbd_features_set(hdev);
+
+       return count;
+}
+
+static ssize_t pointer_dragging_show(struct device *dev,
+               struct device_attribute *attr,
+               char *buf)
+{
+       struct hid_device *hdev;
+       struct tpkbd_data_pointer *data_pointer;
+
+       hdev = container_of(dev, struct hid_device, dev);
+       if (hdev == NULL)
+               return -ENODEV;
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       return snprintf(buf, PAGE_SIZE, "%u\n", data_pointer->dragging);
+}
+
+static ssize_t pointer_dragging_store(struct device *dev,
+               struct device_attribute *attr,
+               const char *buf,
+               size_t count)
+{
+       struct hid_device *hdev;
+       struct tpkbd_data_pointer *data_pointer;
+       int value;
+
+       hdev = container_of(dev, struct hid_device, dev);
+       if (hdev == NULL)
+               return -ENODEV;
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       if (kstrtoint(buf, 10, &value))
+               return -EINVAL;
+       if (value < 0 || value > 1)
+               return -EINVAL;
+
+       data_pointer->dragging = value;
+       tpkbd_features_set(hdev);
+
+       return count;
+}
+
+static ssize_t pointer_release_to_select_show(struct device *dev,
+               struct device_attribute *attr,
+               char *buf)
+{
+       struct hid_device *hdev;
+       struct tpkbd_data_pointer *data_pointer;
+
+       hdev = container_of(dev, struct hid_device, dev);
+       if (hdev == NULL)
+               return -ENODEV;
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       return snprintf(buf, PAGE_SIZE, "%u\n", data_pointer->release_to_select);
+}
+
+static ssize_t pointer_release_to_select_store(struct device *dev,
+               struct device_attribute *attr,
+               const char *buf,
+               size_t count)
+{
+       struct hid_device *hdev;
+       struct tpkbd_data_pointer *data_pointer;
+       int value;
+
+       hdev = container_of(dev, struct hid_device, dev);
+       if (hdev == NULL)
+               return -ENODEV;
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       if (kstrtoint(buf, 10, &value))
+               return -EINVAL;
+       if (value < 0 || value > 1)
+               return -EINVAL;
+
+       data_pointer->release_to_select = value;
+       tpkbd_features_set(hdev);
+
+       return count;
+}
+
+static ssize_t pointer_select_right_show(struct device *dev,
+               struct device_attribute *attr,
+               char *buf)
+{
+       struct hid_device *hdev;
+       struct tpkbd_data_pointer *data_pointer;
+
+       hdev = container_of(dev, struct hid_device, dev);
+       if (hdev == NULL)
+               return -ENODEV;
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       return snprintf(buf, PAGE_SIZE, "%u\n", data_pointer->select_right);
+}
+
+static ssize_t pointer_select_right_store(struct device *dev,
+               struct device_attribute *attr,
+               const char *buf,
+               size_t count)
+{
+       struct hid_device *hdev;
+       struct tpkbd_data_pointer *data_pointer;
+       int value;
+
+       hdev = container_of(dev, struct hid_device, dev);
+       if (hdev == NULL)
+               return -ENODEV;
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       if (kstrtoint(buf, 10, &value))
+               return -EINVAL;
+       if (value < 0 || value > 1)
+               return -EINVAL;
+
+       data_pointer->select_right = value;
+       tpkbd_features_set(hdev);
+
+       return count;
+}
+
+static ssize_t pointer_sensitivity_show(struct device *dev,
+               struct device_attribute *attr,
+               char *buf)
+{
+       struct hid_device *hdev;
+       struct tpkbd_data_pointer *data_pointer;
+
+       hdev = container_of(dev, struct hid_device, dev);
+       if (hdev == NULL)
+               return -ENODEV;
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       return snprintf(buf, PAGE_SIZE, "%u\n",
+               data_pointer->sensitivity);
+}
+
+static ssize_t pointer_sensitivity_store(struct device *dev,
+               struct device_attribute *attr,
+               const char *buf,
+               size_t count)
+{
+       struct hid_device *hdev;
+       struct tpkbd_data_pointer *data_pointer;
+       int value;
+
+       hdev = container_of(dev, struct hid_device, dev);
+       if (hdev == NULL)
+               return -ENODEV;
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       if (kstrtoint(buf, 10, &value) || value < 1 || value > 255)
+               return -EINVAL;
+
+       data_pointer->sensitivity = value;
+       tpkbd_features_set(hdev);
+
+       return count;
+}
+
+static ssize_t pointer_press_speed_show(struct device *dev,
+               struct device_attribute *attr,
+               char *buf)
+{
+       struct hid_device *hdev;
+       struct tpkbd_data_pointer *data_pointer;
+
+       hdev = container_of(dev, struct hid_device, dev);
+       if (hdev == NULL)
+               return -ENODEV;
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       return snprintf(buf, PAGE_SIZE, "%u\n",
+               data_pointer->press_speed);
+}
+
+static ssize_t pointer_press_speed_store(struct device *dev,
+               struct device_attribute *attr,
+               const char *buf,
+               size_t count)
+{
+       struct hid_device *hdev;
+       struct tpkbd_data_pointer *data_pointer;
+       int value;
+
+       hdev = container_of(dev, struct hid_device, dev);
+       if (hdev == NULL)
+               return -ENODEV;
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       if (kstrtoint(buf, 10, &value) || value < 1 || value > 255)
+               return -EINVAL;
+
+       data_pointer->press_speed = value;
+       tpkbd_features_set(hdev);
+
+       return count;
+}
+
+static struct device_attribute dev_attr_pointer_press_to_select =
+       __ATTR(press_to_select, S_IWUSR | S_IRUGO,
+                       pointer_press_to_select_show,
+                       pointer_press_to_select_store);
+
+static struct device_attribute dev_attr_pointer_dragging =
+       __ATTR(dragging, S_IWUSR | S_IRUGO,
+                       pointer_dragging_show,
+                       pointer_dragging_store);
+
+static struct device_attribute dev_attr_pointer_release_to_select =
+       __ATTR(release_to_select, S_IWUSR | S_IRUGO,
+                       pointer_release_to_select_show,
+                       pointer_release_to_select_store);
+
+static struct device_attribute dev_attr_pointer_select_right =
+       __ATTR(select_right, S_IWUSR | S_IRUGO,
+                       pointer_select_right_show,
+                       pointer_select_right_store);
+
+static struct device_attribute dev_attr_pointer_sensitivity =
+       __ATTR(sensitivity, S_IWUSR | S_IRUGO,
+                       pointer_sensitivity_show,
+                       pointer_sensitivity_store);
+
+static struct device_attribute dev_attr_pointer_press_speed =
+       __ATTR(press_speed, S_IWUSR | S_IRUGO,
+                       pointer_press_speed_show,
+                       pointer_press_speed_store);
+
+static struct attribute *tpkbd_attributes_pointer[] = {
+       &dev_attr_pointer_press_to_select.attr,
+       &dev_attr_pointer_dragging.attr,
+       &dev_attr_pointer_release_to_select.attr,
+       &dev_attr_pointer_select_right.attr,
+       &dev_attr_pointer_sensitivity.attr,
+       &dev_attr_pointer_press_speed.attr,
+       NULL
+};
+
+static const struct attribute_group tpkbd_attr_group_pointer = {
+       .attrs = tpkbd_attributes_pointer,
+};
+
+static enum led_brightness tpkbd_led_brightness_get(
+                       struct led_classdev *led_cdev)
+{
+       struct device *dev;
+       struct hid_device *hdev;
+       struct tpkbd_data_pointer *data_pointer;
+       int led_nr = 0;
+
+       dev = led_cdev->dev->parent;
+       hdev = container_of(dev, struct hid_device, dev);
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       if (led_cdev == &data_pointer->led_micmute)
+               led_nr = 1;
+
+       return data_pointer->led_state & (1 << led_nr)
+                               ? LED_FULL
+                               : LED_OFF;
+}
+
+static void tpkbd_led_brightness_set(struct led_classdev *led_cdev,
+                       enum led_brightness value)
+{
+       struct device *dev;
+       struct hid_device *hdev;
+       struct hid_report *report;
+       struct tpkbd_data_pointer *data_pointer;
+       int led_nr = 0;
+
+       dev = led_cdev->dev->parent;
+       hdev = container_of(dev, struct hid_device, dev);
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       if (led_cdev == &data_pointer->led_micmute)
+               led_nr = 1;
+
+       if (value == LED_OFF)
+               data_pointer->led_state &= ~(1 << led_nr);
+       else
+               data_pointer->led_state |= 1 << led_nr;
+
+       report = hdev->report_enum[HID_OUTPUT_REPORT].report_id_hash[3];
+       report->field[0]->value[0] = (data_pointer->led_state >> 0) & 1;
+       report->field[0]->value[1] = (data_pointer->led_state >> 1) & 1;
+       usbhid_submit_report(hdev, report, USB_DIR_OUT);
+}
+
+static int tpkbd_probe_tp(struct hid_device *hdev)
+{
+       struct device *dev = &hdev->dev;
+       struct tpkbd_data_pointer *data_pointer;
+       size_t name_sz = strlen(dev_name(dev)) + 16;
+       char *name_mute, *name_micmute;
+       int ret;
+
+       if (sysfs_create_group(&hdev->dev.kobj,
+                               &tpkbd_attr_group_pointer)) {
+               hid_warn(hdev, "Could not create sysfs group\n");
+       }
+
+       data_pointer = kzalloc(sizeof(struct tpkbd_data_pointer), GFP_KERNEL);
+       if (data_pointer == NULL) {
+               hid_err(hdev, "Could not allocate memory for driver data\n");
+               return -ENOMEM;
+       }
+
+       // set same default values as windows driver
+       data_pointer->sensitivity = 0xa0;
+       data_pointer->press_speed = 0x38;
+
+       name_mute = kzalloc(name_sz, GFP_KERNEL);
+       if (name_mute == NULL) {
+               hid_err(hdev, "Could not allocate memory for led data\n");
+               ret = -ENOMEM;
+               goto err;
+       }
+       snprintf(name_mute, name_sz, "%s:amber:mute", dev_name(dev));
+
+       name_micmute = kzalloc(name_sz, GFP_KERNEL);
+       if (name_micmute == NULL) {
+               hid_err(hdev, "Could not allocate memory for led data\n");
+               ret = -ENOMEM;
+               goto err2;
+       }
+       snprintf(name_micmute, name_sz, "%s:amber:micmute", dev_name(dev));
+
+       hid_set_drvdata(hdev, data_pointer);
+
+       data_pointer->led_mute.name = name_mute;
+       data_pointer->led_mute.brightness_get = tpkbd_led_brightness_get;
+       data_pointer->led_mute.brightness_set = tpkbd_led_brightness_set;
+       data_pointer->led_mute.dev = dev;
+       led_classdev_register(dev, &data_pointer->led_mute);
+
+       data_pointer->led_micmute.name = name_micmute;
+       data_pointer->led_micmute.brightness_get = tpkbd_led_brightness_get;
+       data_pointer->led_micmute.brightness_set = tpkbd_led_brightness_set;
+       data_pointer->led_micmute.dev = dev;
+       led_classdev_register(dev, &data_pointer->led_micmute);
+
+       tpkbd_features_set(hdev);
+
+       return 0;
+
+err2:
+       kfree(name_mute);
+err:
+       kfree(data_pointer);
+       return ret;
+}
+
+static int tpkbd_probe(struct hid_device *hdev,
+               const struct hid_device_id *id)
+{
+       int ret;
+       struct usbhid_device *uhdev;
+
+       ret = hid_parse(hdev);
+       if (ret) {
+               hid_err(hdev, "hid_parse failed\n");
+               goto err_free;
+       }
+
+       ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+       if (ret) {
+               hid_err(hdev, "hid_hw_start failed\n");
+               goto err_free;
+       }
+
+       uhdev = (struct usbhid_device *) hdev->driver_data;
+
+       if (uhdev->ifnum == 1)
+               return tpkbd_probe_tp(hdev);
+
+       return 0;
+err_free:
+       return ret;
+}
+
+static void tpkbd_remove_tp(struct hid_device *hdev)
+{
+       struct tpkbd_data_pointer *data_pointer;
+
+       sysfs_remove_group(&hdev->dev.kobj,
+                       &tpkbd_attr_group_pointer);
+
+       data_pointer = (struct tpkbd_data_pointer *) hid_get_drvdata(hdev);
+
+       led_classdev_unregister(&data_pointer->led_micmute);
+       led_classdev_unregister(&data_pointer->led_mute);
+
+       hid_set_drvdata(hdev, NULL);
+       kfree(data_pointer);
+}
+
+static void tpkbd_remove(struct hid_device *hdev)
+{
+       struct usbhid_device *uhdev;
+
+       uhdev = (struct usbhid_device *) hdev->driver_data;
+       if (uhdev->ifnum == 1)
+               tpkbd_remove_tp(hdev);
+
+       hid_hw_stop(hdev);
+}
+
+static const struct hid_device_id tpkbd_devices[] = {
+       { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO, USB_DEVICE_ID_LENOVO_TPKBD) },
+       { }
+};
+
+MODULE_DEVICE_TABLE(hid, tpkbd_devices);
+
+static struct hid_driver tpkbd_driver = {
+       .name = "lenovo_tpkbd",
+       .id_table = tpkbd_devices,
+       .input_mapping = tpkbd_input_mapping,
+       .probe = tpkbd_probe,
+       .remove = tpkbd_remove,
+};
+
+static int __init tpkbd_init(void)
+{
+       return hid_register_driver(&tpkbd_driver);
+}
+
+static void __exit tpkbd_exit(void)
+{
+       hid_unregister_driver(&tpkbd_driver);
+}
+
+module_init(tpkbd_init);
+module_exit(tpkbd_exit);
+
+MODULE_LICENSE("GPL");