diff options
author | Nikolai Kondrashov <spbnick@gmail.com> | 2019-02-10 12:13:50 +0200 |
---|---|---|
committer | Benjamin Tissoires <benjamin.tissoires@redhat.com> | 2019-02-21 12:00:53 +0100 |
commit | ff0c13d6d2edc9c4952c668f4503a51b5f101ab3 (patch) | |
tree | d34fa11d20bc01b0b5b0f9dec2964f121f01784f /drivers/hid/hid-uclogic-core.c | |
parent | bf226cd3316a01d71eb74ea5736f6c8905ff5041 (diff) |
HID: uclogic: Extract report descriptors to a module
As hid-uclogic has a lot of report descriptors already and there's going
to be more, move them out of the driver code and into a separate module.
Signed-off-by: Nikolai Kondrashov <spbnick@gmail.com>
Signed-off-by: Benjamin Tissoires <benjamin.tissoires@redhat.com>
Diffstat (limited to 'drivers/hid/hid-uclogic-core.c')
-rw-r--r-- | drivers/hid/hid-uclogic-core.c | 493 |
1 files changed, 493 insertions, 0 deletions
diff --git a/drivers/hid/hid-uclogic-core.c b/drivers/hid/hid-uclogic-core.c new file mode 100644 index 000000000000..4042183ee9a3 --- /dev/null +++ b/drivers/hid/hid-uclogic-core.c @@ -0,0 +1,493 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * HID driver for UC-Logic devices not fully compliant with HID standard + * + * Copyright (c) 2010-2014 Nikolai Kondrashov + * Copyright (c) 2013 Martin Rusko + */ + +/* + * 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/device.h> +#include <linux/hid.h> +#include <linux/module.h> +#include <linux/usb.h> +#include "usbhid/usbhid.h" +#include "hid-uclogic-rdesc.h" + +#include "hid-ids.h" + +/* Parameter indices */ +enum uclogic_prm { + UCLOGIC_PRM_X_LM = 1, + UCLOGIC_PRM_Y_LM = 2, + UCLOGIC_PRM_PRESSURE_LM = 4, + UCLOGIC_PRM_RESOLUTION = 5, + UCLOGIC_PRM_NUM +}; + +/* Driver data */ +struct uclogic_drvdata { + __u8 *rdesc; + unsigned int rsize; + bool invert_pen_inrange; + bool ignore_pen_usage; + bool has_virtual_pad_interface; +}; + +static __u8 *uclogic_report_fixup(struct hid_device *hdev, __u8 *rdesc, + unsigned int *rsize) +{ + struct usb_interface *iface = to_usb_interface(hdev->dev.parent); + __u8 iface_num = iface->cur_altsetting->desc.bInterfaceNumber; + struct uclogic_drvdata *drvdata = hid_get_drvdata(hdev); + + if (drvdata->rdesc != NULL) { + rdesc = drvdata->rdesc; + *rsize = drvdata->rsize; + return rdesc; + } + + switch (hdev->product) { + case USB_DEVICE_ID_UCLOGIC_TABLET_PF1209: + if (*rsize == UCLOGIC_RDESC_PF1209_ORIG_SIZE) { + rdesc = uclogic_rdesc_pf1209_fixed_arr; + *rsize = uclogic_rdesc_pf1209_fixed_size; + } + break; + case USB_DEVICE_ID_UCLOGIC_TABLET_WP4030U: + if (*rsize == UCLOGIC_RDESC_WPXXXXU_ORIG_SIZE) { + rdesc = uclogic_rdesc_wp4030u_fixed_arr; + *rsize = uclogic_rdesc_wp4030u_fixed_size; + } + break; + case USB_DEVICE_ID_UCLOGIC_TABLET_WP5540U: + if (*rsize == UCLOGIC_RDESC_WPXXXXU_ORIG_SIZE) { + rdesc = uclogic_rdesc_wp5540u_fixed_arr; + *rsize = uclogic_rdesc_wp5540u_fixed_size; + } + break; + case USB_DEVICE_ID_UCLOGIC_TABLET_WP8060U: + if (*rsize == UCLOGIC_RDESC_WPXXXXU_ORIG_SIZE) { + rdesc = uclogic_rdesc_wp8060u_fixed_arr; + *rsize = uclogic_rdesc_wp8060u_fixed_size; + } + break; + case USB_DEVICE_ID_UCLOGIC_TABLET_WP1062: + if (*rsize == UCLOGIC_RDESC_WP1062_ORIG_SIZE) { + rdesc = uclogic_rdesc_wp1062_fixed_arr; + *rsize = uclogic_rdesc_wp1062_fixed_size; + } + break; + case USB_DEVICE_ID_UCLOGIC_WIRELESS_TABLET_TWHL850: + switch (iface_num) { + case 0: + if (*rsize == UCLOGIC_RDESC_TWHL850_ORIG0_SIZE) { + rdesc = uclogic_rdesc_twhl850_fixed0_arr; + *rsize = uclogic_rdesc_twhl850_fixed0_size; + } + break; + case 1: + if (*rsize == UCLOGIC_RDESC_TWHL850_ORIG1_SIZE) { + rdesc = uclogic_rdesc_twhl850_fixed1_arr; + *rsize = uclogic_rdesc_twhl850_fixed1_size; + } + break; + case 2: + if (*rsize == UCLOGIC_RDESC_TWHL850_ORIG2_SIZE) { + rdesc = uclogic_rdesc_twhl850_fixed2_arr; + *rsize = uclogic_rdesc_twhl850_fixed2_size; + } + break; + } + break; + case USB_DEVICE_ID_UCLOGIC_TABLET_TWHA60: + switch (iface_num) { + case 0: + if (*rsize == UCLOGIC_RDESC_TWHA60_ORIG0_SIZE) { + rdesc = uclogic_rdesc_twha60_fixed0_arr; + *rsize = uclogic_rdesc_twha60_fixed0_size; + } + break; + case 1: + if (*rsize == UCLOGIC_RDESC_TWHA60_ORIG1_SIZE) { + rdesc = uclogic_rdesc_twha60_fixed1_arr; + *rsize = uclogic_rdesc_twha60_fixed1_size; + } + break; + } + break; + } + + return rdesc; +} + +static int uclogic_input_mapping(struct hid_device *hdev, struct hid_input *hi, + struct hid_field *field, struct hid_usage *usage, + unsigned long **bit, int *max) +{ + struct uclogic_drvdata *drvdata = hid_get_drvdata(hdev); + + /* discard the unused pen interface */ + if ((drvdata->ignore_pen_usage) && + (field->application == HID_DG_PEN)) + return -1; + + /* let hid-core decide what to do */ + return 0; +} + +static int uclogic_input_configured(struct hid_device *hdev, + struct hid_input *hi) +{ + char *name; + const char *suffix = NULL; + struct hid_field *field; + size_t len; + + /* no report associated (HID_QUIRK_MULTI_INPUT not set) */ + if (!hi->report) + return 0; + + field = hi->report->field[0]; + + switch (field->application) { + case HID_GD_KEYBOARD: + suffix = "Keyboard"; + break; + case HID_GD_MOUSE: + suffix = "Mouse"; + break; + case HID_GD_KEYPAD: + suffix = "Pad"; + break; + case HID_DG_PEN: + suffix = "Pen"; + break; + case HID_CP_CONSUMER_CONTROL: + suffix = "Consumer Control"; + break; + case HID_GD_SYSTEM_CONTROL: + suffix = "System Control"; + break; + } + + if (suffix) { + len = strlen(hdev->name) + 2 + strlen(suffix); + name = devm_kzalloc(&hi->input->dev, len, GFP_KERNEL); + if (name) { + snprintf(name, len, "%s %s", hdev->name, suffix); + hi->input->name = name; + } + } + + return 0; +} + +/** + * Enable fully-functional tablet mode and determine device parameters. + * + * @hdev: HID device + */ +static int uclogic_tablet_enable(struct hid_device *hdev) +{ + int rc; + struct usb_device *usb_dev = hid_to_usb_dev(hdev); + struct uclogic_drvdata *drvdata = hid_get_drvdata(hdev); + __le16 *buf = NULL; + size_t len; + s32 params[UCLOGIC_RDESC_PEN_PH_ID_NUM]; + s32 resolution; + + /* + * Read string descriptor containing tablet parameters. The specific + * string descriptor and data were discovered by sniffing the Windows + * driver traffic. + * NOTE: This enables fully-functional tablet mode. + */ + len = UCLOGIC_PRM_NUM * sizeof(*buf); + buf = kmalloc(len, GFP_KERNEL); + if (buf == NULL) { + rc = -ENOMEM; + goto cleanup; + } + rc = usb_control_msg(usb_dev, usb_rcvctrlpipe(usb_dev, 0), + USB_REQ_GET_DESCRIPTOR, USB_DIR_IN, + (USB_DT_STRING << 8) + 0x64, + 0x0409, buf, len, + USB_CTRL_GET_TIMEOUT); + if (rc == -EPIPE) { + hid_err(hdev, "device parameters not found\n"); + rc = -ENODEV; + goto cleanup; + } else if (rc < 0) { + hid_err(hdev, "failed to get device parameters: %d\n", rc); + rc = -ENODEV; + goto cleanup; + } else if (rc != len) { + hid_err(hdev, "invalid device parameters\n"); + rc = -ENODEV; + goto cleanup; + } + + /* Extract device parameters */ + params[UCLOGIC_RDESC_PEN_PH_ID_X_LM] = + le16_to_cpu(buf[UCLOGIC_PRM_X_LM]); + params[UCLOGIC_RDESC_PEN_PH_ID_Y_LM] = + le16_to_cpu(buf[UCLOGIC_PRM_Y_LM]); + params[UCLOGIC_RDESC_PEN_PH_ID_PRESSURE_LM] = + le16_to_cpu(buf[UCLOGIC_PRM_PRESSURE_LM]); + resolution = le16_to_cpu(buf[UCLOGIC_PRM_RESOLUTION]); + if (resolution == 0) { + params[UCLOGIC_RDESC_PEN_PH_ID_X_PM] = 0; + params[UCLOGIC_RDESC_PEN_PH_ID_Y_PM] = 0; + } else { + params[UCLOGIC_RDESC_PEN_PH_ID_X_PM] = + params[UCLOGIC_RDESC_PEN_PH_ID_X_LM] * + 1000 / resolution; + params[UCLOGIC_RDESC_PEN_PH_ID_Y_PM] = + params[UCLOGIC_RDESC_PEN_PH_ID_Y_LM] * + 1000 / resolution; + } + + /* Format fixed report descriptor */ + drvdata->rdesc = uclogic_rdesc_template_apply( + uclogic_rdesc_pen_template_arr, + uclogic_rdesc_pen_template_size, + params, ARRAY_SIZE(params)); + if (drvdata->rdesc == NULL) { + rc = -ENOMEM; + goto cleanup; + } + drvdata->rsize = uclogic_rdesc_pen_template_size; + + rc = 0; + +cleanup: + kfree(buf); + return rc; +} + +/** + * Enable actual button mode. + * + * @hdev: HID device + */ +static int uclogic_button_enable(struct hid_device *hdev) +{ + int rc; + struct usb_device *usb_dev = hid_to_usb_dev(hdev); + struct uclogic_drvdata *drvdata = hid_get_drvdata(hdev); + char *str_buf; + size_t str_len = 16; + unsigned char *rdesc; + size_t rdesc_len; + + str_buf = kzalloc(str_len, GFP_KERNEL); + if (str_buf == NULL) { + rc = -ENOMEM; + goto cleanup; + } + + /* Enable abstract keyboard mode */ + rc = usb_string(usb_dev, 0x7b, str_buf, str_len); + if (rc == -EPIPE) { + hid_info(hdev, "button mode setting not found\n"); + rc = 0; + goto cleanup; + } else if (rc < 0) { + hid_err(hdev, "failed to enable abstract keyboard\n"); + goto cleanup; + } else if (strncmp(str_buf, "HK On", rc)) { + hid_info(hdev, "invalid answer when requesting buttons: '%s'\n", + str_buf); + rc = -EINVAL; + goto cleanup; + } + + /* Re-allocate fixed report descriptor */ + rdesc_len = drvdata->rsize + uclogic_rdesc_buttonpad_size; + rdesc = devm_kzalloc(&hdev->dev, rdesc_len, GFP_KERNEL); + if (!rdesc) { + rc = -ENOMEM; + goto cleanup; + } + + memcpy(rdesc, drvdata->rdesc, drvdata->rsize); + + /* Append the buttonpad descriptor */ + memcpy(rdesc + drvdata->rsize, uclogic_rdesc_buttonpad_arr, + uclogic_rdesc_buttonpad_size); + + /* clean up old rdesc and use the new one */ + drvdata->rsize = rdesc_len; + devm_kfree(&hdev->dev, drvdata->rdesc); + drvdata->rdesc = rdesc; + + rc = 0; + +cleanup: + kfree(str_buf); + return rc; +} + +static int uclogic_probe(struct hid_device *hdev, + const struct hid_device_id *id) +{ + int rc; + struct usb_interface *intf = to_usb_interface(hdev->dev.parent); + struct usb_device *udev = hid_to_usb_dev(hdev); + struct uclogic_drvdata *drvdata; + + /* + * libinput requires the pad interface to be on a different node + * than the pen, so use QUIRK_MULTI_INPUT for all tablets. + */ + hdev->quirks |= HID_QUIRK_MULTI_INPUT; + + /* Allocate and assign driver data */ + drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL); + if (drvdata == NULL) + return -ENOMEM; + + hid_set_drvdata(hdev, drvdata); + + switch (id->product) { + case USB_DEVICE_ID_HUION_TABLET: + case USB_DEVICE_ID_YIYNOVA_TABLET: + case USB_DEVICE_ID_UCLOGIC_UGEE_TABLET_81: + case USB_DEVICE_ID_UCLOGIC_DRAWIMAGE_G3: + case USB_DEVICE_ID_UCLOGIC_UGEE_TABLET_45: + /* If this is the pen interface */ + if (intf->cur_altsetting->desc.bInterfaceNumber == 0) { + rc = uclogic_tablet_enable(hdev); + if (rc) { + hid_err(hdev, "tablet enabling failed\n"); + return rc; + } + drvdata->invert_pen_inrange = true; + + rc = uclogic_button_enable(hdev); + drvdata->has_virtual_pad_interface = !rc; + } else { + drvdata->ignore_pen_usage = true; + } + break; + case USB_DEVICE_ID_UGTIZER_TABLET_GP0610: + case USB_DEVICE_ID_UGEE_TABLET_EX07S: + /* If this is the pen interface */ + if (intf->cur_altsetting->desc.bInterfaceNumber == 1) { + rc = uclogic_tablet_enable(hdev); + if (rc) { + hid_err(hdev, "tablet enabling failed\n"); + return rc; + } + drvdata->invert_pen_inrange = true; + } else { + drvdata->ignore_pen_usage = true; + } + break; + case USB_DEVICE_ID_UCLOGIC_TABLET_TWHA60: + /* + * If it is the three-interface version, which is known to + * respond to initialization. + */ + if (udev->config->desc.bNumInterfaces == 3) { + /* If it is the pen interface */ + if (intf->cur_altsetting->desc.bInterfaceNumber == 0) { + rc = uclogic_tablet_enable(hdev); + if (rc) { + hid_err(hdev, "tablet enabling failed\n"); + return rc; + } + drvdata->invert_pen_inrange = true; + + rc = uclogic_button_enable(hdev); + drvdata->has_virtual_pad_interface = !rc; + } else { + drvdata->ignore_pen_usage = true; + } + } + break; + } + + rc = hid_parse(hdev); + if (rc) { + hid_err(hdev, "parse failed\n"); + return rc; + } + + rc = hid_hw_start(hdev, HID_CONNECT_DEFAULT); + if (rc) { + hid_err(hdev, "hw start failed\n"); + return rc; + } + + return 0; +} + +static int uclogic_raw_event(struct hid_device *hdev, struct hid_report *report, + u8 *data, int size) +{ + struct uclogic_drvdata *drvdata = hid_get_drvdata(hdev); + + if ((report->type == HID_INPUT_REPORT) && + (report->id == UCLOGIC_RDESC_PEN_ID) && + (size >= 2)) { + if (drvdata->has_virtual_pad_interface && (data[1] & 0x20)) + /* Change to virtual frame button report ID */ + data[0] = 0xf7; + else if (drvdata->invert_pen_inrange) + /* Invert the in-range bit */ + data[1] ^= 0x40; + } + + return 0; +} + +static const struct hid_device_id uclogic_devices[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_UCLOGIC, + USB_DEVICE_ID_UCLOGIC_TABLET_PF1209) }, + { HID_USB_DEVICE(USB_VENDOR_ID_UCLOGIC, + USB_DEVICE_ID_UCLOGIC_TABLET_WP4030U) }, + { HID_USB_DEVICE(USB_VENDOR_ID_UCLOGIC, + USB_DEVICE_ID_UCLOGIC_TABLET_WP5540U) }, + { HID_USB_DEVICE(USB_VENDOR_ID_UCLOGIC, + USB_DEVICE_ID_UCLOGIC_TABLET_WP8060U) }, + { HID_USB_DEVICE(USB_VENDOR_ID_UCLOGIC, + USB_DEVICE_ID_UCLOGIC_TABLET_WP1062) }, + { HID_USB_DEVICE(USB_VENDOR_ID_UCLOGIC, + USB_DEVICE_ID_UCLOGIC_WIRELESS_TABLET_TWHL850) }, + { HID_USB_DEVICE(USB_VENDOR_ID_UCLOGIC, + USB_DEVICE_ID_UCLOGIC_TABLET_TWHA60) }, + { HID_USB_DEVICE(USB_VENDOR_ID_HUION, USB_DEVICE_ID_HUION_TABLET) }, + { HID_USB_DEVICE(USB_VENDOR_ID_UCLOGIC, USB_DEVICE_ID_HUION_TABLET) }, + { HID_USB_DEVICE(USB_VENDOR_ID_UCLOGIC, USB_DEVICE_ID_YIYNOVA_TABLET) }, + { HID_USB_DEVICE(USB_VENDOR_ID_UCLOGIC, USB_DEVICE_ID_UCLOGIC_UGEE_TABLET_81) }, + { HID_USB_DEVICE(USB_VENDOR_ID_UCLOGIC, USB_DEVICE_ID_UCLOGIC_UGEE_TABLET_45) }, + { HID_USB_DEVICE(USB_VENDOR_ID_UCLOGIC, USB_DEVICE_ID_UCLOGIC_DRAWIMAGE_G3) }, + { HID_USB_DEVICE(USB_VENDOR_ID_UGTIZER, USB_DEVICE_ID_UGTIZER_TABLET_GP0610) }, + { HID_USB_DEVICE(USB_VENDOR_ID_UGEE, USB_DEVICE_ID_UGEE_TABLET_EX07S) }, + { } +}; +MODULE_DEVICE_TABLE(hid, uclogic_devices); + +static struct hid_driver uclogic_driver = { + .name = "uclogic", + .id_table = uclogic_devices, + .probe = uclogic_probe, + .report_fixup = uclogic_report_fixup, + .raw_event = uclogic_raw_event, + .input_mapping = uclogic_input_mapping, + .input_configured = uclogic_input_configured, +}; +module_hid_driver(uclogic_driver); + +MODULE_AUTHOR("Martin Rusko"); +MODULE_AUTHOR("Nikolai Kondrashov"); +MODULE_LICENSE("GPL"); |