/* * Validity VFS0050 driver for libfprint * Copyright (C) 2015-2016 Konstantin Semenov * * 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 "vfs0050" #include "drivers_api.h" #include "vfs0050.h" G_DEFINE_TYPE (FpDeviceVfs0050, fpi_device_vfs0050, FP_TYPE_IMAGE_DEVICE) /* USB functions */ /* Callback for async_write */ static void async_write_callback (FpiUsbTransfer *transfer, FpDevice *device, gpointer user_data, GError *error) { if (error) { fp_err ("USB write transfer: %s", error->message); fpi_ssm_mark_failed (transfer->ssm, error); return; } fpi_ssm_next_state (transfer->ssm); } /* Send data to EP1, the only out endpoint */ FP_GNUC_ACCESS (read_only, 3, 4) static void async_write (FpiSsm *ssm, FpDevice *dev, void *data, int len) { FpiUsbTransfer *transfer; transfer = fpi_usb_transfer_new (FP_DEVICE (dev)); fpi_usb_transfer_fill_bulk_full (transfer, 0x01, data, len, NULL); transfer->ssm = ssm; transfer->short_is_error = TRUE; fpi_usb_transfer_submit (transfer, VFS_USB_TIMEOUT, NULL, async_write_callback, NULL); } /* Callback for async_read */ static void async_read_callback (FpiUsbTransfer *transfer, FpDevice *device, gpointer user_data, GError *error) { int ep = transfer->endpoint; if (error) { fp_err ("USB read transfer on endpoint %d: %s", ep - 0x80, error->message); fpi_ssm_mark_failed (transfer->ssm, error); return; } fpi_ssm_next_state (transfer->ssm); } /* Receive data from the given ep and either discard or fill the given buffer */ static void async_read (FpiSsm *ssm, FpDevice *dev, int ep, void *data, int len) { FpiUsbTransfer *transfer; GDestroyNotify free_func = NULL; ep |= FPI_USB_ENDPOINT_IN; transfer = fpi_usb_transfer_new (FP_DEVICE (dev)); transfer->ssm = ssm; transfer->short_is_error = TRUE; if (data == NULL) { data = g_malloc0 (len); free_func = g_free; } /* 0x83 is the only interrupt endpoint */ if (ep == EP3_IN) fpi_usb_transfer_fill_interrupt_full (transfer, ep, data, len, free_func); else fpi_usb_transfer_fill_bulk_full (transfer, ep, data, len, free_func); fpi_usb_transfer_submit (transfer, VFS_USB_TIMEOUT, NULL, async_read_callback, NULL); } /* Callback for async_abort */ static void async_abort_callback (FpiUsbTransfer *transfer, FpDevice *device, gpointer user_data, GError *error) { int ep = transfer->endpoint; /* In normal case endpoint is empty */ if (g_error_matches (error, G_USB_DEVICE_ERROR, G_USB_DEVICE_ERROR_TIMED_OUT) || (g_strcmp0 (g_getenv ("FP_DEVICE_EMULATION"), "1") == 0 && transfer->actual_length == 0)) { g_clear_error (&error); fpi_ssm_next_state (transfer->ssm); return; } if (error) { fp_err ("USB write transfer: %s", error->message); fpi_ssm_mark_failed (transfer->ssm, error); return; } /* Don't stop process, only print warning */ fp_warn ("Endpoint %d had extra %zd bytes readable", ep - 0x80, transfer->actual_length); fpi_ssm_jump_to_state (transfer->ssm, fpi_ssm_get_cur_state (transfer->ssm)); } /* Receive data from the given ep; continues to the next state once no * more data is available. Otherwise the current state is repeated. */ static void async_abort (FpDevice *dev, FpiSsm *ssm, int ep) { FpiUsbTransfer *transfer; ep |= FPI_USB_ENDPOINT_IN; transfer = fpi_usb_transfer_new (dev); /* 0x83 is the only interrupt endpoint */ if (ep == EP3_IN) fpi_usb_transfer_fill_interrupt (transfer, ep, VFS_USB_BUFFER_SIZE); else fpi_usb_transfer_fill_bulk (transfer, ep, VFS_USB_BUFFER_SIZE); transfer->ssm = ssm; fpi_usb_transfer_submit (transfer, VFS_USB_ABORT_TIMEOUT, NULL, async_abort_callback, NULL); } /* Image processing functions */ /* Pixel getter for fpi_assemble_lines */ static unsigned char vfs0050_get_pixel (struct fpi_line_asmbl_ctx *ctx, GSList * line, unsigned int x) { return ((struct vfs_line *) line->data)->data[x]; } /* Deviation getter for fpi_assemble_lines */ static int vfs0050_get_difference (struct fpi_line_asmbl_ctx *ctx, GSList * line_list_1, GSList * line_list_2) { struct vfs_line *line1 = line_list_1->data; struct vfs_line *line2 = line_list_2->data; const int shift = (VFS_IMAGE_WIDTH - VFS_NEXT_LINE_WIDTH) / 2 - 1; int res = 0; for (int i = 0; i < VFS_NEXT_LINE_WIDTH; ++i) { int x = (int) line1->next_line_part[i] - (int) line2->data[shift + i]; res += x * x; } return res; } #define VFS_NOISE_THRESHOLD 40 /* Checks whether line is noise or not using hardware parameters */ static char is_noise (struct vfs_line *line) { int val1 = line->noise_hash_1; int val2 = line->noise_hash_2; if (val1 > VFS_NOISE_THRESHOLD && val1 < 256 - VFS_NOISE_THRESHOLD && val2 > VFS_NOISE_THRESHOLD && val2 < 256 - VFS_NOISE_THRESHOLD) return 1; return 0; } /* Parameters for fpi_assemble_lines */ static struct fpi_line_asmbl_ctx assembling_ctx = { .line_width = VFS_IMAGE_WIDTH, .max_height = VFS_MAX_HEIGHT, .resolution = 10, .median_filter_size = 25, .max_search_offset = 100, .get_deviation = vfs0050_get_difference, .get_pixel = vfs0050_get_pixel, }; /* Processes image before submitting */ static FpImage * prepare_image (FpDeviceVfs0050 *vdev) { int height = vdev->bytes / VFS_LINE_SIZE; /* Noise cleaning. IMHO, it works pretty well I've not detected cases when it doesn't work or cuts a part of the finger Noise arises at the end of scan when some water remains on the scanner */ while (height > 0) { if (!is_noise (vdev->lines_buffer + height - 1)) break; --height; } if (height > VFS_MAX_HEIGHT) height = VFS_MAX_HEIGHT; /* If image is not good enough */ if (height < VFS_IMAGE_WIDTH) return NULL; /* Building GSList */ GSList *lines = NULL; for (int i = height - 1; i >= 0; --i) lines = g_slist_prepend (lines, vdev->lines_buffer + i); /* Perform line assembling */ FpImage *img = fpi_assemble_lines (&assembling_ctx, lines, height); g_slist_free (lines); return img; } /* Processes and submits image after fingerprint received */ static void submit_image (FpDeviceVfs0050 *self) { FpImageDevice *idev = FP_IMAGE_DEVICE (self); /* We were not asked to submit image actually */ if (!self->active) return; FpImage *img = prepare_image (self); if (!img) fpi_image_device_retry_scan (idev, FP_DEVICE_RETRY_TOO_SHORT); else fpi_image_device_image_captured (idev, img); /* Finger not on the scanner */ fpi_image_device_report_finger_status (idev, FALSE); } /* Proto functions */ /* SSM loop for clear_ep2 */ static void clear_ep2_ssm (FpiSsm *ssm, FpDevice *dev) { char command04 = 0x04; switch (fpi_ssm_get_cur_state (ssm)) { case SUBSM1_COMMAND_04: async_write (ssm, dev, &command04, sizeof (command04)); break; case SUBSM1_RETURN_CODE: async_read (ssm, dev, 1, NULL, 2); break; case SUBSM1_ABORT_2: async_abort (dev, ssm, 2); break; default: fp_err ("Unknown SUBSM1 state"); fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); } } /* Send command to clear EP2 */ static void clear_ep2 (FpDevice *dev, FpiSsm *ssm) { FpiSsm *subsm = fpi_ssm_new (dev, clear_ep2_ssm, SUBSM1_STATES); fpi_ssm_start_subsm (ssm, subsm); } static void send_control_packet_ssm (FpiSsm *ssm, FpDevice *dev) { FpDeviceVfs0050 *self = FPI_DEVICE_VFS0050 (dev); switch (fpi_ssm_get_cur_state (ssm)) { case SUBSM2_SEND_CONTROL: async_write (ssm, dev, self->control_packet, VFS_CONTROL_PACKET_SIZE); break; case SUBSM2_RETURN_CODE: async_read (ssm, dev, 1, NULL, 2); break; case SUBSM2_SEND_COMMIT: /* next_receive_* packets could be sent only in pair */ if (self->control_packet == next_receive_1) { self->control_packet = next_receive_2; fpi_ssm_jump_to_state (ssm, SUBSM2_SEND_CONTROL); break; } /* commit_out in Windows differs in each commit, but I send the same each time */ async_write (ssm, dev, commit_out, sizeof (commit_out)); break; case SUBSM2_COMMIT_RESPONSE: async_read (ssm, dev, 1, NULL, VFS_COMMIT_RESPONSE_SIZE); break; case SUBSM2_READ_EMPTY_INTERRUPT: /* I don't know how to check result, it could be different * NOTE: I guess this comment relates to the above read. */ async_read (ssm, dev, 3, self->interrupt, VFS_INTERRUPT_SIZE); break; case SUBSM2_ABORT_3: /* Check that interrupt is empty */ if (memcmp (self->interrupt, empty_interrupt, VFS_INTERRUPT_SIZE)) { fp_err ("Unknown SUBSM2 state"); fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); break; } async_abort (dev, ssm, 3); break; case SUBSM2_CLEAR_EP2: /* After turn_on Windows doesn't clear EP2 */ if (self->control_packet != turn_on) clear_ep2 (dev, ssm); else fpi_ssm_next_state (ssm); break; default: fp_err ("Unknown SUBSM2 state"); fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); } } /* Send device state control packet */ static void send_control_packet (FpiSsm *ssm, FpDevice *dev) { FpiSsm *subsm = fpi_ssm_new (dev, send_control_packet_ssm, SUBSM2_STATES); fpi_ssm_start_subsm (ssm, subsm); } /* Clears all fprint data */ static void clear_data (FpDeviceVfs0050 *vdev) { g_free (vdev->lines_buffer); vdev->lines_buffer = NULL; vdev->memory = vdev->bytes = 0; } /* After receiving interrupt from EP3 */ static void interrupt_callback (FpiUsbTransfer *transfer, FpDevice *device, gpointer user_data, GError *error) { FpDeviceVfs0050 *self = FPI_DEVICE_VFS0050 (device); unsigned char *interrupt = transfer->buffer; /* we expect a cancellation error when the device is deactivating * go into the SSM_CLEAR_EP2 state in that case. */ if (!self->active && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { g_error_free (error); fpi_ssm_jump_to_state (transfer->ssm, SSM_CLEAR_EP2); return; } if (error) { fp_err ("USB read interrupt transfer: %s", error->message); fpi_ssm_mark_failed (transfer->ssm, error); return; } /* Standard interrupts */ if (memcmp (interrupt, interrupt1, VFS_INTERRUPT_SIZE) == 0 || memcmp (interrupt, interrupt2, VFS_INTERRUPT_SIZE) == 0 || memcmp (interrupt, interrupt3, VFS_INTERRUPT_SIZE) == 0) { /* Go to the next ssm stage */ fpi_ssm_next_state (transfer->ssm); return; } /* When finger is on the scanner before turn_on */ if (interrupt[0] == 0x01) { fp_warn ("Finger is already on the scanner"); /* Go to the next ssm stage */ fpi_ssm_next_state (transfer->ssm); return; } /* Unknown interrupt; abort the session */ fp_err ("Unknown interrupt '%02x:%02x:%02x:%02x:%02x'!", interrupt[0] & 0xff, interrupt[1] & 0xff, interrupt[2] & 0xff, interrupt[3] & 0xff, interrupt[4] & 0xff); /* Abort ssm */ fpi_ssm_mark_failed (transfer->ssm, fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); } static void receive_callback (FpiUsbTransfer *transfer, FpDevice *device, gpointer user_data, GError *error) { FpDeviceVfs0050 *self = FPI_DEVICE_VFS0050 (device); if (error && !g_error_matches (error, G_USB_DEVICE_ERROR, G_USB_DEVICE_ERROR_TIMED_OUT)) { fp_err ("USB read transfer: %s", error->message); fpi_ssm_mark_failed (transfer->ssm, error); return; } if (error) g_error_free (error); /* Capture is done when there is no more data to transfer or device timed out */ if (transfer->actual_length <= 0) { fpi_ssm_next_state (transfer->ssm); } else { self->bytes += transfer->actual_length; /* Try reading more data */ fpi_ssm_jump_to_state (transfer->ssm, fpi_ssm_get_cur_state (transfer->ssm)); } } /* Main SSM loop */ static void activate_ssm (FpiSsm *ssm, FpDevice *dev) { FpImageDevice *idev = FP_IMAGE_DEVICE (dev); FpDeviceVfs0050 *self = FPI_DEVICE_VFS0050 (dev); switch (fpi_ssm_get_cur_state (ssm)) { case SSM_INITIAL_ABORT_1: async_abort (dev, ssm, 1); break; case SSM_INITIAL_ABORT_2: async_abort (dev, ssm, 2); break; case SSM_INITIAL_ABORT_3: async_abort (dev, ssm, 3); break; case SSM_CLEAR_EP2: clear_ep2 (dev, ssm); break; case SSM_TURN_OFF: /* Set control_packet argument */ self->control_packet = turn_off; send_control_packet (ssm, dev); break; case SSM_TURN_ON: if (!self->active) { /* The only correct exit */ fpi_ssm_mark_completed (ssm); if (self->need_report) { fpi_image_device_deactivate_complete (idev, NULL); self->need_report = 0; } break; } /* Set control_packet argument */ self->control_packet = turn_on; send_control_packet (ssm, dev); break; case SSM_ASK_INTERRUPT: { FpiUsbTransfer *transfer; /* Activated, light must be blinking now */ /* If we first time here, report that activate completed */ if (self->need_report) { fpi_image_device_activate_complete (idev, NULL); self->need_report = 0; } /* Asynchronously enquire an interrupt */ transfer = fpi_usb_transfer_new (dev); transfer->ssm = ssm; transfer->short_is_error = TRUE; fpi_usb_transfer_fill_interrupt (transfer, 0x83, VFS_INTERRUPT_SIZE); fpi_usb_transfer_submit (transfer, 0, fpi_device_get_cancellable (dev), interrupt_callback, NULL); /* I've put it here to be sure that data is cleared */ clear_data (self); fpi_ssm_next_state (ssm); break; } case SSM_WAIT_INTERRUPT: /* TODO: This state is unused at this point. When we * are in this state, then a user cancellation will * cause deactivation. In that case, the USB transfer * is cancelled and the device is set to not be active. * We then go into SSM_CLEAR_EP2 based on the * cancellation. */ break; case SSM_RECEIVE_FINGER: { FpiUsbTransfer *transfer; if (self->memory == 0) { /* Initialize fingerprint buffer */ g_free (self->lines_buffer); self->memory = VFS_USB_BUFFER_SIZE; self->lines_buffer = g_malloc (self->memory); self->bytes = 0; /* Finger is on the scanner */ fpi_image_device_report_finger_status (idev, TRUE); } /* Increase buffer size while it's insufficient */ while (self->bytes + VFS_USB_BUFFER_SIZE > self->memory) { self->memory <<= 1; self->lines_buffer = (struct vfs_line *) g_realloc (self->lines_buffer, self->memory); } /* Receive chunk of data */ transfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_bulk_full (transfer, 0x82, (guint8 *) self->lines_buffer + self->bytes, VFS_USB_BUFFER_SIZE, NULL); transfer->ssm = ssm; fpi_usb_transfer_submit (transfer, VFS_USB_TIMEOUT, NULL, receive_callback, NULL); break; } case SSM_SUBMIT_IMAGE: submit_image (self); clear_data (self); /* Wait for probable vdev->active changing */ fpi_ssm_next_state_delayed (ssm, VFS_SSM_TIMEOUT); break; case SSM_NEXT_RECEIVE: if (!self->active) { /* It's the last scan */ fpi_ssm_jump_to_state (ssm, SSM_CLEAR_EP2); break; } /* Set control_packet argument */ self->control_packet = next_receive_1; send_control_packet (ssm, dev); break; case SSM_WAIT_ANOTHER_SCAN: /* Orange light is on now */ fpi_ssm_jump_to_state_delayed (ssm, SSM_TURN_ON, VFS_SSM_ORANGE_TIMEOUT); break; default: fp_err ("Unknown state"); fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); } } /* Driver functions */ /* Callback for dev_activate ssm */ static void dev_activate_callback (FpiSsm *ssm, FpDevice *dev, GError *error) { FpDeviceVfs0050 *self = FPI_DEVICE_VFS0050 (dev); self->ssm_active = 0; if (error) { g_warning ("Unhandled device activation error: %s", error->message); g_error_free (error); } } /* Activate device */ static void dev_activate (FpImageDevice *idev) { FpDeviceVfs0050 *self = FPI_DEVICE_VFS0050 (idev); /* Initialize flags */ self->active = 1; self->need_report = 1; self->ssm_active = 1; FpiSsm *ssm = fpi_ssm_new (FP_DEVICE (idev), activate_ssm, SSM_STATES); fpi_ssm_start (ssm, dev_activate_callback); } /* Deactivate device */ static void dev_deactivate (FpImageDevice *idev) { FpDeviceVfs0050 *self = FPI_DEVICE_VFS0050 (idev); if (!self->ssm_active) { fpi_image_device_deactivate_complete (idev, NULL); return; } /* Initialize flags */ self->active = 0; self->need_report = 1; } /* Callback for dev_open ssm */ static void dev_open_callback (FpiSsm *ssm, FpDevice *dev, GError *error) { /* Notify open complete */ fpi_image_device_open_complete (FP_IMAGE_DEVICE (dev), error); } /* Open device */ static void dev_open (FpImageDevice *idev) { GError *error = NULL; /* Claim usb interface */ if (!g_usb_device_claim_interface (fpi_device_get_usb_device (FP_DEVICE (idev)), 0, 0, &error)) { fpi_image_device_open_complete (idev, error); return; } /* Clearing previous device state */ FpiSsm *ssm = fpi_ssm_new (FP_DEVICE (idev), activate_ssm, SSM_STATES); fpi_ssm_start (ssm, dev_open_callback); } /* Close device */ static void dev_close (FpImageDevice *idev) { GError *error = NULL; FpDeviceVfs0050 *self = FPI_DEVICE_VFS0050 (idev); clear_data (self); /* Release usb interface */ g_usb_device_release_interface (fpi_device_get_usb_device (FP_DEVICE (idev)), 0, 0, &error); /* Notify close complete */ fpi_image_device_close_complete (idev, error); } /* Usb id table of device */ static const FpIdEntry id_table[] = { {.vid = 0x138a, .pid = 0x0050, }, {.vid = 0, .pid = 0, .driver_data = 0}, }; static void fpi_device_vfs0050_init (FpDeviceVfs0050 *self) { } static void fpi_device_vfs0050_class_init (FpDeviceVfs0050Class *klass) { FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass); FpImageDeviceClass *img_class = FP_IMAGE_DEVICE_CLASS (klass); dev_class->id = "vfs0050"; dev_class->full_name = "Validity VFS0050"; dev_class->type = FP_DEVICE_TYPE_USB; dev_class->id_table = id_table; dev_class->scan_type = FP_SCAN_TYPE_SWIPE; img_class->img_open = dev_open; img_class->img_close = dev_close; img_class->activate = dev_activate; img_class->deactivate = dev_deactivate; img_class->bz3_threshold = 24; img_class->img_width = VFS_IMAGE_WIDTH; img_class->img_height = -1; }