From bec97fd564f2bdc3c7a293d5ac63c28851e47198 Mon Sep 17 00:00:00 2001 From: "Morgan J." Date: Sun, 15 Feb 2026 18:00:47 +0900 Subject: [PATCH] Add support for 04e8:7301 Samsung Electronics Fingerprint Device --- libfprint/drivers/fpc1020_samsung.c | 1198 +++++++++++++++++++++++++++ libfprint/meson.build | 2 + meson.build | 2 + 3 files changed, 1202 insertions(+) create mode 100644 libfprint/drivers/fpc1020_samsung.c diff --git a/libfprint/drivers/fpc1020_samsung.c b/libfprint/drivers/fpc1020_samsung.c new file mode 100644 index 0000000..3490249 --- /dev/null +++ b/libfprint/drivers/fpc1020_samsung.c @@ -0,0 +1,1198 @@ +/* + * 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; +} diff --git a/libfprint/meson.build b/libfprint/meson.build index ae0f6e2..4f37af4 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -153,6 +153,8 @@ driver_sources = { [ 'drivers/realtek/realtek.c' ], 'focaltech_moc' : [ 'drivers/focaltech_moc/focaltech_moc.c' ], + 'fpc1020_samsung' : + [ 'drivers/fpc1020_samsung.c' ], } helper_sources = { diff --git a/meson.build b/meson.build index 14fb11f..3151b5e 100644 --- a/meson.build +++ b/meson.build @@ -144,6 +144,7 @@ default_drivers = [ 'fpcmoc', 'realtek', 'focaltech_moc', + 'fpc1020_samsung', ] spi_drivers = [ @@ -168,6 +169,7 @@ endian_independent_drivers = virtual_drivers + [ 'elanmoc', 'etes603', 'focaltech_moc', + 'fpc1020_samsung', 'nb1010', 'realtek', 'synaptics',