diff --git a/libfprint/drivers/secugen/secugen.c b/libfprint/drivers/secugen/secugen.c new file mode 100644 index 00000000..32668e57 --- /dev/null +++ b/libfprint/drivers/secugen/secugen.c @@ -0,0 +1,1832 @@ +/* + * SecuGen Hamster Pro 20 (FDU05) native USB driver for libfprint + * + * Protocol reverse-engineered from USB packet captures. + * No proprietary code or SDK used. + * + * SIDO020A sensor configured via I2C-over-USB control transfers. + * Raw sensor output: 956x688 grayscale via USB bulk (657KB). + * Downsampled to 300x400 at 500 DPI 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 "secugen" + +#include +#include "secugen.h" + +G_DEFINE_TYPE (FpiDeviceSecugen, + fpi_device_secugen, + FP_TYPE_IMAGE_DEVICE); + +/* ---- Blend curve LUT (256 bytes) ---- + * + * Brightness-dependent correction weight for flat-field correction. + * Dark pixels (<16) get zero correction, bright pixels (>191) get full + * correction, middle range scales linearly. Extracted from the sensor + * driver binary and confirmed identical to the FW-stored copy. + */ +static const guint8 g_blend_curve[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 19, 21, 23, 25, 27, 29, 31, 32, 34, 36, 38, 40, 42, 44, + 46, 47, 49, 51, 53, 55, 57, 59, 61, 62, 64, 66, 68, 70, 72, 74, + 76, 77, 79, 81, 83, 85, 87, 89, 91, 92, 94, 96, 98, 100, 102, 104, + 106, 107, 109, 111, 113, 115, 117, 119, 121, 122, 124, 126, 128, 130, 132, 134, + 136, 137, 138, 139, 141, 142, 143, 144, 146, 147, 148, 149, 151, 152, 153, 154, + 156, 157, 158, 159, 161, 162, 163, 164, 166, 167, 168, 169, 171, 172, 173, 174, + 176, 177, 178, 179, 181, 182, 183, 184, 186, 187, 188, 189, 191, 192, 193, 194, + 196, 197, 198, 199, 201, 202, 203, 204, 206, 207, 208, 209, 211, 212, 213, 214, + 216, 217, 218, 219, 221, 222, 223, 224, 226, 227, 228, 229, 231, 232, 233, 234, + 236, 237, 238, 239, 240, 241, 243, 244, 245, 246, 247, 249, 250, 251, 252, 253, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +}; + +/* ---- Bilinear resize 150x100 → 300x400 ---- + * + * The FW stores a low-res (150x100) flat-field reference image captured + * during factory calibration. The SDK resizes it to 300x400 before + * applying the Blend correction. We use bilinear interpolation. + */ +static void +secugen_resize_ref_image (const guint8 *src, guint8 *dst) +{ + int r, c; + + for (r = 0; r < SECUGEN_IMG_HEIGHT; r++) + { + /* Map destination row [0,399] → source row [0,99] */ + float src_rf = r * (SECUGEN_REF_HEIGHT - 1) / (float) (SECUGEN_IMG_HEIGHT - 1); + int r0 = (int) src_rf; + int r1 = r0 + 1; + float fr = src_rf - r0; + + if (r1 >= SECUGEN_REF_HEIGHT) + r1 = SECUGEN_REF_HEIGHT - 1; + + for (c = 0; c < SECUGEN_IMG_WIDTH; c++) + { + /* Map destination col [0,299] → source col [0,149] */ + float src_cf = c * (SECUGEN_REF_WIDTH - 1) / (float) (SECUGEN_IMG_WIDTH - 1); + int c0 = (int) src_cf; + int c1 = c0 + 1; + float fc = src_cf - c0; + float val; + + if (c1 >= SECUGEN_REF_WIDTH) + c1 = SECUGEN_REF_WIDTH - 1; + + val = (1 - fr) * (1 - fc) * src[r0 * SECUGEN_REF_WIDTH + c0] + + (1 - fr) * fc * src[r0 * SECUGEN_REF_WIDTH + c1] + + fr * (1 - fc) * src[r1 * SECUGEN_REF_WIDTH + c0] + + fr * fc * src[r1 * SECUGEN_REF_WIDTH + c1]; + + dst[r * SECUGEN_IMG_WIDTH + c] = (guint8) (val + 0.5f); + } + } +} + +/* ---- SIDO020A I2C register init table ---- */ + +/* + * Phase 1: Main init registers (54 regs). + * SDK writes these first. 0xa5-0xa8 are NOT included here -- they are + * written later (after a 0x03 rewrite to enable the sensor). + * 0xb7-0xb9 come BEFORE 0xa5-0xa8 per the SDK's actual order. + */ +static const struct secugen_reg_entry sido020a_init_regs[] = { + { 0x03, 0x02 }, /* Power/reset control - hold in reset */ + { 0x04, 0x81 }, + { 0x05, 0x0a }, + { 0x08, 0x00 }, + { 0x09, 0x11 }, + { 0x0a, 0x11 }, + { 0x10, 0x11 }, + { 0x11, 0x23 }, + { 0x12, 0x85 }, + { 0x13, 0x00 }, + { 0x14, 0x27 }, + { 0x16, 0xb6 }, + { 0x30, 0x01 }, + { 0x31, 0xc0 }, + { 0x32, 0x08 }, + { 0x41, 0x00 }, + { 0x42, 0x00 }, + { 0x43, 0x06 }, + { 0x44, 0x43 }, + { 0x45, 0x00 }, + { 0x46, 0x00 }, + { 0x47, 0x04 }, + { 0x48, 0xb3 }, + { 0x49, 0x00 }, + { 0x4a, 0x20 }, + { 0x4b, 0x00 }, + { 0x4c, 0x00 }, + { 0x4d, 0x00 }, + { 0x4e, 0x00 }, + { 0x60, 0x0b }, + { 0x61, 0x16 }, + { 0x62, 0x32 }, + { 0x63, 0x80 }, + { 0x71, 0x08 }, + { 0x80, 0xf8 }, + { 0x81, 0x06 }, + { 0x90, 0xaa }, + { 0x91, 0x08 }, + { 0x92, 0x10 }, + { 0x93, 0x40 }, + { 0x94, 0x04 }, + { 0x95, 0x01 }, + { 0x96, 0x02 }, + { 0x97, 0x08 }, + { 0x98, 0x10 }, + { 0x99, 0x08 }, + { 0x9a, 0x03 }, + { 0x9b, 0xb0 }, + { 0x9c, 0x08 }, + { 0x9d, 0x24 }, + { 0x9e, 0x30 }, + { 0xb7, 0x15 }, + { 0xb8, 0x28 }, + { 0xb9, 0x04 }, +}; + +#define N_INIT_REGS G_N_ELEMENTS (sido020a_init_regs) + +/* + * Phase 2 late init: written AFTER 0x03 is rewritten to 0x05 (sensor enable). + * SDK writes these after the power-on transition. + */ +static const struct secugen_reg_entry late_init_regs[] = { + { 0xa5, 0x00 }, + { 0xa6, 0x00 }, + { 0xa7, 0x00 }, + { 0xa8, 0x00 }, +}; + +#define N_LATE_INIT_REGS G_N_ELEMENTS (late_init_regs) + +/* Capture window config (written after late init, before FW reads) */ +static const struct secugen_reg_entry capture_window_regs[] = { + { 0x41, 0x00 }, { 0x42, 0xfa }, { 0x43, 0x04 }, { 0x44, 0x4f }, + { 0x45, 0x00 }, { 0x46, 0x28 }, { 0x47, 0x03 }, { 0x48, 0x23 }, +}; + +/* Frame window config (written after calibration capture) */ +static const struct secugen_reg_entry frame_window_regs[] = { + { 0x41, 0x01 }, { 0x42, 0x48 }, { 0x43, 0x03 }, { 0x44, 0xbf }, + { 0x45, 0x00 }, { 0x46, 0xcc }, { 0x47, 0x02 }, { 0x48, 0xb3 }, +}; + +#define N_CAPTURE_WINDOW_REGS G_N_ELEMENTS (capture_window_regs) +#define N_FRAME_WINDOW_REGS G_N_ELEMENTS (frame_window_regs) + +/* ================================================================ + * USB Transfer Helpers + * ================================================================ */ + +/* Send a vendor control OUT transfer (no data) and advance SSM */ +static void +secugen_ctrl_out (FpiSsm *ssm, + FpImageDevice *dev, + guint8 request, + guint16 value, + guint16 idx) +{ + FpiUsbTransfer *transfer = fpi_usb_transfer_new (FP_DEVICE (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, + request, value, idx, 0); + transfer->ssm = ssm; + fpi_usb_transfer_submit (transfer, SECUGEN_CTRL_TIMEOUT, NULL, + fpi_ssm_usb_transfer_cb, NULL); +} + +/* Send a vendor control OUT transfer with data payload and advance SSM */ +static void +secugen_ctrl_out_data (FpiSsm *ssm, + FpImageDevice *dev, + guint8 request, + guint16 value, + guint16 idx, + const guint8 *data, + gsize len) +{ + FpiUsbTransfer *transfer = fpi_usb_transfer_new (FP_DEVICE (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, + request, value, idx, len); + memcpy (transfer->buffer, data, len); + transfer->ssm = ssm; + fpi_usb_transfer_submit (transfer, SECUGEN_CTRL_TIMEOUT, NULL, + fpi_ssm_usb_transfer_cb, NULL); +} + +/* Read via vendor control IN transfer, callback receives data */ +static void +secugen_ctrl_in (FpiSsm *ssm, + FpImageDevice *dev, + guint8 request, + guint16 value, + guint16 idx, + gsize len, + FpiUsbTransferCallback callback) +{ + FpiUsbTransfer *transfer = fpi_usb_transfer_new (FP_DEVICE (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, + request, value, idx, len); + transfer->ssm = ssm; + fpi_usb_transfer_submit (transfer, SECUGEN_CTRL_TIMEOUT, NULL, + callback, NULL); +} + +/* Write a single I2C register on the SIDO020A sensor */ +static void +secugen_i2c_write (FpiSsm *ssm, + FpImageDevice *dev, + guint8 reg, + guint8 val) +{ + secugen_ctrl_out_data (ssm, dev, SECUGEN_REQ_I2C_REG, + SECUGEN_I2C_ADDR, reg, &val, 1); +} + +/* Read back a single I2C register (1 byte) */ +static void +secugen_i2c_read (FpiSsm *ssm, + FpImageDevice *dev, + guint8 reg, + FpiUsbTransferCallback callback) +{ + secugen_ctrl_in (ssm, dev, SECUGEN_REQ_I2C_REG, + SECUGEN_I2C_ADDR, reg, 1, callback); +} + +/* Set exposure value (big-endian 16-bit + 2 zero bytes) */ +static void +secugen_set_exposure (FpiSsm *ssm, + FpImageDevice *dev, + guint16 exposure) +{ + guint8 data[4] = { + (exposure >> 8) & 0xff, + exposure & 0xff, + 0x00, + 0x00 + }; + + secugen_ctrl_out_data (ssm, dev, SECUGEN_REQ_SET_EXPOSURE, + 0x0000, 0, data, 4); +} + +/* ================================================================ + * Init State Machine + * + * Matches SDK's actual USB sequence: + * 1. GET_DEVICE_ID + * 2. 54 main I2C regs (with readback) + * 3. Rewrite 0x03 = 0x05 (enable sensor) + * 4. 4 late I2C regs: 0xa5-0xa8 (with readback) + * 5. 8 capture window regs: 0x41-0x48 (with readback) + * 6. START_CAPTURE (activates sensor subsystem) + * 7. 6 FW/calibration data reads + * 8. 0x32=0x00, SET_EXPOSURE(1000), START_CAPTURE, START_STREAM, + * bulk read 120KB calibration image, STOP_STREAM + * 9. 8 frame window regs: 0x41-0x48 (with readback) + * 10. Post-calibration tuning: 0x32/0x30/0x31 writes + SET_EXPOSURE(1116) + * ================================================================ */ + +enum init_states { + INIT_GET_DEVICE_ID = 0, + /* Phase 1: Main I2C register init (54 regs) */ + INIT_I2C_WRITE, + INIT_I2C_READBACK, + /* Phase 2: Power enable + late regs */ + INIT_POWER_ENABLE, + INIT_LATE_I2C_WRITE, + INIT_LATE_I2C_READBACK, + /* Phase 3: Capture window config */ + INIT_WINDOW_I2C_WRITE, + INIT_WINDOW_I2C_READBACK, + /* Phase 4: START_CAPTURE + FW data reads */ + INIT_PRE_FW_CAPTURE, + INIT_FW_READ, + /* Phase 5: Calibration capture */ + INIT_CAL_REG32, + INIT_CAL_EXPOSURE, + INIT_CAL_START_CAPTURE, + INIT_CAL_START_STREAM, + INIT_CAL_BULK_READ, + INIT_CAL_STOP_STREAM, + /* Phase 6: Frame window config */ + INIT_FRAME_I2C_WRITE, + INIT_FRAME_I2C_READBACK, + /* Phase 7: Post-calibration tuning */ + INIT_POST_REG32_CLEAR, + INIT_POST_EXPOSURE, + INIT_POST_REG32_CLEAR2, + INIT_POST_REG30, + INIT_POST_REG31, + INIT_POST_REG32_CLEAR3, + INIT_POST_REG32_SET, + INIT_POST_EXPOSURE2, + /* Phase 8: Final calibration capture (operational settings) */ + INIT_FINAL_REG32, + INIT_FINAL_START_CAPTURE, + INIT_FINAL_START_STREAM, + INIT_FINAL_BULK_READ, + INIT_FINAL_STOP_STREAM, + INIT_NUM_STATES, +}; + +static void +init_device_id_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error) +{ + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + + if (error) + { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + memcpy (self->device_id, transfer->buffer, MIN (transfer->actual_length, 30)); + fp_dbg ("Device ID: %02x %02x %02x %02x ...", + self->device_id[0], self->device_id[1], + self->device_id[2], self->device_id[3]); + fpi_ssm_next_state (transfer->ssm); +} + +/* Readback callback for main init regs (54 entries) */ +static void +init_i2c_readback_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error) +{ + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + + if (error) + { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + self->last_reg_rd = transfer->buffer[0]; + self->init_reg_idx++; + + if (self->init_reg_idx < (int) N_INIT_REGS) + fpi_ssm_jump_to_state (transfer->ssm, INIT_I2C_WRITE); + else + { + self->init_reg_idx = 0; + fpi_ssm_next_state (transfer->ssm); + } +} + +/* Readback callback for late init regs (4 entries: 0xa5-0xa8) */ +static void +init_late_readback_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error) +{ + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + + if (error) + { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + self->last_reg_rd = transfer->buffer[0]; + self->init_reg_idx++; + + if (self->init_reg_idx < (int) N_LATE_INIT_REGS) + fpi_ssm_jump_to_state (transfer->ssm, INIT_LATE_I2C_WRITE); + else + { + self->init_reg_idx = 0; + fpi_ssm_next_state (transfer->ssm); + } +} + +/* Readback callback for capture window regs (8 entries) */ +static void +init_window_readback_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error) +{ + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + + if (error) + { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + self->last_reg_rd = transfer->buffer[0]; + self->init_reg_idx++; + + if (self->init_reg_idx < (int) N_CAPTURE_WINDOW_REGS) + fpi_ssm_jump_to_state (transfer->ssm, INIT_WINDOW_I2C_WRITE); + else + { + self->init_reg_idx = 0; + fpi_ssm_next_state (transfer->ssm); + } +} + +/* FW data read callback — accumulate data and extract reference image */ +static void +init_fw_read_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error) +{ + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + + if (error) + { + fp_warn ("FW data read failed (non-fatal): %s", error->message); + g_error_free (error); + fpi_ssm_next_state (transfer->ssm); + return; + } + + /* Accumulate FW data into contiguous buffer */ + { + gsize to_copy = transfer->actual_length; + + if (self->fw_data_len + to_copy > SECUGEN_FW_DATA_SIZE) + to_copy = SECUGEN_FW_DATA_SIZE - self->fw_data_len; + if (to_copy > 0 && self->fw_data) + { + memcpy (self->fw_data + self->fw_data_len, transfer->buffer, to_copy); + self->fw_data_len += to_copy; + } + } + + self->fw_read_idx++; + if (self->fw_read_idx < SECUGEN_FW_DATA_CHUNKS) + { + fpi_ssm_jump_to_state (transfer->ssm, INIT_FW_READ); + } + else + { + /* All FW data received — extract image processing parameters */ + + /* BLC region offsets: 16 × int16 at fw_data[0x0e] */ + if (self->fw_data_len >= SECUGEN_BLC_OFFSETS_FW + 32) + { + int i; + + for (i = 0; i < 16; i++) + { + guint16 raw_val; + + memcpy (&raw_val, + self->fw_data + SECUGEN_BLC_OFFSETS_FW + i * 2, 2); + self->blc_offsets[i] = (gint16) GUINT16_FROM_LE (raw_val); + } + self->has_blc_offsets = TRUE; + fp_dbg ("BLC offsets: %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d", + self->blc_offsets[0], self->blc_offsets[1], + self->blc_offsets[2], self->blc_offsets[3], + self->blc_offsets[4], self->blc_offsets[5], + self->blc_offsets[6], self->blc_offsets[7], + self->blc_offsets[8], self->blc_offsets[9], + self->blc_offsets[10], self->blc_offsets[11], + self->blc_offsets[12], self->blc_offsets[13], + self->blc_offsets[14], self->blc_offsets[15]); + } + + /* Reference image: 150×100 at fw_data[0x1df8] */ + if (self->fw_data_len >= SECUGEN_REF_OFFSET + SECUGEN_REF_SIZE) + { + const guint8 *ref_src = self->fw_data + SECUGEN_REF_OFFSET; + + if (!self->ref_image) + self->ref_image = g_malloc (SECUGEN_IMG_SIZE); + + secugen_resize_ref_image (ref_src, self->ref_image); + self->has_ref_image = TRUE; + fp_dbg ("Flat-field reference image extracted and resized to %dx%d", + SECUGEN_IMG_WIDTH, SECUGEN_IMG_HEIGHT); + } + else + { + fp_warn ("FW data too short for reference image: %zu < %d", + self->fw_data_len, + SECUGEN_REF_OFFSET + SECUGEN_REF_SIZE); + } + + /* Blend cal_value: uint16 at fw_data[0x5892] */ + if (self->fw_data_len >= SECUGEN_CAL_VALUE_FW + 2) + { + guint16 raw_val; + + memcpy (&raw_val, self->fw_data + SECUGEN_CAL_VALUE_FW, 2); + self->cal_value = GUINT16_FROM_LE (raw_val); + fp_dbg ("Blend cal_value from FW: %u", self->cal_value); + } + + /* Sharpen parameters */ + if (self->fw_data_len >= SECUGEN_SHARPEN_AMOUNT_FW + 1) + { + self->sharpen_threshold = self->fw_data[SECUGEN_SHARPEN_THRESH_FW]; + self->sharpen_amount = self->fw_data[SECUGEN_SHARPEN_AMOUNT_FW]; + self->sharpen_limit = 10; /* Default; exact FW offset is past our read */ + self->sharpen_enabled = (self->sharpen_threshold > 0 && + self->sharpen_amount > 0); + fp_dbg ("Sharpen: threshold=%u amount=%u limit=%u enabled=%d", + self->sharpen_threshold, self->sharpen_amount, + self->sharpen_limit, self->sharpen_enabled); + } + + fpi_ssm_next_state (transfer->ssm); + } +} + +/* Readback callback for frame window regs (8 entries) */ +static void +init_frame_readback_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error) +{ + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + + if (error) + { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + self->last_reg_rd = transfer->buffer[0]; + self->init_reg_idx++; + + if (self->init_reg_idx < (int) N_FRAME_WINDOW_REGS) + fpi_ssm_jump_to_state (transfer->ssm, INIT_FRAME_I2C_WRITE); + else + { + self->init_reg_idx = 0; + fpi_ssm_next_state (transfer->ssm); + } +} + +/* Calibration bulk read callback — save background frame */ +static void +init_cal_bulk_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error) +{ + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + + if (error) + { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + memcpy (self->cal_raw, transfer->buffer, + MIN ((gsize) transfer->actual_length, (gsize) SECUGEN_BULK_BUF_SIZE)); + fpi_ssm_next_state (transfer->ssm); +} + +static void +init_run_state (FpiSsm *ssm, FpDevice *_dev) +{ + FpImageDevice *dev = FP_IMAGE_DEVICE (_dev); + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (_dev); + switch (fpi_ssm_get_cur_state (ssm)) + { + case INIT_GET_DEVICE_ID: + secugen_ctrl_in (ssm, dev, SECUGEN_REQ_GET_DEVICE_ID, + 0x0000, 0, 30, init_device_id_cb); + break; + + /* ---- Phase 1: Main I2C init (54 regs) ---- */ + + case INIT_I2C_WRITE: + { + const struct secugen_reg_entry *entry = + &sido020a_init_regs[self->init_reg_idx]; + secugen_i2c_write (ssm, dev, entry->reg, entry->val); + } + break; + + case INIT_I2C_READBACK: + { + const struct secugen_reg_entry *entry = + &sido020a_init_regs[self->init_reg_idx]; + secugen_i2c_read (ssm, dev, entry->reg, init_i2c_readback_cb); + } + break; + + /* ---- Phase 2: Power enable + late regs ---- */ + + case INIT_POWER_ENABLE: + /* Rewrite reg 0x03 from 0x02 (reset) to 0x05 (enable) */ + secugen_i2c_write (ssm, dev, 0x03, 0x05); + break; + + case INIT_LATE_I2C_WRITE: + { + const struct secugen_reg_entry *entry; + + entry = &late_init_regs[self->init_reg_idx]; + secugen_i2c_write (ssm, dev, entry->reg, entry->val); + } + break; + + case INIT_LATE_I2C_READBACK: + { + const struct secugen_reg_entry *entry = + &late_init_regs[self->init_reg_idx]; + secugen_i2c_read (ssm, dev, entry->reg, init_late_readback_cb); + } + break; + + /* ---- Phase 3: Capture window config ---- */ + + case INIT_WINDOW_I2C_WRITE: + { + const struct secugen_reg_entry *entry; + + entry = &capture_window_regs[self->init_reg_idx]; + secugen_i2c_write (ssm, dev, entry->reg, entry->val); + } + break; + + case INIT_WINDOW_I2C_READBACK: + { + const struct secugen_reg_entry *entry = + &capture_window_regs[self->init_reg_idx]; + secugen_i2c_read (ssm, dev, entry->reg, init_window_readback_cb); + } + break; + + /* ---- Phase 4: START_CAPTURE + FW data reads ---- */ + + case INIT_PRE_FW_CAPTURE: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_CAPTURE, 0x0001, 0); + break; + + case INIT_FW_READ: + { + guint16 offset = SECUGEN_FW_DATA_START + + (self->fw_read_idx * SECUGEN_FW_DATA_CHUNK); + gsize len = (self->fw_read_idx == SECUGEN_FW_DATA_CHUNKS - 1) + ? SECUGEN_FW_DATA_LAST_LEN + : SECUGEN_FW_DATA_CHUNK; + + { + FpiUsbTransfer *transfer = fpi_usb_transfer_new (FP_DEVICE (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, + SECUGEN_REQ_READ_FW_DATA, + 0x0000, offset, len); + transfer->ssm = ssm; + fpi_usb_transfer_submit (transfer, SECUGEN_FW_READ_TIMEOUT, NULL, + init_fw_read_cb, NULL); + } + } + break; + + /* ---- Phase 5: Calibration capture ---- */ + + case INIT_CAL_REG32: + /* Must clear reg 0x32 before each image capture */ + secugen_i2c_write (ssm, dev, 0x32, 0x00); + break; + + case INIT_CAL_EXPOSURE: + self->exposure = SECUGEN_EXPOSURE_INIT; + secugen_set_exposure (ssm, dev, SECUGEN_EXPOSURE_INIT); + break; + + case INIT_CAL_START_CAPTURE: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_CAPTURE, 0x0001, 0); + break; + + case INIT_CAL_START_STREAM: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_STREAM, 0x0000, 0); + break; + + case INIT_CAL_BULK_READ: + { + FpiUsbTransfer *transfer = fpi_usb_transfer_new (FP_DEVICE (dev)); + + fpi_usb_transfer_fill_bulk (transfer, SECUGEN_EP_DATA, + SECUGEN_BULK_BUF_SIZE); + transfer->ssm = ssm; + fpi_usb_transfer_submit (transfer, SECUGEN_BULK_TIMEOUT, NULL, + init_cal_bulk_cb, NULL); + } + break; + + case INIT_CAL_STOP_STREAM: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_STOP_STREAM, 0x0000, 0); + break; + + /* ---- Phase 6: Frame window config ---- */ + + case INIT_FRAME_I2C_WRITE: + { + const struct secugen_reg_entry *entry; + + entry = &frame_window_regs[self->init_reg_idx]; + secugen_i2c_write (ssm, dev, entry->reg, entry->val); + } + break; + + case INIT_FRAME_I2C_READBACK: + { + const struct secugen_reg_entry *entry = + &frame_window_regs[self->init_reg_idx]; + secugen_i2c_read (ssm, dev, entry->reg, init_frame_readback_cb); + } + break; + + /* ---- Phase 7: Post-calibration tuning ---- */ + + case INIT_POST_REG32_CLEAR: + secugen_i2c_write (ssm, dev, 0x32, 0x00); + break; + + case INIT_POST_EXPOSURE: + self->exposure = SECUGEN_EXPOSURE_NORMAL; + secugen_set_exposure (ssm, dev, SECUGEN_EXPOSURE_NORMAL); + break; + + case INIT_POST_REG32_CLEAR2: + secugen_i2c_write (ssm, dev, 0x32, 0x00); + break; + + case INIT_POST_REG30: + /* Frame control transitions from init value 0x01 to 0x04 */ + secugen_i2c_write (ssm, dev, 0x30, 0x04); + break; + + case INIT_POST_REG31: + /* Frame size transitions from init value 0xc0 to 0x5c */ + secugen_i2c_write (ssm, dev, 0x31, 0x5c); + break; + + case INIT_POST_REG32_CLEAR3: + secugen_i2c_write (ssm, dev, 0x32, 0x00); + break; + + case INIT_POST_REG32_SET: + /* Final 0x32 value for operational mode */ + secugen_i2c_write (ssm, dev, 0x32, 0x24); + break; + + case INIT_POST_EXPOSURE2: + secugen_set_exposure (ssm, dev, SECUGEN_EXPOSURE_NORMAL); + break; + + /* ---- Phase 8: Final calibration capture (operational settings) ---- */ + + case INIT_FINAL_REG32: + /* Clear reg 0x32 before capture (same as every capture) */ + secugen_i2c_write (ssm, dev, 0x32, 0x00); + break; + + case INIT_FINAL_START_CAPTURE: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_CAPTURE, 0x0001, 0); + break; + + case INIT_FINAL_START_STREAM: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_STREAM, 0x0000, 0); + break; + + case INIT_FINAL_BULK_READ: + { + FpiUsbTransfer *transfer = fpi_usb_transfer_new (FP_DEVICE (dev)); + + fpi_usb_transfer_fill_bulk (transfer, SECUGEN_EP_DATA, + SECUGEN_BULK_BUF_SIZE); + transfer->ssm = ssm; + fpi_usb_transfer_submit (transfer, SECUGEN_BULK_TIMEOUT, NULL, + init_cal_bulk_cb, NULL); + } + break; + + case INIT_FINAL_STOP_STREAM: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_STOP_STREAM, 0x0000, 0); + break; + } +} + +/* ================================================================ + * Image Processing Pipeline (SDK-matching) + * + * Matches the SDK's CSIDO020A::ReadImage + DoModify pipeline: + * 1. Blc::PixelCompensation — band compensation at 956x688 + * 2. CImageProc::UnsharpMask — edge-aware sharpening at 956x688 + * 3. Unperspective::Resize — bilinear downsample to 300x400 + * 4. CSIDO020A::Blend — flat-field correction at 300x400 + * 5. CSIDO020A::SharpenImage — 6-directional sharpening at 300x400 + * 6. Invert — bitwise NOT (~pixel) + * ================================================================ */ + +/* Brightness-dependent offset scaling (SDK Blc::PixelCompensationPixel). + * Thresholds confirmed from disassembly: 200, 160, 100, 70, 26. */ +static inline int +blc_scale_offset (int pixel, int offset) +{ + if (pixel > 200) + return offset; + else if (pixel > 160) + return (offset * 3) >> 2; + else if (pixel > 100) + return offset >> 1; + else if (pixel > 70) + return offset >> 2; + else if (pixel >= 26) + return offset >> 3; + else + return 0; +} + +/* SDK Blc::PixelCompensation — per-region band compensation. + * Region layout: 2 rows × 8 columns (16 regions). + * Processes every other row with 3-phase dithering pattern. */ +static void +secugen_blc_compensate (guint8 *image, + int width, + int height, + const gint16 *offsets) +{ + static const int phase_table[12] = { + 7, 11, 13, 14, 5, 3, 6, 9, 4, 8, 2, 1 + }; + int half_h = height / 2; + int eighth_w = width / 8; + int region; + + for (region = 0; region < 16; region++) + { + int rc = region % 8; + int rr = region / 8; + int x0 = rc * eighth_w; + int x1 = (rc == 7) ? width - 1 : (rc + 1) * eighth_w - 1; + int y0 = rr * half_h; + int y1 = (rr == 1) ? height - 1 : half_h - 1; + int offset = offsets[region]; + int phase_counter = -1; + int row, col; + + for (row = y0 + 1; row <= y1; row += 2) + { + phase_counter++; + if (phase_counter > 2) + phase_counter = 0; + + for (col = x0; col <= x1; col++) + { + int idx = row * width + col; + int pixel = image[idx]; + int scaled = blc_scale_offset (pixel, offset); + int quot = scaled / 100; + int remainder = scaled - quot * 100; + int abs_rem = remainder < 0 ? -remainder : remainder; + int extra = 0; + int bit_idx = (col - x0) & 3; + int result; + + if (abs_rem > 74) + extra = (phase_table[phase_counter] >> bit_idx) & 1; + else if (abs_rem > 49) + extra = (phase_table[phase_counter + 4] >> bit_idx) & 1; + else if (abs_rem > 24) + extra = (phase_table[phase_counter + 8] >> bit_idx) & 1; + + if (scaled >= 0) + result = pixel - (quot + extra); + else + result = pixel - (quot - extra); + + if (result < 0) + result = 0; + else if (result > 255) + result = 255; + image[idx] = (guint8) result; + } + } + } +} + +/* SDK CImageProc::UnsharpMask — edge-aware sharpening. + * 3×3 box filter, threshold = pixel/8 + 2. + * Only sharpens pixels where any neighbor differs by more than threshold. + * Boost = (center - averaged) / 2, only for bright-side edges. */ +static void +secugen_unsharp_mask (guint8 *image, int width, int height) +{ + int total = width * height; + guint8 *original; + guint8 *averaged; + int r, c; + + original = g_malloc (total); + memcpy (original, image, total); + averaged = g_malloc (total); + memcpy (averaged, original, total); + + /* 3×3 box filter (AverageFilter with radius=1) */ + for (r = 1; r < height - 1; r++) + for (c = 1; c < width - 1; c++) + { + int sum = original[(r - 1) * width + c - 1] + + original[(r - 1) * width + c] + + original[(r - 1) * width + c + 1] + + original[r * width + c - 1] + + original[r * width + c] + + original[r * width + c + 1] + + original[(r + 1) * width + c - 1] + + original[(r + 1) * width + c] + + original[(r + 1) * width + c + 1]; + + averaged[r * width + c] = (guint8) (sum / 9); + } + + /* Edge-aware sharpening (SDK skips top 2 rows, bottom 1 row, + * left/right 1 column) */ + for (r = 2; r < height - 1; r++) + for (c = 1; c < width - 1; c++) + { + int idx = r * width + c; + int center = original[idx]; + int threshold = (center >> 3) + 2; + + if (abs (center - original[idx - 1]) > threshold || + abs (center - original[idx + 1]) > threshold || + abs (center - original[idx - width]) > threshold || + abs (center - original[idx + width]) > threshold) + { + int avg = averaged[idx]; + + if (center > avg + 1) + { + int boost = (center - avg) / 2; + int result = center + boost; + + if (result > 255) + result = 255; + image[idx] = (guint8) result; + } + /* else: keep original (already in image) */ + } + /* else: keep original */ + } + + g_free (averaged); + g_free (original); +} + +/* Bilinear downsample from raw sensor to output dimensions. */ +static void +secugen_resize_bilinear (const guint8 *src, int src_w, int src_h, + guint8 *dst, int dst_w, int dst_h) +{ + int r, c; + + for (r = 0; r < dst_h; r++) + { + float src_rf = r * (src_h - 1) / (float) (dst_h - 1); + int r0 = (int) src_rf; + int r1 = r0 + 1; + float fr = src_rf - r0; + + if (r1 >= src_h) + r1 = src_h - 1; + + for (c = 0; c < dst_w; c++) + { + float src_cf = c * (src_w - 1) / (float) (dst_w - 1); + int c0 = (int) src_cf; + int c1 = c0 + 1; + float fc = src_cf - c0; + float val; + int pixel; + + if (c1 >= src_w) + c1 = src_w - 1; + + val = (1 - fr) * (1 - fc) * src[r0 * src_w + c0] + + (1 - fr) * fc * src[r0 * src_w + c1] + + fr * (1 - fc) * src[r1 * src_w + c0] + + fr * fc * src[r1 * src_w + c1]; + + pixel = (int) (val + 0.5f); + if (pixel > 255) + pixel = 255; + dst[r * dst_w + c] = (guint8) pixel; + } + } +} + +/* SDK CSIDO020A::Blend — flat-field correction. + * Formula: result = pixel + ((cal_val - ref[i]) * curve[pixel]) >> 8 + * The curve is an S-curve that protects dark pixels and applies + * full correction to bright pixels. */ +static void +secugen_blend (guint8 *image, + int width, + int height, + const guint8 *ref_image, + int cal_val, + const guint8 *curve) +{ + int r, c; + + for (r = 0; r < height; r++) + for (c = 0; c < width; c++) + { + int idx = r * width + c; + int pixel = image[idx]; + int ref_val = ref_image[idx]; + int curve_val = curve[pixel]; + int correction = ((cal_val - ref_val) * curve_val) >> 8; + int result = pixel + correction; + + if (result > 255) + result = 255; + else if (result < 0) + result = 0; + image[idx] = (guint8) result; + } +} + +/* SDK CSIDO020A::SharpenImage — 6-directional gradient sharpening. + * Only enhances pixels where ALL 6 gradients agree on edge direction. + * Case 1: all gradients > threshold (peak) → positive boost. + * Case 2: all gradients < -threshold (valley) → negative boost. */ +static void +secugen_sharpen (const guint8 *src, + guint8 *dst, + int width, + int height, + int threshold, + int amount, + int limit) +{ + int neg_threshold = -threshold; + int neg_amount = -amount; + int r, c; + + for (r = 0; r < height; r++) + for (c = 0; c < width; c++) + { + int idx = r * width + c; + + if (r == 0 || r == height - 1 || c == 0 || c == width - 1) + { + dst[idx] = src[idx]; + continue; + } + + { + int center = src[idx]; + int d_up = center - src[(r - 1) * width + c]; + int d_down = center - src[(r + 1) * width + c]; + int d_left = center - src[r * width + c - 1]; + int d_right = center - src[r * width + c + 1]; + int d_ul = src[r * width + c - 1] - + src[(r - 1) * width + c - 1]; + int d_dr = src[r * width + c + 1] - + src[(r + 1) * width + c + 1]; + + if (d_up > threshold && d_down > threshold && + d_left > threshold && d_right > threshold && + d_ul > threshold && d_dr > threshold) + { + int g1 = MIN (MIN (d_up, d_left), amount); + int g2 = MIN (MIN (d_down, d_right), amount); + int g3 = MIN (MIN (d_ul, d_dr), amount); + int avg = (g1 + g2 + g3) / 3; + int boost = (avg - threshold) * limit / 10; + int result = center + boost; + + if (result > 255) result = 255; + else if (result < 0) result = 0; + dst[idx] = (guint8) result; + } + else if (d_up < neg_threshold && d_down < neg_threshold && + d_left < neg_threshold && d_right < neg_threshold && + d_ul < neg_threshold && d_dr < neg_threshold) + { + int g1 = MAX (MAX (d_up, d_left), neg_amount); + int g2 = MAX (MAX (d_down, d_right), neg_amount); + int g3 = MAX (MAX (d_ul, d_dr), neg_amount); + int avg = (g1 + g2 + g3) / 3; + int boost = (avg + threshold) * limit / 10; + int result = center + boost; + + if (result > 255) result = 255; + else if (result < 0) result = 0; + dst[idx] = (guint8) result; + } + else + { + dst[idx] = src[idx]; + } + } + } +} + +/* ================================================================ + * Capture State Machine + * + * SDK capture sequence from USB captures: + * I2C 0x32 = 0x00 -> SET_EXPOSURE -> START_CAPTURE -> + * START_STREAM -> bulk read 657KB -> STOP_STREAM -> GET_STATUS + * ================================================================ */ + +enum capture_states { + CAPTURE_REG32_CLEAR = 0, + CAPTURE_LED_ON, + CAPTURE_SET_EXPOSURE, + CAPTURE_START, + CAPTURE_STREAM_ON, + CAPTURE_BULK_READ, + CAPTURE_STREAM_OFF, + CAPTURE_GET_STATUS, + CAPTURE_DONE, + CAPTURE_NUM_STATES, +}; + +static void +capture_status_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error) +{ + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + + if (error) + { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + memcpy (self->last_status, transfer->buffer, + MIN (transfer->actual_length, 4)); + fpi_ssm_next_state (transfer->ssm); +} + +static void +capture_bulk_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error) +{ + if (error) + { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + fpi_ssm_set_data (transfer->ssm, + fpi_usb_transfer_ref (transfer), + (GDestroyNotify) fpi_usb_transfer_unref); + fpi_ssm_next_state (transfer->ssm); +} + +static void +capture_run_state (FpiSsm *ssm, FpDevice *_dev) +{ + FpImageDevice *dev = FP_IMAGE_DEVICE (_dev); + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (_dev); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case CAPTURE_REG32_CLEAR: + /* Must clear reg 0x32 before each capture (SDK pattern) */ + secugen_i2c_write (ssm, dev, 0x32, 0x00); + break; + + case CAPTURE_LED_ON: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_LED_CONTROL, 0x0001, 0); + break; + + case CAPTURE_SET_EXPOSURE: + secugen_set_exposure (ssm, dev, self->exposure); + break; + + case CAPTURE_START: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_CAPTURE, 0x0001, 0); + break; + + case CAPTURE_STREAM_ON: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_STREAM, 0x0000, 0); + break; + + case CAPTURE_BULK_READ: + { + FpiUsbTransfer *transfer = fpi_usb_transfer_new (FP_DEVICE (dev)); + + fpi_usb_transfer_fill_bulk (transfer, SECUGEN_EP_DATA, + SECUGEN_BULK_BUF_SIZE); + transfer->ssm = ssm; + fpi_usb_transfer_submit (transfer, SECUGEN_BULK_TIMEOUT, NULL, + capture_bulk_cb, NULL); + } + break; + + case CAPTURE_STREAM_OFF: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_STOP_STREAM, 0x0000, 0); + break; + + case CAPTURE_GET_STATUS: + secugen_ctrl_in (ssm, dev, SECUGEN_REQ_GET_STATUS, + 0x0000, 0, 4, capture_status_cb); + break; + + case CAPTURE_DONE: + { + FpiUsbTransfer *img_transfer = fpi_ssm_get_data (ssm); + FpImage *img; + guint8 *raw; + int i; + + if (!img_transfer || img_transfer->actual_length < SECUGEN_RAW_SIZE) + { + fp_warn ("Short image data: got %ld, expected %d", + img_transfer ? (long) img_transfer->actual_length : 0, + SECUGEN_RAW_SIZE); + fpi_ssm_mark_failed (ssm, + fpi_device_error_new (FP_DEVICE_ERROR_PROTO)); + return; + } + + img = fp_image_new (SECUGEN_IMG_WIDTH, SECUGEN_IMG_HEIGHT); + img->ppmm = SECUGEN_PPMM; + img->flags = FPI_IMAGE_V_FLIPPED | FPI_IMAGE_H_FLIPPED; + + /* Work on a copy of the raw 956x688 sensor data */ + raw = g_malloc (SECUGEN_RAW_SIZE); + memcpy (raw, img_transfer->buffer, SECUGEN_RAW_SIZE); + + /* + * SDK-matching image processing pipeline: + * 1. Blc::PixelCompensation at 956x688 + * 2. CImageProc::UnsharpMask at 956x688 + * 3. Bilinear downsample 956x688 → 300x400 + * 4. CSIDO020A::Blend at 300x400 + * 5. CSIDO020A::SharpenImage at 300x400 (conditional) + * 6. Bitwise NOT invert + */ + + /* Step 1: BLC band compensation */ + if (self->has_blc_offsets) + { + secugen_blc_compensate (raw, SECUGEN_RAW_WIDTH, + SECUGEN_RAW_HEIGHT, + self->blc_offsets); + } + + /* Step 2: Edge-aware unsharp mask */ + secugen_unsharp_mask (raw, SECUGEN_RAW_WIDTH, SECUGEN_RAW_HEIGHT); + + /* Step 3: Bilinear downsample to 300x400 */ + secugen_resize_bilinear (raw, + SECUGEN_RAW_WIDTH, SECUGEN_RAW_HEIGHT, + img->data, + SECUGEN_IMG_WIDTH, SECUGEN_IMG_HEIGHT); + + /* Step 4: Blend flat-field correction */ + if (self->has_ref_image) + { + secugen_blend (img->data, + SECUGEN_IMG_WIDTH, SECUGEN_IMG_HEIGHT, + self->ref_image, self->cal_value, + g_blend_curve); + } + + /* Step 5: Directional sharpening (conditional) */ + if (self->sharpen_enabled) + { + guint8 *sharpened = g_malloc (SECUGEN_IMG_SIZE); + + secugen_sharpen (img->data, sharpened, + SECUGEN_IMG_WIDTH, SECUGEN_IMG_HEIGHT, + self->sharpen_threshold, + self->sharpen_amount, + self->sharpen_limit); + memcpy (img->data, sharpened, SECUGEN_IMG_SIZE); + g_free (sharpened); + } + + /* Step 6: Invert (bitwise NOT, same as SDK's notb loop) */ + for (i = 0; i < SECUGEN_IMG_SIZE; i++) + img->data[i] = ~img->data[i]; + + g_free (raw); + + fpi_image_device_image_captured (dev, img); + fpi_ssm_mark_completed (ssm); + } + break; + } +} + +static void +capture_ssm_complete (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + FpImageDevice *imgdev = FP_IMAGE_DEVICE (dev); + + if (error) + { + fp_warn ("Capture failed: %s", error->message); + fpi_image_device_session_error (imgdev, error); + } +} + +/* No-op callback for fire-and-forget USB transfers (e.g. LED control) */ +static void +secugen_noop_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error) +{ + if (error) + { + fp_warn ("Fire-and-forget transfer failed: %s", error->message); + g_error_free (error); + } +} + +/* ================================================================ + * Finger Detection (Image-Based) + * + * The SIDO020A has no touch/proximity chip. GET_STATUS always returns + * zeros. The SDK's CAutoOn class detects finger presence by capturing + * preview frames and checking mean brightness: + * - No finger: dark image (mean ~20) + * - Finger present: reflected LED light (mean ~70+) + * + * We implement this as a detect SSM that captures a frame, computes + * mean brightness of the central region, and reports finger status. + * If no finger, we schedule another poll after SECUGEN_FINGER_POLL_MS. + * ================================================================ */ + +enum detect_states { + DETECT_REG32_CLEAR = 0, + DETECT_SET_EXPOSURE, + DETECT_START_CAPTURE, + DETECT_START_STREAM, + DETECT_BULK_READ, + DETECT_STOP_STREAM, + DETECT_ANALYZE, + DETECT_NUM_STATES, +}; + +static void detect_start (FpImageDevice *dev); +static gboolean detect_retry_cb (gpointer user_data); + +static void +detect_bulk_cb (FpiUsbTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error) +{ + if (error) + { + fpi_ssm_mark_failed (transfer->ssm, error); + return; + } + + fpi_ssm_set_data (transfer->ssm, + fpi_usb_transfer_ref (transfer), + (GDestroyNotify) fpi_usb_transfer_unref); + fpi_ssm_next_state (transfer->ssm); +} + +static void +detect_run_state (FpiSsm *ssm, FpDevice *_dev) +{ + FpImageDevice *dev = FP_IMAGE_DEVICE (_dev); + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (_dev); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case DETECT_REG32_CLEAR: + secugen_i2c_write (ssm, dev, 0x32, 0x00); + break; + + case DETECT_SET_EXPOSURE: + secugen_set_exposure (ssm, dev, self->exposure); + break; + + case DETECT_START_CAPTURE: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_CAPTURE, 0x0001, 0); + break; + + case DETECT_START_STREAM: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_STREAM, 0x0000, 0); + break; + + case DETECT_BULK_READ: + { + FpiUsbTransfer *transfer = fpi_usb_transfer_new (FP_DEVICE (dev)); + + fpi_usb_transfer_fill_bulk (transfer, SECUGEN_EP_DATA, + SECUGEN_BULK_BUF_SIZE); + transfer->ssm = ssm; + fpi_usb_transfer_submit (transfer, SECUGEN_BULK_TIMEOUT, NULL, + detect_bulk_cb, NULL); + } + break; + + case DETECT_STOP_STREAM: + secugen_ctrl_out (ssm, dev, SECUGEN_REQ_STOP_STREAM, 0x0000, 0); + break; + + case DETECT_ANALYZE: + { + FpiUsbTransfer *img_transfer = fpi_ssm_get_data (ssm); + guint64 sum = 0; + /* Use central 50% of the 956x688 raw sensor area */ + int start_row = SECUGEN_RAW_HEIGHT / 4; + int end_row = 3 * SECUGEN_RAW_HEIGHT / 4; + int start_col = SECUGEN_RAW_WIDTH / 4; + int end_col = 3 * SECUGEN_RAW_WIDTH / 4; + int count = 0; + guint mean; + int y, x; + + /* Compute mean brightness of central 50% region */ + for (y = start_row; y < end_row; y++) + for (x = start_col; x < end_col; x++) + { + int idx = y * SECUGEN_RAW_WIDTH + x; + + if (idx < (int) img_transfer->actual_length && + idx < SECUGEN_BULK_BUF_SIZE) + { + int val = (int) img_transfer->buffer[idx] - + (int) self->cal_raw[idx]; + + if (val < 0) + val = 0; + sum += val; + count++; + } + } + + mean = count > 0 ? (guint) (sum / count) : 0; + fp_dbg ("Finger detect: mean brightness = %u (threshold %d)", + mean, SECUGEN_FINGER_THRESHOLD); + + if (mean >= SECUGEN_FINGER_THRESHOLD) + { + fp_dbg ("Finger detected!"); + fpi_image_device_report_finger_status (dev, TRUE); + } + else + { + /* + * No finger — update calibration reference from this frame + * only if truly empty (mean < 5). This avoids contaminating + * the reference with partial finger touches. + */ + if (mean <= 5) + memcpy (self->cal_raw, img_transfer->buffer, + MIN ((gsize) img_transfer->actual_length, + (gsize) SECUGEN_BULK_BUF_SIZE)); + + /* Schedule another poll */ + self->finger_poll_source = + g_timeout_add (SECUGEN_FINGER_POLL_MS, + (GSourceFunc) detect_retry_cb, dev); + } + + fpi_ssm_mark_completed (ssm); + } + break; + } +} + +static gboolean +detect_retry_cb (gpointer user_data) +{ + FpImageDevice *dev = FP_IMAGE_DEVICE (user_data); + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + + self->finger_poll_source = 0; + detect_start (dev); + + return G_SOURCE_REMOVE; +} + +static void +detect_ssm_complete (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + if (error) + { + fp_warn ("Finger detect failed: %s", error->message); + fpi_image_device_session_error (FP_IMAGE_DEVICE (dev), error); + } +} + +static void +detect_start (FpImageDevice *dev) +{ + FpiSsm *ssm; + + ssm = fpi_ssm_new (FP_DEVICE (dev), detect_run_state, DETECT_NUM_STATES); + fpi_ssm_start (ssm, detect_ssm_complete); +} + +static gboolean +finger_off_cb (gpointer user_data) +{ + FpImageDevice *dev = FP_IMAGE_DEVICE (user_data); + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + + self->finger_poll_source = 0; + fpi_image_device_report_finger_status (dev, FALSE); + + return G_SOURCE_REMOVE; +} + +/* ================================================================ + * Device Lifecycle + * ================================================================ */ + +static void +dev_init (FpImageDevice *dev) +{ + GError *error = NULL; + g_autoptr(GPtrArray) interfaces = NULL; + GUsbInterface *iface = NULL; + int i; + + interfaces = g_usb_device_get_interfaces ( + fpi_device_get_usb_device (FP_DEVICE (dev)), &error); + if (error) + { + fpi_image_device_open_complete (dev, error); + return; + } + + /* Find our vendor-specific interface (0xFF/0xFF/0xFF) */ + for (i = 0; i < (int) interfaces->len; i++) + { + GUsbInterface *cur = g_ptr_array_index (interfaces, i); + + if (g_usb_interface_get_class (cur) == 0xFF && + g_usb_interface_get_subclass (cur) == 0xFF && + g_usb_interface_get_protocol (cur) == 0xFF) + { + iface = cur; + break; + } + } + + if (!iface) + { + fpi_image_device_open_complete (dev, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Could not find fingerprint interface")); + return; + } + + { + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + + self->iface_num = g_usb_interface_get_number (iface); + + if (!g_usb_device_claim_interface ( + fpi_device_get_usb_device (FP_DEVICE (dev)), + self->iface_num, 0, &error)) + { + fpi_image_device_open_complete (dev, error); + return; + } + + /* Allocate buffers */ + self->cal_raw = g_malloc0 (SECUGEN_BULK_BUF_SIZE); + self->fw_data = g_malloc0 (SECUGEN_FW_DATA_SIZE); + self->fw_data_len = 0; + self->ref_image = NULL; + self->has_ref_image = FALSE; + self->has_blc_offsets = FALSE; + self->cal_value = SECUGEN_BLEND_CAL_VAL; /* Default 240, overridden by FW */ + self->sharpen_enabled = FALSE; + } + + fpi_image_device_open_complete (dev, NULL); +} + +static void +dev_deinit (FpImageDevice *dev) +{ + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + GError *error = NULL; + + g_clear_pointer (&self->cal_raw, g_free); + g_clear_pointer (&self->fw_data, g_free); + g_clear_pointer (&self->ref_image, g_free); + self->has_ref_image = FALSE; + + g_usb_device_release_interface ( + fpi_device_get_usb_device (FP_DEVICE (dev)), + self->iface_num, 0, &error); + + fpi_image_device_close_complete (dev, error); +} + +static void +activate_complete (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + FpImageDevice *imgdev = FP_IMAGE_DEVICE (dev); + + if (error) + fp_warn ("Activation failed: %s", error->message); + + fpi_image_device_activate_complete (imgdev, error); +} + +static void +dev_activate (FpImageDevice *dev) +{ + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + FpiSsm *ssm; + + /* Reset init counters */ + self->init_reg_idx = 0; + self->fw_read_idx = 0; + self->exposure = SECUGEN_EXPOSURE_NORMAL; + + ssm = fpi_ssm_new (FP_DEVICE (dev), init_run_state, INIT_NUM_STATES); + fpi_ssm_start (ssm, activate_complete); +} + +static void +dev_deactivate (FpImageDevice *dev) +{ + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + + /* Cancel any pending timeout */ + if (self->finger_poll_source) + { + g_source_remove (self->finger_poll_source); + self->finger_poll_source = 0; + } + + /* Turn off LED (fire and forget) */ + { + FpiUsbTransfer *transfer = fpi_usb_transfer_new (FP_DEVICE (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, + SECUGEN_REQ_LED_CONTROL, + 0x0000, 0, 0); + fpi_usb_transfer_submit (transfer, SECUGEN_CTRL_TIMEOUT, NULL, + secugen_noop_cb, NULL); + } + + fpi_image_device_deactivate_complete (dev, NULL); +} + +static void +dev_change_state (FpImageDevice *dev, + FpiImageDeviceState state) +{ + FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev); + + switch (state) + { + case FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_ON: + fp_dbg ("Awaiting finger (image-based detection)"); + + /* Turn on LED */ + { + FpiUsbTransfer *transfer = fpi_usb_transfer_new (FP_DEVICE (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, + SECUGEN_REQ_LED_CONTROL, + 0x0001, 0, 0); + fpi_usb_transfer_submit (transfer, SECUGEN_CTRL_TIMEOUT, NULL, + secugen_noop_cb, NULL); + } + + /* Start image-based finger detection polling */ + detect_start (dev); + break; + + case FPI_IMAGE_DEVICE_STATE_CAPTURE: + { + FpiSsm *ssm; + + fp_dbg ("Starting image capture"); + + /* Cancel timeout if still running */ + if (self->finger_poll_source) + { + g_source_remove (self->finger_poll_source); + self->finger_poll_source = 0; + } + + ssm = fpi_ssm_new (FP_DEVICE (dev), capture_run_state, + CAPTURE_NUM_STATES); + fpi_ssm_start (ssm, capture_ssm_complete); + } + break; + + case FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_OFF: + fp_dbg ("Waiting for finger off"); + self->finger_poll_source = g_timeout_add (500, finger_off_cb, dev); + break; + + case FPI_IMAGE_DEVICE_STATE_IDLE: + case FPI_IMAGE_DEVICE_STATE_INACTIVE: + case FPI_IMAGE_DEVICE_STATE_ACTIVATING: + case FPI_IMAGE_DEVICE_STATE_DEACTIVATING: + break; + } +} + +/* ================================================================ + * Driver Registration + * ================================================================ */ + +static const FpIdEntry id_table[] = { + { .vid = 0x1162, .pid = 0x2200, }, /* SecuGen Hamster Pro 20 (FDU05) */ + { .vid = 0, .pid = 0, }, /* terminator */ +}; + +static void +fpi_device_secugen_init (FpiDeviceSecugen *self) +{ + self->init_reg_idx = 0; + self->fw_read_idx = 0; + self->finger_poll_source = 0; + self->exposure = SECUGEN_EXPOSURE_NORMAL; +} + +static void +fpi_device_secugen_class_init (FpiDeviceSecugenClass *klass) +{ + FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass); + FpImageDeviceClass *img_class = FP_IMAGE_DEVICE_CLASS (klass); + + dev_class->id = "secugen"; + dev_class->full_name = "SecuGen Hamster Pro 20"; + dev_class->type = FP_DEVICE_TYPE_USB; + dev_class->id_table = id_table; + dev_class->scan_type = FP_SCAN_TYPE_PRESS; + + img_class->img_open = dev_init; + img_class->img_close = dev_deinit; + img_class->activate = dev_activate; + img_class->deactivate = dev_deactivate; + img_class->change_state = dev_change_state; + + img_class->img_width = SECUGEN_IMG_WIDTH; + img_class->img_height = SECUGEN_IMG_HEIGHT; +} diff --git a/libfprint/drivers/secugen/secugen.h b/libfprint/drivers/secugen/secugen.h new file mode 100644 index 00000000..569a6a02 --- /dev/null +++ b/libfprint/drivers/secugen/secugen.h @@ -0,0 +1,151 @@ +/* + * SecuGen Hamster Pro 20 (FDU05) native USB driver for libfprint + * Copyright (C) 2026 + * + * Protocol reverse-engineered from USB packet captures. + * No proprietary code or SDK used. + * + * 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 + */ + +#pragma once + +#include "drivers_api.h" + +/* ---- Device constants ---- */ + +#define SECUGEN_VID 0x1162 +/* Final output image (what libfprint sees) */ +#define SECUGEN_IMG_WIDTH 300 +#define SECUGEN_IMG_HEIGHT 400 +#define SECUGEN_IMG_SIZE (SECUGEN_IMG_WIDTH * SECUGEN_IMG_HEIGHT) /* 120000 */ + +/* Raw sensor array (full SIDO020A readout) */ +#define SECUGEN_RAW_WIDTH 956 +#define SECUGEN_RAW_HEIGHT 688 +#define SECUGEN_RAW_SIZE (SECUGEN_RAW_WIDTH * SECUGEN_RAW_HEIGHT) /* 657728 */ +#define SECUGEN_BULK_BUF_SIZE 657920 /* Actual USB stream: 688*956 + 192 trailing */ +#define SECUGEN_DPI 500 +#define SECUGEN_PPMM 19.685 /* 500 DPI / 25.4 mm/in */ +#define SECUGEN_EP_DATA 0x82 /* Bulk IN endpoint */ + +/* ---- Vendor control transfer bRequest codes ---- */ + +#define SECUGEN_REQ_START_STREAM 1 +#define SECUGEN_REQ_STOP_STREAM 2 +#define SECUGEN_REQ_START_CAPTURE 5 +#define SECUGEN_REQ_READ_FW_DATA 8 +#define SECUGEN_REQ_LED_CONTROL 17 +#define SECUGEN_REQ_GET_STATUS 22 +#define SECUGEN_REQ_I2C_REG 34 +#define SECUGEN_REQ_GET_DEVICE_ID 37 +#define SECUGEN_REQ_SET_EXPOSURE 64 + +/* ---- I2C / SIDO020A sensor ---- */ + +#define SECUGEN_I2C_ADDR 0x0037 /* wValue for I2C transfers */ + +/* ---- Exposure values ---- */ + +#define SECUGEN_EXPOSURE_INIT 1000 /* Initial calibration exposure */ +#define SECUGEN_EXPOSURE_NORMAL 1116 /* Normal capture exposure */ + +/* ---- Calibration / FW data ---- */ + +#define SECUGEN_FW_DATA_START 0x2000 +#define SECUGEN_FW_DATA_CHUNK 4096 +#define SECUGEN_FW_DATA_CHUNKS 6 +#define SECUGEN_FW_DATA_LAST_LEN 2462 /* Last chunk is partial */ +#define SECUGEN_FW_DATA_SIZE 22942 /* Total FW data: 4*4096 + 2*3072 ... */ + +/* Flat-field reference image stored in FW data at struct offset 0x1df8 */ +#define SECUGEN_REF_WIDTH 150 +#define SECUGEN_REF_HEIGHT 100 +#define SECUGEN_REF_SIZE (SECUGEN_REF_WIDTH * SECUGEN_REF_HEIGHT) /* 15000 */ +#define SECUGEN_REF_OFFSET 0x1df8 /* Offset of ref image in FW data */ +#define SECUGEN_BLEND_CAL_VAL 240 /* Target uniform brightness */ + +/* FW data offsets for image processing parameters */ +#define SECUGEN_BLC_OFFSETS_FW 0x0e /* 16 × int16 BLC region offsets */ +#define SECUGEN_CAL_VALUE_FW 0x5892 /* uint16 blend target (usually 240) */ +#define SECUGEN_SHARPEN_THRESH_FW 0x599C /* uint8 sharpening threshold */ +#define SECUGEN_SHARPEN_AMOUNT_FW 0x599D /* uint8 sharpening amount */ + +/* ---- Timeouts (ms) ---- */ + +#define SECUGEN_CTRL_TIMEOUT 2000 +#define SECUGEN_BULK_TIMEOUT 10000 /* Longer timeout for 657KB bulk read */ +#define SECUGEN_FW_READ_TIMEOUT 5000 +#define SECUGEN_FINGER_POLL_MS 200 /* Finger detection polling interval */ +#define SECUGEN_FINGER_THRESHOLD 25 /* Mean brightness above this = finger present */ + +/* ---- I2C register init table entry ---- */ + +struct secugen_reg_entry +{ + guint8 reg; + guint8 val; +}; + +/* ---- GObject type ---- */ + +G_DECLARE_FINAL_TYPE (FpiDeviceSecugen, + fpi_device_secugen, + FPI, + DEVICE_SECUGEN, + FpImageDevice); + +struct _FpiDeviceSecugen +{ + FpImageDevice parent; + + /* Initialization tracking */ + int init_reg_idx; /* Current I2C register being written */ + int fw_read_idx; /* Current calibration data chunk */ + guint8 iface_num; /* Claimed USB interface number */ + + /* Finger detection */ + guint finger_poll_source; /* GLib timeout source ID */ + + /* Calibration / flat-field correction */ + guint8 *cal_raw; /* Background frame at raw sensor res (956x688) */ + guint8 *fw_data; /* Accumulated FW data from device (22942 bytes) */ + gsize fw_data_len; /* Bytes accumulated so far */ + guint8 *ref_image; /* Resized 300x400 flat-field reference image */ + gboolean has_ref_image; /* Whether ref_image was successfully extracted */ + + /* BLC band compensation */ + gint16 blc_offsets[16]; /* Factory-calibrated region offsets from FW */ + gboolean has_blc_offsets; /* Whether BLC offsets were extracted */ + + /* Image processing params from FW */ + guint16 cal_value; /* Blend target brightness (from FW, default 240) */ + guint8 sharpen_threshold; /* Min gradient for sharpening */ + guint8 sharpen_amount; /* Max gradient contribution */ + guint8 sharpen_limit; /* Overall scaling factor (/10) */ + gboolean sharpen_enabled; /* Whether post-resize sharpening is active */ + + /* Capture state */ + guint16 exposure; /* Current exposure value */ + + /* Device info */ + guint8 device_id[30]; + + /* Last register readback */ + guint8 last_reg_rd; + + /* Last status */ + guint8 last_status[4]; +}; diff --git a/libfprint/meson.build b/libfprint/meson.build index 34494813..ba755c72 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' ], + 'secugen' : + [ 'drivers/secugen/secugen.c' ], } helper_sources = { diff --git a/meson.build b/meson.build index baafa19c..86ad3a25 100644 --- a/meson.build +++ b/meson.build @@ -144,6 +144,7 @@ default_drivers = [ 'fpcmoc', 'realtek', 'focaltech_moc', + 'secugen', ] spi_drivers = [