diff options
Diffstat (limited to 'drivers/hid/hid-letsketch.c')
-rw-r--r-- | drivers/hid/hid-letsketch.c | 322 |
1 files changed, 322 insertions, 0 deletions
diff --git a/drivers/hid/hid-letsketch.c b/drivers/hid/hid-letsketch.c new file mode 100644 index 000000000000..74d17cf518ba --- /dev/null +++ b/drivers/hid/hid-letsketch.c @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (c) 2021 Hans de Goede <hdegoede@redhat.com> + * + * Driver for the LetSketch / VSON WP9620N drawing tablet. + * This drawing tablet is also sold under other brand names such as Case U, + * presumably this driver will work for all of them. But it has only been + * tested with a LetSketch WP9620N model. + * + * These tablets also work without a special HID driver, but then only part + * of the active area works and both the pad and stylus buttons are hardwired + * to special key-combos. E.g. the 2 stylus buttons send right mouse clicks / + * resp. "e" key presses. + * + * This device has 4 USB interfaces: + * + * Interface 0 EP 0x81 bootclass mouse, rdesc len 18, report id 0x08, + * Application(ff00.0001) + * This interface sends raw event input reports in a custom format, but only + * after doing the special dance from letsketch_probe(). After enabling this + * interface the other 3 interfaces are disabled. + * + * Interface 1 EP 0x82 bootclass mouse, rdesc len 83, report id 0x0a, Tablet + * This interface sends absolute events for the pen, including pressure, + * but only for some part of the active area due to special "aspect ratio" + * correction and only half by default since it assumes it will be used + * with a phone in portraid mode, while using the tablet in landscape mode. + * Also stylus + pad button events are not reported here. + * + * Interface 2 EP 0x83 bootclass keybd, rdesc len 64, report id none, Std Kbd + * This interfaces send various hard-coded key-combos for the pad buttons + * and "e" keypresses for the 2nd stylus button + * + * Interface 3 EP 0x84 bootclass mouse, rdesc len 75, report id 0x01, Std Mouse + * This reports right-click mouse-button events for the 1st stylus button + */ +#include <linux/device.h> +#include <linux/input.h> +#include <linux/hid.h> +#include <linux/module.h> +#include <linux/timer.h> +#include <linux/usb.h> + +#include <asm/unaligned.h> + +#include "hid-ids.h" + +#define LETSKETCH_RAW_IF 0 + +#define LETSKETCH_RAW_DATA_LEN 12 +#define LETSKETCH_RAW_REPORT_ID 8 + +#define LETSKETCH_PAD_BUTTONS 5 + +#define LETSKETCH_INFO_STR_IDX_BEGIN 0xc8 +#define LETSKETCH_INFO_STR_IDX_END 0xca + +#define LETSKETCH_GET_STRING_RETRIES 5 + +struct letsketch_data { + struct hid_device *hdev; + struct input_dev *input_tablet; + struct input_dev *input_tablet_pad; + struct timer_list inrange_timer; +}; + +static int letsketch_open(struct input_dev *dev) +{ + struct letsketch_data *data = input_get_drvdata(dev); + + return hid_hw_open(data->hdev); +} + +static void letsketch_close(struct input_dev *dev) +{ + struct letsketch_data *data = input_get_drvdata(dev); + + hid_hw_close(data->hdev); +} + +static struct input_dev *letsketch_alloc_input_dev(struct letsketch_data *data) +{ + struct input_dev *input; + + input = devm_input_allocate_device(&data->hdev->dev); + if (!input) + return NULL; + + input->id.bustype = data->hdev->bus; + input->id.vendor = data->hdev->vendor; + input->id.product = data->hdev->product; + input->id.version = data->hdev->bus; + input->phys = data->hdev->phys; + input->uniq = data->hdev->uniq; + input->open = letsketch_open; + input->close = letsketch_close; + + input_set_drvdata(input, data); + + return input; +} + +static int letsketch_setup_input_tablet(struct letsketch_data *data) +{ + struct input_dev *input; + + input = letsketch_alloc_input_dev(data); + if (!input) + return -ENOMEM; + + input_set_abs_params(input, ABS_X, 0, 50800, 0, 0); + input_set_abs_params(input, ABS_Y, 0, 31750, 0, 0); + input_set_abs_params(input, ABS_PRESSURE, 0, 8192, 0, 0); + input_abs_set_res(input, ABS_X, 240); + input_abs_set_res(input, ABS_Y, 225); + input_set_capability(input, EV_KEY, BTN_TOUCH); + input_set_capability(input, EV_KEY, BTN_TOOL_PEN); + input_set_capability(input, EV_KEY, BTN_STYLUS); + input_set_capability(input, EV_KEY, BTN_STYLUS2); + + /* All known brands selling this tablet use WP9620[N] as model name */ + input->name = "WP9620 Tablet"; + + data->input_tablet = input; + + return input_register_device(data->input_tablet); +} + +static int letsketch_setup_input_tablet_pad(struct letsketch_data *data) +{ + struct input_dev *input; + int i; + + input = letsketch_alloc_input_dev(data); + if (!input) + return -ENOMEM; + + for (i = 0; i < LETSKETCH_PAD_BUTTONS; i++) + input_set_capability(input, EV_KEY, BTN_0 + i); + + /* + * These are never send on the pad input_dev, but must be set + * on the Pad to make udev / libwacom happy. + */ + input_set_abs_params(input, ABS_X, 0, 1, 0, 0); + input_set_abs_params(input, ABS_Y, 0, 1, 0, 0); + input_set_capability(input, EV_KEY, BTN_STYLUS); + + input->name = "WP9620 Pad"; + + data->input_tablet_pad = input; + + return input_register_device(data->input_tablet_pad); +} + +static void letsketch_inrange_timeout(struct timer_list *t) +{ + struct letsketch_data *data = from_timer(data, t, inrange_timer); + struct input_dev *input = data->input_tablet; + + input_report_key(input, BTN_TOOL_PEN, 0); + input_sync(input); +} + +static int letsketch_raw_event(struct hid_device *hdev, + struct hid_report *report, + u8 *raw_data, int size) +{ + struct letsketch_data *data = hid_get_drvdata(hdev); + struct input_dev *input; + int i; + + if (size != LETSKETCH_RAW_DATA_LEN || raw_data[0] != LETSKETCH_RAW_REPORT_ID) + return 0; + + switch (raw_data[1] & 0xf0) { + case 0x80: /* Pen data */ + input = data->input_tablet; + input_report_key(input, BTN_TOOL_PEN, 1); + input_report_key(input, BTN_TOUCH, raw_data[1] & 0x01); + input_report_key(input, BTN_STYLUS, raw_data[1] & 0x02); + input_report_key(input, BTN_STYLUS2, raw_data[1] & 0x04); + input_report_abs(input, ABS_X, + get_unaligned_le16(raw_data + 2)); + input_report_abs(input, ABS_Y, + get_unaligned_le16(raw_data + 4)); + input_report_abs(input, ABS_PRESSURE, + get_unaligned_le16(raw_data + 6)); + /* + * There is no out of range event, so use a timer for this + * when in range we get an event approx. every 8 ms. + */ + mod_timer(&data->inrange_timer, jiffies + msecs_to_jiffies(100)); + break; + case 0xe0: /* Pad data */ + input = data->input_tablet_pad; + for (i = 0; i < LETSKETCH_PAD_BUTTONS; i++) + input_report_key(input, BTN_0 + i, raw_data[4] == (i + 1)); + break; + default: + hid_warn(data->hdev, "Warning unknown data header: 0x%02x\n", + raw_data[0]); + return 0; + } + + input_sync(input); + return 0; +} + +/* + * The tablets magic handshake to put it in raw mode relies on getting + * string descriptors. But the firmware is buggy and does not like it if + * we do this too fast. Even if we go slow sometimes the usb_string() call + * fails. Ignore errors and retry it a couple of times if necessary. + */ +static int letsketch_get_string(struct usb_device *udev, int index, char *buf, int size) +{ + int i, ret; + + for (i = 0; i < LETSKETCH_GET_STRING_RETRIES; i++) { + usleep_range(5000, 7000); + ret = usb_string(udev, index, buf, size); + if (ret > 0) + return 0; + } + + dev_err(&udev->dev, "Max retries (%d) exceeded reading string descriptor %d\n", + LETSKETCH_GET_STRING_RETRIES, index); + return ret ? ret : -EIO; +} + +static int letsketch_probe(struct hid_device *hdev, const struct hid_device_id *id) +{ + struct device *dev = &hdev->dev; + struct letsketch_data *data; + struct usb_interface *intf; + struct usb_device *udev; + char buf[256]; + int i, ret; + + if (!hid_is_using_ll_driver(hdev, &usb_hid_driver)) + return -ENODEV; + + intf = to_usb_interface(hdev->dev.parent); + if (intf->altsetting->desc.bInterfaceNumber != LETSKETCH_RAW_IF) + return -ENODEV; /* Ignore the other interfaces */ + + udev = interface_to_usbdev(intf); + + /* + * Instead of using a set-feature request, or even a custom USB ctrl + * message the tablet needs this elaborate magic reading of USB + * string descriptors to kick it into raw mode. This is what the + * Windows drivers are seen doing in an USB trace under Windows. + */ + for (i = LETSKETCH_INFO_STR_IDX_BEGIN; i <= LETSKETCH_INFO_STR_IDX_END; i++) { + ret = letsketch_get_string(udev, i, buf, sizeof(buf)); + if (ret) + return ret; + + hid_info(hdev, "Device info: %s\n", buf); + } + + for (i = 1; i <= 250; i++) { + ret = letsketch_get_string(udev, i, buf, sizeof(buf)); + if (ret) + return ret; + } + + ret = letsketch_get_string(udev, 0x64, buf, sizeof(buf)); + if (ret) + return ret; + + ret = letsketch_get_string(udev, LETSKETCH_INFO_STR_IDX_BEGIN, buf, sizeof(buf)); + if (ret) + return ret; + + /* + * The tablet should be in raw mode now, end with a final delay before + * doing further IO to the device. + */ + usleep_range(5000, 7000); + + ret = hid_parse(hdev); + if (ret) + return ret; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->hdev = hdev; + timer_setup(&data->inrange_timer, letsketch_inrange_timeout, 0); + hid_set_drvdata(hdev, data); + + ret = letsketch_setup_input_tablet(data); + if (ret) + return ret; + + ret = letsketch_setup_input_tablet_pad(data); + if (ret) + return ret; + + return hid_hw_start(hdev, HID_CONNECT_HIDRAW); +} + +static const struct hid_device_id letsketch_devices[] = { + { HID_USB_DEVICE(USB_VENDOR_ID_LETSKETCH, USB_DEVICE_ID_WP9620N) }, + { } +}; +MODULE_DEVICE_TABLE(hid, letsketch_devices); + +static struct hid_driver letsketch_driver = { + .name = "letsketch", + .id_table = letsketch_devices, + .probe = letsketch_probe, + .raw_event = letsketch_raw_event, +}; +module_hid_driver(letsketch_driver); + +MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>"); +MODULE_LICENSE("GPL"); |