/* * Samsung USB FPC1020 driver for libfprint * * 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 "fpc1020_samsung" #include "drivers_api.h" #define FPC1020_EP_OUT (0x01 | FPI_USB_ENDPOINT_OUT) #define FPC1020_EP_IN (0x02 | FPI_USB_ENDPOINT_IN) #define FPC1020_IMG_W 160 #define FPC1020_IMG_H 160 #define FPC1020_HALF_SIZE 12800 #define FPC1020_XFER_SIZE 12802 // 2-byte header + 12800 pixels #define FPC1020_TIMEOUT 2000 #define FPC1020_IMG_TIMEOUT 6000 // longer timout for images #define FPC1020_NUM_INIT_CMDS 16 #define FPC1020_NUM_INIT_PASSES 2 #define FPC1020_POLL_MAX 100 static const guint8 SPI_CONFIG[16] = { 0xc0, 0xc6, 0x2d, 0x00, 0x08, 0x00, 0xff, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // backend device init command static const guint8 CMD_RESET[] = { 0xf8 }; static const guint8 CMD_IRQ_READ[] = { 0x1c, 0x00 }; static const guint8 CMD_HWID[] = { 0xfc, 0x00, 0x00 }; static const guint8 CMD_CONFIG[] = { 0x9c, 0x55, 0x40, 0x00, 0x2d, 0x24 }; static const guint8 CMD_REG_6C[] = { 0x6c, 0x29 }; static const guint8 CMD_REG_90[] = { 0x90, 0x32 }; static const guint8 CMD_CAL[] = { 0x68, 0x31, 0x31, 0x31, 0x31, 0x3d, 0x3d, 0x3d, 0x3d }; static const guint8 CMD_CRYPTO[] = { 0x98, 0x03, 0xeb, 0x3f, 0xb8, 0x0a, 0x07, 0x11, 0x22, 0x6a, 0x5d }; static const guint8 CMD_REG_8C[] = { 0x8c, 0x12 }; static const guint8 CMD_PXLCTRL[] = { 0x5c, 0x0b }; static const guint8 CMD_FDET_THRES[] = { 0xd8, 0x20 }; static const guint8 CMD_FDET_CNTR[] = { 0xdc, 0x00, 0x0a }; static const guint8 CMD_ADC_GAIN[] = { 0xa8, 0x0f, 0x0a }; static const guint8 CMD_PXL_OFFSET[] = { 0xa0, 0x0a, 0x02 }; static const guint8 CMD_FINALIZE[] = { 0x28 }; struct spi_cmd { const guint8 *data; gsize len; }; static const struct spi_cmd INIT_CMDS[FPC1020_NUM_INIT_CMDS] = { { CMD_RESET, sizeof (CMD_RESET) }, { CMD_IRQ_READ, sizeof (CMD_IRQ_READ) }, { CMD_IRQ_READ, sizeof (CMD_IRQ_READ) }, { CMD_HWID, sizeof (CMD_HWID) }, { CMD_CONFIG, sizeof (CMD_CONFIG) }, { CMD_REG_6C, sizeof (CMD_REG_6C) }, { CMD_REG_90, sizeof (CMD_REG_90) }, { CMD_CAL, sizeof (CMD_CAL) }, { CMD_CRYPTO, sizeof (CMD_CRYPTO) }, { CMD_REG_8C, sizeof (CMD_REG_8C) }, { CMD_PXLCTRL, sizeof (CMD_PXLCTRL) }, { CMD_FDET_THRES, sizeof (CMD_FDET_THRES) }, { CMD_FDET_CNTR, sizeof (CMD_FDET_CNTR) }, { CMD_ADC_GAIN, sizeof (CMD_ADC_GAIN) }, { CMD_PXL_OFFSET, sizeof (CMD_PXL_OFFSET) }, { CMD_FINALIZE, sizeof (CMD_FINALIZE) }, }; // full resolution static const guint8 CMD_FR_OFFSET[] = { 0xa0, 0x0e, 0x03 }; static const guint8 CMD_FR_SAMPLE[] = { 0x64, 0x1e }; static const guint8 CMD_FR_PXLCTRL[] = { 0x5c, 0x0b }; static const guint8 CMD_FR_WINDOW[] = { 0x54, 0x00, 0xa0, 0x00, 0xa0 }; static const guint8 CMD_ARM[] = { 0x20 }; static const guint8 CMD_CAPTURE[] = { 0xc0 }; static const guint8 CMD_FINGER_DET[] = { 0xd4, 0x00, 0x00, 0x00, 0x00 }; struct _FpDeviceFpc1020Samsung { FpImageDevice parent; const guint8 *current_cmd; gsize current_cmd_len; guint8 spi_response[16]; guint8 *top_raw; guint8 *bot_raw; gint xfer_complete_count; FpiSsm *capture_ssm; gint init_pass; gint cmd_idx; gint poll_count; GSource *finger_poll_source; gboolean finger_detected; gboolean needs_reinit; gboolean deactivating; gboolean ssm_active; }; G_DECLARE_FINAL_TYPE (FpDeviceFpc1020Samsung, fpi_device_fpc1020_samsung, FPI, DEVICE_FPC1020_SAMSUNG, FpImageDevice); G_DEFINE_TYPE (FpDeviceFpc1020Samsung, fpi_device_fpc1020_samsung, FP_TYPE_IMAGE_DEVICE); static void start_finger_poll (FpDeviceFpc1020Samsung *self); static void start_finger_off_poll (FpDeviceFpc1020Samsung *self); static void start_capture (FpDeviceFpc1020Samsung *self); enum spi_cmd_states { SPI_CMD_BRIDGE_CONFIG, SPI_CMD_BRIDGE_SET_SIZE, SPI_CMD_BULK_WRITE, SPI_CMD_BULK_READ, SPI_CMD_NUM_STATES, }; static void spi_read_cb (FpiUsbTransfer *transfer, FpDevice *dev, gpointer user_data, GError *error) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); if (error) { fpi_ssm_mark_failed (transfer->ssm, error); return; } memset (self->spi_response, 0, sizeof (self->spi_response)); memcpy (self->spi_response, transfer->buffer, MIN ((gsize) transfer->actual_length, sizeof (self->spi_response))); fpi_ssm_next_state (transfer->ssm); } static void spi_cmd_run_state (FpiSsm *ssm, FpDevice *dev) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); FpiUsbTransfer *transfer; switch (fpi_ssm_get_cur_state (ssm)) { case SPI_CMD_BRIDGE_CONFIG: transfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_control (transfer, G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, G_USB_DEVICE_REQUEST_TYPE_VENDOR, G_USB_DEVICE_RECIPIENT_DEVICE, 0xC3, 0x0000, 0x0000, sizeof (SPI_CONFIG)); memcpy (transfer->buffer, SPI_CONFIG, sizeof (SPI_CONFIG)); transfer->ssm = ssm; fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL, fpi_ssm_usb_transfer_cb, NULL); break; case SPI_CMD_BRIDGE_SET_SIZE: transfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_control (transfer, G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, G_USB_DEVICE_REQUEST_TYPE_VENDOR, G_USB_DEVICE_RECIPIENT_DEVICE, 0xCA, 0x0003, (guint16) self->current_cmd_len, 0); transfer->ssm = ssm; fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL, fpi_ssm_usb_transfer_cb, NULL); break; case SPI_CMD_BULK_WRITE: transfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_bulk_full (transfer, FPC1020_EP_OUT, g_memdup2 (self->current_cmd, self->current_cmd_len), self->current_cmd_len, g_free); transfer->ssm = ssm; transfer->short_is_error = TRUE; fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL, fpi_ssm_usb_transfer_cb, NULL); break; case SPI_CMD_BULK_READ: transfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_bulk (transfer, FPC1020_EP_IN, self->current_cmd_len); transfer->ssm = ssm; fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL, spi_read_cb, NULL); break; default: g_assert_not_reached (); } } static void start_spi_cmd_subsm (FpiSsm *parent, FpDeviceFpc1020Samsung *self, const guint8 *cmd, gsize len) { FpiSsm *subsm; self->current_cmd = cmd; self->current_cmd_len = len; subsm = fpi_ssm_new (FP_DEVICE (self), spi_cmd_run_state, SPI_CMD_NUM_STATES); fpi_ssm_start_subsm (parent, subsm); } enum capture_states { CAP_CONFIRM_ARM, CAP_BWAIT_1, CAP_BWAIT_2, CAP_BWAIT_3, CAP_BWAIT_4, CAP_CONFIRM_IRQ, CAP_CONFIRM_DETAIL, CAP_FR_OFFSET, CAP_FR_SAMPLE, CAP_FR_PXLCTRL, CAP_FR_WINDOW, CAP_TRIGGER, CAP_POLL_IRQ, CAP_POLL_CHECK, CAP_POLL_BSTAT, CAP_TOP_CONFIG, CAP_TOP_SIZE, CAP_TOP_XFER, CAP_BOT_CONFIG, CAP_BOT_SIZE, CAP_BOT_XFER, CAP_TEARDOWN_1, CAP_TEARDOWN_2, CAP_TEARDOWN_3, CAP_TEARDOWN_4, CAP_TEARDOWN_ARM, CAP_SUBMIT, CAP_NUM_STATES, }; static void bstat_delay_cb (FpiUsbTransfer *transfer, FpDevice *dev, gpointer user_data, GError *error) { if (error) { fpi_ssm_mark_failed (transfer->ssm, error); return; } fpi_ssm_next_state_delayed (transfer->ssm, 10); } static void do_bridge_status (FpiSsm *ssm, FpDevice *dev, FpiUsbTransferCallback cb) { FpiUsbTransfer *transfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_control (transfer, G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST, G_USB_DEVICE_REQUEST_TYPE_VENDOR, G_USB_DEVICE_RECIPIENT_DEVICE, 0xDA, 0x0007, 0x0000, 2); transfer->ssm = ssm; fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL, cb, NULL); } static void poll_bstat_cb (FpiUsbTransfer *transfer, FpDevice *dev, gpointer user_data, GError *error) { if (error) { fpi_ssm_mark_failed (transfer->ssm, error); return; } fpi_ssm_jump_to_state_delayed (transfer->ssm, CAP_POLL_IRQ, 10); } enum open_states { OPEN_BRIDGE_INIT_1, OPEN_BRIDGE_INIT_2, OPEN_INIT_CMD, OPEN_INIT_NEXT, OPEN_NUM_STATES, }; static void open_run_state (FpiSsm *ssm, FpDevice *dev) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); FpiUsbTransfer *transfer; switch (fpi_ssm_get_cur_state (ssm)) { case OPEN_BRIDGE_INIT_1: transfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_control (transfer, G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, G_USB_DEVICE_REQUEST_TYPE_VENDOR, G_USB_DEVICE_RECIPIENT_DEVICE, 0xDB, 0x0006, 0x0000, 0); transfer->ssm = ssm; fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL, fpi_ssm_usb_transfer_cb, NULL); break; case OPEN_BRIDGE_INIT_2: transfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_control (transfer, G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, G_USB_DEVICE_REQUEST_TYPE_VENDOR, G_USB_DEVICE_RECIPIENT_DEVICE, 0xDB, 0x0006, 0x0001, 0); transfer->ssm = ssm; fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL, fpi_ssm_usb_transfer_cb, NULL); break; case OPEN_INIT_CMD: { const struct spi_cmd *cmd = &INIT_CMDS[self->cmd_idx]; start_spi_cmd_subsm (ssm, self, cmd->data, cmd->len); } break; case OPEN_INIT_NEXT: self->cmd_idx++; if (self->cmd_idx < FPC1020_NUM_INIT_CMDS) { fpi_ssm_jump_to_state (ssm, OPEN_INIT_CMD); } else { self->init_pass++; if (self->init_pass < FPC1020_NUM_INIT_PASSES) { self->cmd_idx = 0; fpi_ssm_jump_to_state (ssm, OPEN_INIT_CMD); } else { fp_dbg ("FPC1020 init complete (%d passes)", FPC1020_NUM_INIT_PASSES); fpi_ssm_mark_completed (ssm); } } break; default: g_assert_not_reached (); } } static void open_complete (FpiSsm *ssm, FpDevice *dev, GError *error) { FpImageDevice *img_dev = FP_IMAGE_DEVICE (dev); fpi_image_device_open_complete (img_dev, error); } static void fpc1020_open (FpImageDevice *dev) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); FpiSsm *ssm; GError *error = NULL; if (!g_usb_device_claim_interface (fpi_device_get_usb_device (FP_DEVICE (dev)), 0, 0, &error)) { fpi_image_device_open_complete (dev, error); return; } self->top_raw = g_malloc0 (FPC1020_XFER_SIZE); self->bot_raw = g_malloc0 (FPC1020_XFER_SIZE); self->init_pass = 0; self->cmd_idx = 0; ssm = fpi_ssm_new (FP_DEVICE (dev), open_run_state, OPEN_NUM_STATES); fpi_ssm_start (ssm, open_complete); } static void fpc1020_close (FpImageDevice *dev) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); GError *error = NULL; g_clear_pointer (&self->top_raw, g_free); g_clear_pointer (&self->bot_raw, g_free); g_usb_device_release_interface (fpi_device_get_usb_device (FP_DEVICE (dev)), 0, 0, &error); fpi_image_device_close_complete (dev, error); } static void fpc1020_activate (FpImageDevice *dev) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); self->deactivating = FALSE; fpi_image_device_activate_complete (dev, NULL); } static void fpc1020_deactivate (FpImageDevice *dev) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); self->deactivating = TRUE; if (self->finger_poll_source) { g_source_destroy (self->finger_poll_source); self->finger_poll_source = NULL; } if (!self->ssm_active) fpi_image_device_deactivate_complete (dev, NULL); } enum finger_poll_states { FPOLL_READ_IRQ, FPOLL_CHECK_IRQ, FPOLL_ARM, FPOLL_BWAIT_1, FPOLL_BWAIT_2, FPOLL_BWAIT_3, FPOLL_BWAIT_4, FPOLL_REREAD_IRQ, FPOLL_CHECK_REREAD, FPOLL_READ_DETAIL, FPOLL_DONE, FPOLL_NUM_STATES, }; static void finger_poll_timer_cb (FpDevice *dev, gpointer user_data); static void finger_poll_complete (FpiSsm *ssm, FpDevice *dev, GError *error) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); self->ssm_active = FALSE; if (self->deactivating) { fpi_image_device_deactivate_complete (FP_IMAGE_DEVICE (dev), error); return; } if (error) { fpi_image_device_session_error (FP_IMAGE_DEVICE (dev), error); return; } if (self->finger_detected) { fpi_image_device_report_finger_status (FP_IMAGE_DEVICE (dev), TRUE); } else if (self->needs_reinit) { self->needs_reinit = FALSE; start_finger_poll (self); } else { if (!self->deactivating) self->finger_poll_source = fpi_device_add_timeout (FP_DEVICE (self), 100, finger_poll_timer_cb, NULL, NULL); } } static void finger_poll_run_state (FpiSsm *ssm, FpDevice *dev) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); if (self->deactivating) { fpi_ssm_mark_completed (ssm); return; } switch (fpi_ssm_get_cur_state (ssm)) { case FPOLL_READ_IRQ: start_spi_cmd_subsm (ssm, self, CMD_IRQ_READ, sizeof (CMD_IRQ_READ)); break; case FPOLL_CHECK_IRQ: fp_dbg ("finger poll IRQ: 0x%02x", self->spi_response[1]); if (self->spi_response[1] == 0x01) { fpi_ssm_next_state (ssm); } else if (self->spi_response[1] & 0x80) { fpi_ssm_jump_to_state (ssm, FPOLL_READ_DETAIL); } else { fpi_ssm_mark_completed (ssm); } break; case FPOLL_ARM: start_spi_cmd_subsm (ssm, self, CMD_ARM, sizeof (CMD_ARM)); break; case FPOLL_BWAIT_1: do_bridge_status (ssm, dev, bstat_delay_cb); break; case FPOLL_BWAIT_2: do_bridge_status (ssm, dev, bstat_delay_cb); break; case FPOLL_BWAIT_3: do_bridge_status (ssm, dev, bstat_delay_cb); break; case FPOLL_BWAIT_4: do_bridge_status (ssm, dev, bstat_delay_cb); break; case FPOLL_REREAD_IRQ: start_spi_cmd_subsm (ssm, self, CMD_IRQ_READ, sizeof (CMD_IRQ_READ)); break; case FPOLL_CHECK_REREAD: fp_dbg ("finger poll re-read IRQ: 0x%02x", self->spi_response[1]); if (self->spi_response[1] & 0x80) { fpi_ssm_next_state (ssm); } else { fp_dbg ("ARM consumed state, will reinit"); self->needs_reinit = TRUE; fpi_ssm_mark_completed (ssm); } break; case FPOLL_READ_DETAIL: start_spi_cmd_subsm (ssm, self, CMD_FINGER_DET, sizeof (CMD_FINGER_DET)); break; case FPOLL_DONE: self->finger_detected = TRUE; fpi_ssm_mark_completed (ssm); break; default: g_assert_not_reached (); } } static void finger_poll_timer_cb (FpDevice *dev, gpointer user_data) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); FpiSsm *ssm; self->finger_poll_source = NULL; if (self->deactivating) { fpi_image_device_deactivate_complete (FP_IMAGE_DEVICE (dev), NULL); return; } self->finger_detected = FALSE; ssm = fpi_ssm_new (dev, finger_poll_run_state, FPOLL_NUM_STATES); self->ssm_active = TRUE; fpi_ssm_start (ssm, finger_poll_complete); } enum reinit_states { REINIT_CMD, REINIT_NEXT, REINIT_NUM_STATES, }; static void reinit_run_state (FpiSsm *ssm, FpDevice *dev) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); if (self->deactivating) { fpi_ssm_mark_completed (ssm); return; } switch (fpi_ssm_get_cur_state (ssm)) { case REINIT_CMD: { const struct spi_cmd *cmd = &INIT_CMDS[self->cmd_idx]; start_spi_cmd_subsm (ssm, self, cmd->data, cmd->len); } break; case REINIT_NEXT: self->cmd_idx++; if (self->cmd_idx < FPC1020_NUM_INIT_CMDS) { fpi_ssm_jump_to_state (ssm, REINIT_CMD); } else { self->init_pass++; if (self->init_pass < FPC1020_NUM_INIT_PASSES) { self->cmd_idx = 0; fpi_ssm_jump_to_state (ssm, REINIT_CMD); } else { fp_dbg ("FPC1020 re-init complete"); fpi_ssm_mark_completed (ssm); } } break; default: g_assert_not_reached (); } } static void reinit_complete (FpiSsm *ssm, FpDevice *dev, GError *error) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); self->ssm_active = FALSE; if (self->deactivating) { fpi_image_device_deactivate_complete (FP_IMAGE_DEVICE (dev), error); return; } if (error) { fpi_image_device_session_error (FP_IMAGE_DEVICE (dev), error); return; } self->finger_poll_source = fpi_device_add_timeout (FP_DEVICE (self), 100, finger_poll_timer_cb, NULL, NULL); } static void start_finger_poll (FpDeviceFpc1020Samsung *self) { FpiSsm *ssm; if (self->deactivating) return; self->init_pass = 0; self->cmd_idx = 0; ssm = fpi_ssm_new (FP_DEVICE (self), reinit_run_state, REINIT_NUM_STATES); self->ssm_active = TRUE; fpi_ssm_start (ssm, reinit_complete); } static void img_xfer_check_done (FpDeviceFpc1020Samsung *self) { if (g_atomic_int_add (&self->xfer_complete_count, 1) + 1 >= 2) fpi_ssm_next_state (self->capture_ssm); } static void img_in_cb (FpiUsbTransfer *transfer, FpDevice *dev, gpointer user_data, GError *error) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); guint8 *dest = user_data; if (error) { fpi_ssm_mark_failed (self->capture_ssm, error); return; } memcpy (dest, transfer->buffer, MIN ((gsize) transfer->actual_length, FPC1020_XFER_SIZE)); fp_dbg ("img IN: %ld bytes", (long) transfer->actual_length); img_xfer_check_done (self); } static void img_out_cb (FpiUsbTransfer *transfer, FpDevice *dev, gpointer user_data, GError *error) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); if (error) { fpi_ssm_mark_failed (self->capture_ssm, error); return; } fp_dbg ("img OUT: %ld bytes", (long) transfer->actual_length); img_xfer_check_done (self); } static void submit_concurrent_image_xfer (FpiSsm *ssm, FpDevice *dev, guint8 *dest) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); FpiUsbTransfer *in_xfer; FpiUsbTransfer *out_xfer; guint8 *out_buf; g_atomic_int_set (&self->xfer_complete_count, 0); self->capture_ssm = ssm; /* Submit bulk IN (doesnt work if not ordered? bridge req.) */ in_xfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_bulk (in_xfer, FPC1020_EP_IN, FPC1020_XFER_SIZE); fpi_usb_transfer_submit (in_xfer, FPC1020_IMG_TIMEOUT, NULL, img_in_cb, dest); /* Submit bulk OUT */ out_buf = g_malloc0 (FPC1020_XFER_SIZE); out_buf[0] = 0xC4; out_xfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_bulk_full (out_xfer, FPC1020_EP_OUT, out_buf, FPC1020_XFER_SIZE, g_free); out_xfer->short_is_error = TRUE; fpi_usb_transfer_submit (out_xfer, FPC1020_IMG_TIMEOUT, NULL, img_out_cb, NULL); } static void capture_run_state (FpiSsm *ssm, FpDevice *dev) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); FpiUsbTransfer *transfer; if (self->deactivating) { fpi_ssm_mark_completed (ssm); return; } switch (fpi_ssm_get_cur_state (ssm)) { case CAP_CONFIRM_ARM: start_spi_cmd_subsm (ssm, self, CMD_ARM, sizeof (CMD_ARM)); break; case CAP_BWAIT_1: case CAP_BWAIT_2: case CAP_BWAIT_3: case CAP_BWAIT_4: do_bridge_status (ssm, dev, bstat_delay_cb); break; case CAP_CONFIRM_IRQ: start_spi_cmd_subsm (ssm, self, CMD_IRQ_READ, sizeof (CMD_IRQ_READ)); break; case CAP_CONFIRM_DETAIL: start_spi_cmd_subsm (ssm, self, CMD_FINGER_DET, sizeof (CMD_FINGER_DET)); break; case CAP_FR_OFFSET: start_spi_cmd_subsm (ssm, self, CMD_FR_OFFSET, sizeof (CMD_FR_OFFSET)); break; case CAP_FR_SAMPLE: start_spi_cmd_subsm (ssm, self, CMD_FR_SAMPLE, sizeof (CMD_FR_SAMPLE)); break; case CAP_FR_PXLCTRL: start_spi_cmd_subsm (ssm, self, CMD_FR_PXLCTRL, sizeof (CMD_FR_PXLCTRL)); break; case CAP_FR_WINDOW: start_spi_cmd_subsm (ssm, self, CMD_FR_WINDOW, sizeof (CMD_FR_WINDOW)); break; case CAP_TRIGGER: self->poll_count = 0; start_spi_cmd_subsm (ssm, self, CMD_CAPTURE, sizeof (CMD_CAPTURE)); break; case CAP_POLL_IRQ: start_spi_cmd_subsm (ssm, self, CMD_IRQ_READ, sizeof (CMD_IRQ_READ)); break; case CAP_POLL_CHECK: if (self->spi_response[1] == 0x20) { fp_dbg ("Capture ready after %d polls", self->poll_count + 1); fpi_ssm_jump_to_state (ssm, CAP_TOP_CONFIG); } else { self->poll_count++; if (self->poll_count >= FPC1020_POLL_MAX) { fpi_ssm_mark_failed (ssm, fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, "Capture not ready after %d polls", FPC1020_POLL_MAX)); return; } fpi_ssm_next_state (ssm); } break; case CAP_POLL_BSTAT: { FpiUsbTransfer *xfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_control (xfer, G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST, G_USB_DEVICE_REQUEST_TYPE_VENDOR, G_USB_DEVICE_RECIPIENT_DEVICE, 0xDA, 0x0007, 0x0000, 2); xfer->ssm = ssm; fpi_usb_transfer_submit (xfer, FPC1020_TIMEOUT, NULL, poll_bstat_cb, NULL); } break; case CAP_TOP_CONFIG: transfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_control (transfer, G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, G_USB_DEVICE_REQUEST_TYPE_VENDOR, G_USB_DEVICE_RECIPIENT_DEVICE, 0xC3, 0x0000, 0x0000, sizeof (SPI_CONFIG)); memcpy (transfer->buffer, SPI_CONFIG, sizeof (SPI_CONFIG)); transfer->ssm = ssm; fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL, fpi_ssm_usb_transfer_cb, NULL); break; case CAP_TOP_SIZE: transfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_control (transfer, G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, G_USB_DEVICE_REQUEST_TYPE_VENDOR, G_USB_DEVICE_RECIPIENT_DEVICE, 0xCA, 0x0003, (guint16) FPC1020_XFER_SIZE, 0); transfer->ssm = ssm; fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL, fpi_ssm_usb_transfer_cb, NULL); break; case CAP_TOP_XFER: submit_concurrent_image_xfer (ssm, dev, self->top_raw); break; case CAP_BOT_CONFIG: transfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_control (transfer, G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, G_USB_DEVICE_REQUEST_TYPE_VENDOR, G_USB_DEVICE_RECIPIENT_DEVICE, 0xC3, 0x0000, 0x0000, sizeof (SPI_CONFIG)); memcpy (transfer->buffer, SPI_CONFIG, sizeof (SPI_CONFIG)); transfer->ssm = ssm; fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL, fpi_ssm_usb_transfer_cb, NULL); break; case CAP_BOT_SIZE: transfer = fpi_usb_transfer_new (dev); fpi_usb_transfer_fill_control (transfer, G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, G_USB_DEVICE_REQUEST_TYPE_VENDOR, G_USB_DEVICE_RECIPIENT_DEVICE, 0xCA, 0x0003, (guint16) FPC1020_XFER_SIZE, 0); transfer->ssm = ssm; fpi_usb_transfer_submit (transfer, FPC1020_TIMEOUT, NULL, fpi_ssm_usb_transfer_cb, NULL); break; case CAP_BOT_XFER: submit_concurrent_image_xfer (ssm, dev, self->bot_raw); break; /* ── Teardown ── */ case CAP_TEARDOWN_1: start_spi_cmd_subsm (ssm, self, CMD_ADC_GAIN, sizeof (CMD_ADC_GAIN)); break; case CAP_TEARDOWN_2: start_spi_cmd_subsm (ssm, self, CMD_PXL_OFFSET, sizeof (CMD_PXL_OFFSET)); break; case CAP_TEARDOWN_3: start_spi_cmd_subsm (ssm, self, CMD_ADC_GAIN, sizeof (CMD_ADC_GAIN)); break; case CAP_TEARDOWN_4: start_spi_cmd_subsm (ssm, self, CMD_PXL_OFFSET, sizeof (CMD_PXL_OFFSET)); break; case CAP_TEARDOWN_ARM: start_spi_cmd_subsm (ssm, self, CMD_ARM, sizeof (CMD_ARM)); break; case CAP_SUBMIT: { FpImage *img = fp_image_new (FPC1020_IMG_W, FPC1020_IMG_H); memcpy (img->data, self->top_raw + 2, FPC1020_HALF_SIZE); memcpy (img->data + FPC1020_HALF_SIZE, self->bot_raw + 2, FPC1020_HALF_SIZE); /* Needs invert and reverse. */ img->flags = FPI_IMAGE_COLORS_INVERTED | FPI_IMAGE_PARTIAL; img->ppmm = 20.0; { guint8 pmin = 255, pmax = 0; gulong psum = 0; int i; for (i = 0; i < FPC1020_IMG_W * FPC1020_IMG_H; i++) { guint8 v = img->data[i]; if (v < pmin) pmin = v; if (v > pmax) pmax = v; psum += v; } fp_dbg ("Image stats: min=%u max=%u mean=%lu hdr_top=%02x%02x hdr_bot=%02x%02x", pmin, pmax, psum / (FPC1020_IMG_W * FPC1020_IMG_H), self->top_raw[0], self->top_raw[1], self->bot_raw[0], self->bot_raw[1]); } fp_dbg ("Image captured (%dx%d)", FPC1020_IMG_W, FPC1020_IMG_H); fpi_image_device_image_captured (FP_IMAGE_DEVICE (dev), img); fpi_ssm_mark_completed (ssm); } break; default: g_assert_not_reached (); } } static void capture_complete (FpiSsm *ssm, FpDevice *dev, GError *error) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); self->ssm_active = FALSE; self->capture_ssm = NULL; if (self->deactivating) { fpi_image_device_deactivate_complete (FP_IMAGE_DEVICE (dev), error); return; } if (error) fpi_image_device_session_error (FP_IMAGE_DEVICE (dev), error); } static void start_capture (FpDeviceFpc1020Samsung *self) { FpiSsm *ssm; ssm = fpi_ssm_new (FP_DEVICE (self), capture_run_state, CAP_NUM_STATES); self->ssm_active = TRUE; self->capture_ssm = ssm; fpi_ssm_start (ssm, capture_complete); } enum finger_off_states { FOFF_READ_IRQ, FOFF_CHECK, FOFF_NUM_STATES, }; static void finger_off_poll_complete (FpiSsm *ssm, FpDevice *dev, GError *error); static void finger_off_run_state (FpiSsm *ssm, FpDevice *dev) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); if (self->deactivating) { fpi_ssm_mark_completed (ssm); return; } switch (fpi_ssm_get_cur_state (ssm)) { case FOFF_READ_IRQ: start_spi_cmd_subsm (ssm, self, CMD_IRQ_READ, sizeof (CMD_IRQ_READ)); break; case FOFF_CHECK: fp_dbg ("finger off IRQ: 0x%02x", self->spi_response[1]); if (self->spi_response[1] == 0x00) { fpi_ssm_mark_completed (ssm); } else { fpi_ssm_mark_completed (ssm); } break; default: g_assert_not_reached (); } } static void finger_off_timer_cb (FpDevice *dev, gpointer user_data); static void finger_off_poll_complete (FpiSsm *ssm, FpDevice *dev, GError *error) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); self->ssm_active = FALSE; if (self->deactivating) { fpi_image_device_deactivate_complete (FP_IMAGE_DEVICE (dev), error); return; } if (error) { fpi_image_device_session_error (FP_IMAGE_DEVICE (dev), error); return; } if (self->spi_response[1] == 0x00) { fpi_image_device_report_finger_status (FP_IMAGE_DEVICE (dev), FALSE); } else { start_finger_off_poll (self); } } static void finger_off_timer_cb (FpDevice *dev, gpointer user_data) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); FpiSsm *ssm; self->finger_poll_source = NULL; if (self->deactivating) { fpi_image_device_deactivate_complete (FP_IMAGE_DEVICE (dev), NULL); return; } ssm = fpi_ssm_new (dev, finger_off_run_state, FOFF_NUM_STATES); self->ssm_active = TRUE; fpi_ssm_start (ssm, finger_off_poll_complete); } static void start_finger_off_poll (FpDeviceFpc1020Samsung *self) { if (self->deactivating) return; self->finger_poll_source = fpi_device_add_timeout (FP_DEVICE (self), 100, finger_off_timer_cb, NULL, NULL); } static void fpc1020_change_state (FpImageDevice *dev, FpiImageDeviceState state) { FpDeviceFpc1020Samsung *self = FPI_DEVICE_FPC1020_SAMSUNG (dev); switch (state) { case FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_ON: start_finger_poll (self); break; case FPI_IMAGE_DEVICE_STATE_CAPTURE: start_capture (self); break; case FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_OFF: start_finger_off_poll (self); break; case FPI_IMAGE_DEVICE_STATE_INACTIVE: case FPI_IMAGE_DEVICE_STATE_ACTIVATING: case FPI_IMAGE_DEVICE_STATE_DEACTIVATING: case FPI_IMAGE_DEVICE_STATE_IDLE: break; } } static const FpIdEntry id_table[] = { { .vid = 0x04e8, .pid = 0x7301 }, { .vid = 0, .pid = 0 }, }; static void fpi_device_fpc1020_samsung_init (FpDeviceFpc1020Samsung *self) { } static void fpi_device_fpc1020_samsung_class_init (FpDeviceFpc1020SamsungClass *klass) { FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass); FpImageDeviceClass *img_class = FP_IMAGE_DEVICE_CLASS (klass); dev_class->id = FP_COMPONENT; dev_class->full_name = "FPC1020 (Samsung USB-SPI Bridge)"; dev_class->type = FP_DEVICE_TYPE_USB; dev_class->id_table = id_table; dev_class->scan_type = FP_SCAN_TYPE_PRESS; img_class->img_width = FPC1020_IMG_W; img_class->img_height = FPC1020_IMG_H; img_class->bz3_threshold = 12; img_class->img_open = fpc1020_open; img_class->img_close = fpc1020_close; img_class->activate = fpc1020_activate; img_class->deactivate = fpc1020_deactivate; img_class->change_state = fpc1020_change_state; }