From 0ddfef6c21e9944e1ed34e4cbb895e105b82359d Mon Sep 17 00:00:00 2001 From: Daniel Drake Date: Sat, 27 Oct 2007 00:10:32 +0100 Subject: [PATCH] Add uru4000 driver --- TODO | 1 - libfprint/Makefile.am | 3 +- libfprint/drivers/uru4000.c | 506 ++++++++++++++++++++++++++++++++++++ 3 files changed, 508 insertions(+), 2 deletions(-) create mode 100644 libfprint/drivers/uru4000.c diff --git a/TODO b/TODO index d33bc88..586e45a 100644 --- a/TODO +++ b/TODO @@ -11,7 +11,6 @@ Sunplus 895 driver AES3501 driver AES4000 driver ID Mouse driver -DigitalPersona U.are.U 4000/4000B driver Support for 2nd generation MS devices Support for 2nd generation UPEK devices diff --git a/libfprint/Makefile.am b/libfprint/Makefile.am index 5ffb8d6..bdec471 100644 --- a/libfprint/Makefile.am +++ b/libfprint/Makefile.am @@ -1,8 +1,9 @@ lib_LTLIBRARIES = libfprint.la UPEKTS_SRC = drivers/upekts.c +URU4000_SRC = drivers/uru4000.c -DRIVER_SRC = $(UPEKTS_SRC) +DRIVER_SRC = $(UPEKTS_SRC) $(URU4000_SRC) libfprint_la_CFLAGS = -fvisibility=hidden $(LIBUSB_CFLAGS) $(GLIB_CFLAGS) $(AM_CFLAGS) libfprint_la_LDFLAGS = -version-info @lt_major@:@lt_revision@:@lt_age@ diff --git a/libfprint/drivers/uru4000.c b/libfprint/drivers/uru4000.c new file mode 100644 index 0000000..8a32497 --- /dev/null +++ b/libfprint/drivers/uru4000.c @@ -0,0 +1,506 @@ +/* + * Digital Persona U.are.U 4000/4000B driver for libfprint + * Copyright (C) 2007 Daniel Drake + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define FP_COMPONENT "uru4000" + +#include +#include +#include + +#include + +#include + +#define EP_INTR (1 | USB_ENDPOINT_IN) +#define EP_DATA (2 | USB_ENDPOINT_IN) +#define USB_RQ 0x04 +#define CTRL_IN (USB_TYPE_VENDOR | USB_ENDPOINT_IN) +#define CTRL_OUT (USB_TYPE_VENDOR | USB_ENDPOINT_OUT) +#define CTRL_TIMEOUT 5000 +#define BULK_TIMEOUT 5000 +#define DATABLK1_RQLEN 0x10000 +#define DATABLK2_RQLEN 0xb340 +#define DATABLK2_EXPECT 0xb1c0 +#define CAPTURE_HDRLEN 64 +#define IRQ_LENGTH 64 +#define IMG_WIDTH 384 +#define IMG_HEIGHT 289 + +enum { + IRQDATA_SCANPWR_ON = 0x56aa, + IRQDATA_FINGER_ON = 0x0101, + IRQDATA_FINGER_OFF = 0x0200, +}; + +enum { + REG_HWSTAT = 0x07, + REG_MODE = 0x4e, +}; + +enum { + MODE_INIT = 0x00, + MODE_AWAIT_FINGER_ON = 0x10, + MODE_AWAIT_FINGER_OFF = 0x12, + MODE_CAPTURE = 0x20, + MODE_SHUT_UP = 0x30, + MODE_READY = 0x80, +}; + +enum { + MS_KBD, + MS_INTELLIMOUSE, + MS_STANDALONE, + MS_STANDALONE_V2, + DP_URU4000, + DP_URU4000B, +}; + +static const struct uru4k_dev_profile { + const char *name; + uint16_t firmware_start; + uint16_t fw_enc_offset; +} uru4k_dev_info[] = { + [MS_KBD] = { + .name = "Microsoft Keyboard with Fingerprint Reader", + .firmware_start = 0x100, + .fw_enc_offset = 0x42b, + }, + [MS_INTELLIMOUSE] = { + .name = "Microsoft Wireless IntelliMouse with Fingerprint Reader", + .firmware_start = 0x100, + .fw_enc_offset = 0x42b, + }, + [MS_STANDALONE] = { + .name = "Microsoft Fingerprint Reader", + .firmware_start = 0x100, + .fw_enc_offset = 0x42b, + }, + [DP_URU4000B] = { + .name = "Digital Persona U.are.U 4000B", + .firmware_start = 0x100, + .fw_enc_offset = 0x42b, + }, +}; + +struct uru4k_dev { + uint8_t interface; +}; + +static int get_hwstat(struct fp_img_dev *dev, unsigned char *data) +{ + int r; + + /* The windows driver uses a request of 0x0c here. We use 0x04 to be + * consistent with every other command we know about. */ + r = usb_control_msg(dev->udev, CTRL_IN, USB_RQ, REG_HWSTAT, 0, + data, 1, CTRL_TIMEOUT); + if (r < 0) { + fp_err("error %d", r); + return r; + } else if (r < 1) { + fp_err("read too short (%d)", r); + return -EIO; + } + + fp_dbg("val=%02x", *data); + return 0; +} + +static int set_hwstat(struct fp_img_dev *dev, unsigned char data) +{ + int r; + fp_dbg("val=%02x", data); + + r = usb_control_msg(dev->udev, CTRL_OUT, USB_RQ, REG_HWSTAT, 0, + &data, 1, CTRL_TIMEOUT); + if (r < 0) { + fp_err("error %d", r); + return r; + } else if (r < 1) { + fp_err("read too short (%d)", r); + return -EIO; + } + + return 0; +} + +static int set_mode(struct fp_img_dev *dev, unsigned char mode) +{ + int r; + + fp_dbg("%02x", mode); + r = usb_control_msg(dev->udev, CTRL_OUT, USB_RQ, REG_MODE, 0, &mode, 1, + CTRL_TIMEOUT); + if (r < 0) { + fp_err("error %d", r); + return r; + } else if (r < 1) { + fp_err("write too short (%d)", r); + return -EIO; + } + + return 0; +} + +static int get_irq(struct fp_img_dev *dev, unsigned char *buf, int timeout) +{ + uint16_t type; + int r; + int infinite_timeout = 0; + + if (timeout == 0) + infinite_timeout = 1; + + /* Darwin and Linux behave inconsistently with regard to infinite timeouts. + * Linux accepts a timeout value of 0 as infinite timeout, whereas darwin + * returns -ETIMEDOUT immediately when a 0 timeout is used. We use a + * looping hack until libusb is fixed. + * See http://thread.gmane.org/gmane.comp.lib.libusb.devel.general/1315 */ + +retry: + r = usb_interrupt_read(dev->udev, EP_INTR, buf, IRQ_LENGTH, 1000); + if (r == -ETIMEDOUT && + ((!infinite_timeout && timeout > 0) || infinite_timeout)) { + fp_dbg("timeout, retry"); + timeout--; + goto retry; + } + + if (r < 0) { + fp_err("interrupt read failed, error %d", r); + return r; + } else if (r < IRQ_LENGTH) { + fp_err("received %d byte IRQ!?", r); + return -EIO; + } + + type = be16_to_cpu(*((uint16_t *) buf)); + fp_dbg("irq type %04x", type); + + return 0; +} + +static int get_irq_with_type(struct fp_img_dev *dev, uint16_t irqtype, + int timeout) +{ + uint16_t hdr; + int discarded = -1; + unsigned char irqbuf[IRQ_LENGTH]; + + fp_dbg("type=%04x", irqtype); + + /* Sometimes we get an interrupt from a previous 'session' indicating + * finger-on-sensor, we ignore this and wait for the real interrupt */ + do { + int r; + discarded++; + + r = get_irq(dev, irqbuf, timeout); + if (r < 0) + return r; + hdr = be16_to_cpu(*((uint16_t *) irqbuf)); + } while (hdr != irqtype); + + if (discarded > 0) + fp_dbg("discarded %d interrupts", discarded); + + return 0; +} + +static int await_finger_on(struct fp_img_dev *dev) +{ + int r; + + r = set_mode(dev, MODE_AWAIT_FINGER_ON); + if (r < 0) + return r; + + return get_irq_with_type(dev, IRQDATA_FINGER_ON, 0); +} + +static int await_finger_off(struct fp_img_dev *dev) +{ + int r; + + r = set_mode(dev, MODE_AWAIT_FINGER_OFF); + if (r < 0) + return r; + + return get_irq_with_type(dev, IRQDATA_FINGER_OFF, 0); +} + +static int capture(struct fp_img_dev *dev, gboolean unconditional, + struct fp_img **ret) +{ + int r; + struct fp_img *img; + size_t image_size = DATABLK1_RQLEN + DATABLK2_EXPECT - CAPTURE_HDRLEN; + + r = set_mode(dev, MODE_CAPTURE); + if (r < 0) + return r; + + /* The image is split up into 2 blocks over 2 USB transactions, which are + * joined contiguously. The image is prepended by a 64 byte header which + * we completely ignore. + * + * We mimic the windows driver behaviour by requesting 0xb340 bytes in the + * 2nd request, but we only expect 0xb1c0 in response. However, our buffers + * must be set up on the offchance that we receive as much data as we + * asked for. */ + + img = fpi_img_new(DATABLK1_RQLEN + DATABLK2_RQLEN); + + r = usb_bulk_read(dev->udev, EP_DATA, img->data, DATABLK1_RQLEN, + BULK_TIMEOUT); + if (r < 0) { + fp_err("part 1 capture failed, error %d", r); + goto err; + } else if (r < DATABLK1_RQLEN) { + fp_err("part 1 capture too short (%d)", r); + r = -EIO; + goto err; + } + + r = usb_bulk_read(dev->udev, EP_DATA, img->data + DATABLK1_RQLEN, + DATABLK2_RQLEN, BULK_TIMEOUT); + if (r < 0) { + fp_err("part 2 capture failed, error %d", r); + goto err; + } else if (r != DATABLK2_EXPECT) { + fp_err("unexpected part 2 capture size (%d)", r); + r = -EIO; + goto err; + } + + /* remove header and shrink allocation */ + g_memmove(img->data, img->data + CAPTURE_HDRLEN, image_size); + img = fpi_img_resize(img, image_size); + img->width = IMG_WIDTH; + img->height = IMG_HEIGHT; + + *ret = img; + return 0; +err: + g_free(img); + return r; +} + +static int do_init(struct fp_img_dev *dev) +{ + unsigned char status; + unsigned char tmp; + int i; + int r; + + r = get_hwstat(dev, &status); + if (r < 0) + return r; + + /* After closing an app and setting hwstat to 0x80, my ms keyboard + * gets in a confused state and returns hwstat 0x85. On next app run, + * we don't get the 56aa interrupt. This is the best way I've found to + * fix it: mess around with hwstat until it starts returning more + * recognisable values. This doesn't happen on my other devices: + * uru4000, uru4000b, ms fp rdr v2 + * The windows driver copes with this OK, but then again it uploads + * firmware right after reading the 0x85 hwstat, allowing some time + * to pass before it attempts to tweak hwstat again... */ + if ((status & 0x84) == 0x84) { + fp_dbg("rebooting device power"); + r = set_hwstat(dev, status & 0xf); + if (r < 0) + return r; + + for (i = 0; i < 100; i++) { + r = get_hwstat(dev, &status); + if (r < 0) + return r; + if (status & 0x1) + break; + usleep(10000); + } + if ((status & 0x1) == 0) { + fp_err("could not reboot device power"); + return -EIO; + } + } + + if ((status & 0x80) == 0) { + status |= 0x80; + r = set_hwstat(dev, status); + if (r < 0) + return r; + } + + /* FIXME fix firmware (disable encryption) */ + + /* Power up device and wait for interrupt notification */ + /* The combination of both modifying firmware *and* doing C-R auth on + * my ms fp v2 device causes us not to get to get the 56aa interrupt and + * for the hwstat write not to take effect. We loop a few times, + * authenticating each time, until the device wakes up. */ + for (i = 0; i < 100; i++) { /* max 1 sec */ + r = set_hwstat(dev, status & 0xf); + if (r < 0) + return r; + + r = get_hwstat(dev, &tmp); + if (r < 0) + return r; + + if ((tmp & 0x80) == 0) + break; + + usleep(10000); + + /* FIXME do C-R auth for v2 devices */ + } + + if (tmp & 0x80) { + fp_err("could not power up device"); + return -EIO; + } + + r = get_irq_with_type(dev, IRQDATA_SCANPWR_ON, 5); + if (r < 0) + return r; + + return 0; +} + +static int dev_init(struct fp_img_dev *dev, unsigned long driver_data) +{ + struct usb_config_descriptor *config; + struct usb_interface *iface = NULL; + struct usb_interface_descriptor *iface_desc; + struct usb_endpoint_descriptor *ep; + struct uru4k_dev *urudev; + int i; + int r; + + /* Find fingerprint interface */ + config = usb_device(dev->udev)->config; + for (i = 0; i < config->bNumInterfaces; i++) { + struct usb_interface *cur_iface = &config->interface[i]; + + if (cur_iface->num_altsetting < 1) + continue; + + iface_desc = &cur_iface->altsetting[0]; + if (iface_desc->bInterfaceClass == 255 + && iface_desc->bInterfaceSubClass == 255 + && iface_desc->bInterfaceProtocol == 255) { + iface = cur_iface; + break; + } + } + + if (iface == NULL) { + fp_err("could not find interface"); + return -ENODEV; + } + + /* Find/check endpoints */ + + if (iface_desc->bNumEndpoints != 2) { + fp_err("found %d endpoints!?", iface_desc->bNumEndpoints); + return -ENODEV; + } + + ep = &iface_desc->endpoint[0]; + if (ep->bEndpointAddress != EP_INTR + || (ep->bmAttributes & USB_ENDPOINT_TYPE_MASK) != + USB_ENDPOINT_TYPE_INTERRUPT) { + fp_err("unrecognised interrupt endpoint"); + return -ENODEV; + } + + ep = &iface_desc->endpoint[1]; + if (ep->bEndpointAddress != EP_DATA + || (ep->bmAttributes & USB_ENDPOINT_TYPE_MASK) != + USB_ENDPOINT_TYPE_BULK) { + fp_err("unrecognised bulk endpoint"); + return -ENODEV; + } + + /* Device looks like a supported reader */ + + r = usb_claim_interface(dev->udev, iface_desc->bInterfaceNumber); + if (r < 0) { + fp_err("interface claim failed"); + return r; + } + + urudev = g_malloc0(sizeof(*urudev)); + urudev->interface = iface_desc->bInterfaceNumber; + dev->priv = urudev; + + r = do_init(dev); + if (r < 0) + goto err; + + return 0; +err: + usb_release_interface(dev->udev, iface_desc->bInterfaceNumber); + g_free(urudev); + return r; +} + +static void dev_exit(struct fp_img_dev *dev) +{ + struct uru4k_dev *urudev = dev->priv; + + set_mode(dev, MODE_INIT); + set_hwstat(dev, 0x80); + usb_release_interface(dev->udev, urudev->interface); + g_free(urudev); +} + +static const struct usb_id id_table[] = { + /* ms kbd with fp rdr */ + { .vendor = 0x045e, .product = 0x00bb, .driver_data = MS_KBD }, + + /* ms intellimouse with fp rdr */ + { .vendor = 0x045e, .product = 0x00bc, .driver_data = MS_INTELLIMOUSE }, + + /* ms fp rdr (standalone) */ + { .vendor = 0x045e, .product = 0x00bd, .driver_data = MS_STANDALONE }, + + /* dp uru4000b (standalone) */ + { .vendor = 0x05ba, .product = 0x000a, .driver_data = DP_URU4000B }, + + /* terminating entry */ + { 0, 0, 0, }, +}; + +struct fp_img_driver uru4000_driver = { + .driver = { + .name = FP_COMPONENT, + .full_name = "Digital Persona U.are.U 4000/4000B", + .id_table = id_table, + }, + .flags = FP_IMGDRV_SUPPORTS_UNCONDITIONAL_CAPTURE, + .init = dev_init, + .exit = dev_exit, + .await_finger_on = await_finger_on, + .await_finger_off = await_finger_off, + .capture = capture, +}; +