Benefit from the previously introduced actuator infrastructure and introduce the first actuator device driver. This driver controls Linak table moving actuators over USB2LIN usb cable.
Signed-off-by: Jiri Pirko <[email protected]> --- drivers/actuator/Kconfig | 11 ++ drivers/actuator/Makefile | 1 + drivers/actuator/usb2lin.c | 433 +++++++++++++++++++++++++++++++++++++++++++++ drivers/hid/hid-core.c | 1 + drivers/hid/hid-ids.h | 3 + 5 files changed, 449 insertions(+) create mode 100644 drivers/actuator/usb2lin.c diff --git a/drivers/actuator/Kconfig b/drivers/actuator/Kconfig index 6deac93..43150d8 100644 --- a/drivers/actuator/Kconfig +++ b/drivers/actuator/Kconfig @@ -12,4 +12,15 @@ menuconfig ACTUATOR if ACTUATOR +menuconfig ACTUATOR_USB2LIN + tristate "Linak USB2LIN cable support" + depends on USB + ---help--- + This provides a support for Linak USB2LIN devices. + + If you want this support, you should say Y here. + + This support can also be built as a module. If so, the module + will be called usb2lin. + endif diff --git a/drivers/actuator/Makefile b/drivers/actuator/Makefile index c45af36..743dc18 100644 --- a/drivers/actuator/Makefile +++ b/drivers/actuator/Makefile @@ -1 +1,2 @@ obj-$(CONFIG_ACTUATOR) += actuator.o +obj-$(CONFIG_ACTUATOR_USB2LIN) += usb2lin.o diff --git a/drivers/actuator/usb2lin.c b/drivers/actuator/usb2lin.c new file mode 100644 index 0000000..a39a994 --- /dev/null +++ b/drivers/actuator/usb2lin.c @@ -0,0 +1,433 @@ +/* + * drivers/actuator/usb2lin.c - Linak USB2LIN cable support + * Copyright (c) 2016-2017 Jiri Pirko <[email protected]> + * + * 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/kernel.h> +#include <linux/module.h> +#include <linux/types.h> +#include <linux/slab.h> +#include <linux/mutex.h> +#include <linux/workqueue.h> +#include <linux/usb.h> +#include <linux/actuator.h> +#include "../../drivers/hid/hid-ids.h" + +static const char u2l_driver_name[] = "usb2lin"; + +static const struct usb_device_id u2l_id_table[] = { + { USB_DEVICE(USB_VENDOR_ID_LINAK, USB_DEVICE_ID_LINAK_USB2LIN) }, + { }, +}; + +struct u2l_status { + __le16 header; + __le16 move_status; +#define U2L_HEIGHT_MULTIPLIER 100 + __le16 height; +#define U2L_STATUS_MOVE_FLAGS_BLOCKED BIT(1) +#define U2L_STATUS_MOVE_FLAGS_DIR BIT(7) + u8 move_flags; + u8 move_magic; /* not 0 when moving :O */ + u8 unknown1[12]; +#define U2L_STATUS_TARGET_HEIGHT_MAGIC_STOPPED 0x8001 + __le16 target_height; + u8 unknown2[42]; +} __packed; + +struct u2l_move { + u8 header; +#define U2L_MOVE_TARGET_HEIGHT_MAGIC_UP 0x8000 +#define U2L_MOVE_TARGET_HEIGHT_MAGIC_DOWN 0x7fff + __le16 target_height[4]; /* unaligned access */ + u8 pad[59]; /* zero padding */ +} __packed; + +struct u2l_init { + __le32 header; + u8 pad[60]; /* zero padding */ +} __packed; + +struct u2l { + struct mutex lock; + struct actuator *act; + struct usb_device *udev; + struct u2l_status last_status; + struct u2l_init init; + struct delayed_work dw; + struct { + bool in_progress; + bool started; + unsigned long start_jiffies; + u16 target_height; + } move; + union { + struct u2l_move status; + struct u2l_move move; + } msg; +}; + +struct u2l_usbmsg_info { + u8 request; + u8 requesttype; + u16 value; +}; + +enum u2l_usbmsg_type { + U2L_USBMSG_HELLO, + U2L_USBMSG_INIT, + U2L_USBMSG_STATUS, + U2L_USBMSG_MOVE, +}; + +#define U2L_USBMSG_REQUEST_TYPE_IN 0xa1 +#define U2L_USBMSG_REQUEST_TYPE_OUT 0x21 + +static const struct u2l_usbmsg_info u2l_ubsmsg_infos[] = { + [U2L_USBMSG_HELLO] = { + .request = 0x0a, + .requesttype = U2L_USBMSG_REQUEST_TYPE_OUT, + }, + [U2L_USBMSG_INIT] = { + .request = 0x09, + .requesttype = U2L_USBMSG_REQUEST_TYPE_OUT, + .value = 0x0303, + }, + [U2L_USBMSG_STATUS] = { + .request = 0x01, + .requesttype = U2L_USBMSG_REQUEST_TYPE_IN, + .value = 0x0304, + }, + [U2L_USBMSG_MOVE] = { + .request = 0x09, + .requesttype = U2L_USBMSG_REQUEST_TYPE_OUT, + .value = 0x0305, + }, +}; + +#define U2L_USB_CONTROL_MSG_TIMEOUT 300 + +static int u2l_usb_control_msg(struct u2l *u2l, enum u2l_usbmsg_type type, + void *data, u16 size) +{ + const struct u2l_usbmsg_info *info = &u2l_ubsmsg_infos[type]; + unsigned int pipe; + int err; + + pipe = info->requesttype == U2L_USBMSG_REQUEST_TYPE_IN ? + usb_rcvctrlpipe(u2l->udev, 0) : + usb_sndctrlpipe(u2l->udev, 0); + + err = usb_control_msg(u2l->udev, pipe, info->request, info->requesttype, + info->value, 0, data, size, + U2L_USB_CONTROL_MSG_TIMEOUT); + return err < 0 ? err : 0; +} + +static int u2l_cmd_hello(struct u2l *u2l) +{ + return u2l_usb_control_msg(u2l, U2L_USBMSG_HELLO, NULL, 0); +} + +#define U2L_USBMSG_INIT_HEADER_MAGIC 0xfb000403 + +static int u2l_cmd_init(struct u2l *u2l) +{ + u2l->init.header = cpu_to_le32(U2L_USBMSG_INIT_HEADER_MAGIC); + return u2l_usb_control_msg(u2l, U2L_USBMSG_INIT, + &u2l->init, sizeof(u2l->init)); +} + +static int u2l_cmd_status(struct u2l *u2l, struct u2l_status *status) +{ + int err; + + err = u2l_usb_control_msg(u2l, U2L_USBMSG_STATUS, + &u2l->msg.status, sizeof(*status)); + if (err) + return err; + memcpy(status, &u2l->msg.status, sizeof(*status)); + dev_dbg(&u2l->udev->dev, "move_status %04x, height %04x (%u), move_flags %02x (%s%s), move_magic %02x, target_height %04x (%u)\n", + status->move_status, status->height, status->height, + status->move_flags, + status->move_flags & U2L_STATUS_MOVE_FLAGS_BLOCKED ? + "blocked " : "", + status->move_flags & U2L_STATUS_MOVE_FLAGS_DIR ? + "down" : "up", + status->move_magic, + status->target_height, status->target_height); + return 0; +} + +static void u2l_cmd_last_status_save(struct u2l *u2l, struct u2l_status *status) +{ + memcpy(&u2l->last_status, status, sizeof(u2l->last_status)); +} + +#define U2L_USBMSG_MOVE_HEADER_MAGIC 0x05 + +static int u2l_cmd_move(struct u2l *u2l, u16 target_height) +{ + __le16 tmp; + int i; + + u2l->msg.move.header = U2L_USBMSG_MOVE_HEADER_MAGIC; + tmp = cpu_to_le16(target_height); + for (i = 0; i < 4; i++) + memcpy(&u2l->msg.move.target_height[i], &tmp, sizeof(tmp)); + + return u2l_usb_control_msg(u2l, U2L_USBMSG_MOVE, + &u2l->msg.move, sizeof(u2l->move)); +} + +static void u2l_actuator_status_fill(struct u2l *u2l, + struct actuator_status *act_status, + struct u2l_status *status) +{ + act_status->value = le16_to_cpu(status->height) * U2L_HEIGHT_MULTIPLIER; + if (!status->move_magic) { + act_status->move_state = ACTUATOR_MOVE_STATE_STOPPED; + } else { + act_status->move_state = + status->move_flags & U2L_STATUS_MOVE_FLAGS_DIR ? + ACTUATOR_MOVE_STATE_NEGATIVE : + ACTUATOR_MOVE_STATE_POSITIVE; + } +} + +static int u2l_actuator_status(struct actuator *act, + struct actuator_status *act_status) +{ + struct u2l *u2l = actuator_priv(act); + struct u2l_status status; + int err; + + mutex_lock(&u2l->lock); + err = u2l_cmd_status(u2l, &status); + mutex_unlock(&u2l->lock); + if (err) + return err; + u2l_actuator_status_fill(u2l, act_status, &status); + return 0; +} + +#define U2L_MOVE_START_DELAY 500 /* ms */ + +static void u2l_check_move(struct u2l *u2l, struct u2l_status *status) +{ + if (!u2l->move.in_progress) + return; + + if (!u2l->move.started) { + if (status->move_magic) + u2l->move.started = true; + else if (jiffies_to_msecs(jiffies - u2l->move.start_jiffies) > + U2L_MOVE_START_DELAY) { + goto finish; /* Move start timed-out */ + } + } else if (!status->move_magic) { + goto finish; /* We are at the destination or limit */ + } + + u2l_cmd_move(u2l, u2l->move.target_height); + return; +finish: + u2l->move.in_progress = false; +} + +static void u2l_check_notify(struct u2l *u2l, struct u2l_status *status) +{ + struct actuator_status act_status; + + if (!!status->move_magic != !!u2l->last_status.move_magic) { + u2l_actuator_status_fill(u2l, &act_status, status); + actuator_notify_status(u2l->act, &act_status); + } +} + +#define U2L_WORK_DELAY 100 /* ms */ + +static void u2l_schedule_work(struct u2l *u2l) +{ + schedule_delayed_work(&u2l->dw, U2L_WORK_DELAY); +} + +static void u2l_work(struct work_struct *work) +{ + struct u2l_status status; + struct u2l *u2l; + int err; + + u2l = container_of(work, struct u2l, dw.work); + + mutex_lock(&u2l->lock); + err = u2l_cmd_status(u2l, &status); + if (err) + goto unlock; + u2l_check_move(u2l, &status); + u2l_check_notify(u2l, &status); + u2l_cmd_last_status_save(u2l, &status); + u2l_schedule_work(u2l); +unlock: + mutex_unlock(&u2l->lock); +} + +static int u2l_actuator_move(struct actuator *act, u32 value) +{ + struct u2l *u2l = actuator_priv(act); + int err; + + value /= U2L_HEIGHT_MULTIPLIER; + if (value < 0 || value > 0xFFFF) + return -EINVAL; + + mutex_lock(&u2l->lock); + + u2l->move.in_progress = true; + u2l->move.started = false; + u2l->move.start_jiffies = jiffies; + u2l->move.target_height = value; + + /* Do move cmd twice, first one would wake up the control in case + * it sleeps. Unfortunately there is no known way to find out. + */ + err = u2l_cmd_move(u2l, u2l->move.target_height); + if (err) + goto unlock; + err = u2l_cmd_move(u2l, u2l->move.target_height); + if (err) + goto unlock; + +unlock: + mutex_unlock(&u2l->lock); + return err; +} + +static int u2l_actuator_stop(struct actuator *act) +{ + struct u2l *u2l = actuator_priv(act); + struct u2l_status status; + u16 wakeup_target_height; + int err; + + mutex_lock(&u2l->lock); + + err = u2l_cmd_status(u2l, &status); + if (err) + goto unlock; + if (le16_to_cpu(status.target_height) == + U2L_STATUS_TARGET_HEIGHT_MAGIC_STOPPED) + goto unlock; + + wakeup_target_height = + status.move_flags & U2L_STATUS_MOVE_FLAGS_DIR ? + U2L_MOVE_TARGET_HEIGHT_MAGIC_UP : + U2L_MOVE_TARGET_HEIGHT_MAGIC_DOWN; + + err = u2l_cmd_move(u2l, wakeup_target_height); + + u2l->move.in_progress = false; + +unlock: + mutex_unlock(&u2l->lock); + return err; +} + +static int u2l_init(struct u2l *u2l) +{ + struct u2l_status status; + u16 current_height; + int err; + + err = u2l_cmd_hello(u2l); + if (err) + return err; + + err = u2l_cmd_status(u2l, &status); + if (err) + return err; + current_height = le16_to_cpu(status.height); + u2l_cmd_last_status_save(u2l, &status); + + err = u2l_cmd_init(u2l); + if (err) + return err; + + return u2l_cmd_move(u2l, current_height); +} + +static const struct actuator_ops u2l_actuator_ops = { + .priv_size = sizeof(struct u2l), + .driver_name = u2l_driver_name, + .type = ACTUATOR_TYPE_LINEAR, + .units = ACTUATOR_UNITS_UM, + .status = u2l_actuator_status, + .move = u2l_actuator_move, + .stop = u2l_actuator_stop, +}; + +static int u2l_probe(struct usb_interface *interface, + const struct usb_device_id *id) +{ + struct usb_device *udev = interface_to_usbdev(interface); + struct actuator *act; + struct u2l *u2l; + int err; + + act = actuator_alloc(&u2l_actuator_ops); + if (!act) + return -ENOMEM; + u2l = actuator_priv(act); + u2l->act = act; + u2l->udev = udev; + mutex_init(&u2l->lock); + INIT_DELAYED_WORK(&u2l->dw, u2l_work); + + err = u2l_init(u2l); + if (err) { + dev_err(&u2l->udev->dev, "Initialization failed\n"); + goto err_init; + } + + err = actuator_register(act, &udev->dev, 0); + if (err) + goto err_actuator_register; + + usb_set_intfdata(interface, act); + u2l_schedule_work(u2l); + return 0; + +err_actuator_register: +err_init: + actuator_free(act); + return err; +} + +static void u2l_disconnect(struct usb_interface *interface) +{ + struct actuator *act = usb_get_intfdata(interface); + struct u2l *u2l = actuator_priv(act); + + cancel_delayed_work_sync(&u2l->dw); + actuator_unregister(act); + actuator_free(act); +} + +static struct usb_driver u2l_driver = { + .name = u2l_driver_name, + .probe = u2l_probe, + .disconnect = u2l_disconnect, + .id_table = u2l_id_table, +}; + +module_usb_driver(u2l_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Jiri Pirko <[email protected]>"); +MODULE_DESCRIPTION("Linak USB2LIN cable support"); +MODULE_DEVICE_TABLE(usb, u2l_id_table); diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c index 6fd01a6..3311a96 100644 --- a/drivers/hid/hid-core.c +++ b/drivers/hid/hid-core.c @@ -2740,6 +2740,7 @@ static const struct hid_device_id hid_ignore_list[] = { { HID_USB_DEVICE(USB_VENDOR_ID_LD, USB_DEVICE_ID_LD_MCT) }, { HID_USB_DEVICE(USB_VENDOR_ID_LD, USB_DEVICE_ID_LD_HYBRID) }, { HID_USB_DEVICE(USB_VENDOR_ID_LD, USB_DEVICE_ID_LD_HEATCONTROL) }, + { HID_USB_DEVICE(USB_VENDOR_ID_LINAK, USB_DEVICE_ID_LINAK_USB2LIN) }, { HID_USB_DEVICE(USB_VENDOR_ID_MADCATZ, USB_DEVICE_ID_MADCATZ_BEATPAD) }, { HID_USB_DEVICE(USB_VENDOR_ID_MCC, USB_DEVICE_ID_MCC_PMD1024LS) }, { HID_USB_DEVICE(USB_VENDOR_ID_MCC, USB_DEVICE_ID_MCC_PMD1208LS) }, diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 3d911bf..0650fe9 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -663,6 +663,9 @@ #define USB_DEVICE_ID_LG_MULTITOUCH 0x0064 #define USB_DEVICE_ID_LG_MELFAS_MT 0x6007 +#define USB_VENDOR_ID_LINAK 0x12d3 +#define USB_DEVICE_ID_LINAK_USB2LIN 0x0002 + #define USB_VENDOR_ID_LOGITECH 0x046d #define USB_DEVICE_ID_LOGITECH_AUDIOHUB 0x0a0e #define USB_DEVICE_ID_LOGITECH_T651 0xb00c -- 2.9.3

