From fbaa97eaeb61d4ec4678d731db598ddc3aed3d53 Mon Sep 17 00:00:00 2001 From: lignah Date: Sun, 7 Dec 2025 15:59:11 +0900 Subject: [PATCH] drivers: add initial Samsung 730B image driver Add an experimental driver for the Samsung 730B USB fingerprint sensor (04e8:730b). The implementation is based on reverse engineering of the Windows traffic and a Python prototype: - repo: https://github.com/lignah/samsung-730b-reverse - script: scripts/samsung_730b_v1_3.py Implemented pieces: - device init sequence (0xC3 control + A9/A8 bulk writes) - image capture using 0xCA control and 256-byte bulk chunks - image layout: 112x96 at offset 180 in the raw buffer - basic image-based finger detection (short probe + ff ratio heuristic) The driver currently exposes the device as a press-type image sensor and can capture usable fingerprints via libfprint. Known limitations: - Finger detection and capture work, but fprintd enrollment UX is still rough (minutiae detection sometimes fails, and higher-level behavior needs tuning). - The driver is marked experimental on purpose; further iteration with libfprint/fprintd maintainers is expected. --- libfprint/drivers/samsung730b.c | 1023 +++++++++++++++++-------------- 1 file changed, 561 insertions(+), 462 deletions(-) diff --git a/libfprint/drivers/samsung730b.c b/libfprint/drivers/samsung730b.c index 460a70c7..319a7d74 100644 --- a/libfprint/drivers/samsung730b.c +++ b/libfprint/drivers/samsung730b.c @@ -5,521 +5,624 @@ #define SAMSUNG730B_VID 0x04e8 #define SAMSUNG730B_PID 0x730b -#define SAMSUNG730B_BULK_EP_IN (0x82 | FPI_USB_ENDPOINT_IN) -#define SAMSUNG730B_BULK_EP_OUT (0x01 | FPI_USB_ENDPOINT_OUT) +#define SAMSUNG730B_EP_IN (0x82 | FPI_USB_ENDPOINT_IN) +#define SAMSUNG730B_EP_OUT (0x01 | FPI_USB_ENDPOINT_OUT) +#define BULK_PACKET_SIZE 256 -#define SAMSUNG730B_IMG_WIDTH 112 -#define SAMSUNG730B_IMG_HEIGHT 96 -#define SAMSUNG730B_DATA_OFFSET 180 +#define IMG_W 112 +#define IMG_H 96 +#define IMG_OFFSET 180 -#define BULK_PACKET_SIZE 256 +/* detect 파라미터 (파이썬 v1.3 기준) */ +#define DETECT_MAX_ROUNDS 5 +#define DETECT_PROBES_PER_ROUND 5 +#define DETECT_PROBE_CHUNKS 6 +#define DETECT_INTERVAL_MS 400 +#define DETECT_INIT_TIMEOUT_MS 500 +#define DETECT_BULK_TIMEOUT_MS 700 -struct _FpiDeviceSamsung730b +/* capture 파라미터 */ +#define CAPTURE_NUM_CHUNKS 85 +#define CAPTURE_START_IDX 0x032a +#define CAPTURE_CTRL_REQ 0xCA +#define CAPTURE_CTRL_VAL 0x0003 + +typedef struct _FpiDeviceSamsung730b { FpImageDevice parent; -}; +} FpiDeviceSamsung730b; G_DECLARE_FINAL_TYPE (FpiDeviceSamsung730b, fpi_device_samsung730b, FPI, DEVICE_SAMSUNG730B, FpImageDevice); - G_DEFINE_TYPE (FpiDeviceSamsung730b, fpi_device_samsung730b, FP_TYPE_IMAGE_DEVICE); +/* ---------- init 시퀀스 ---------- */ - - - -struct Samsung730bCmd +typedef struct { const guint8 *data; gsize len; +} SamsungCmd; + +#define CMD(...) (const guint8[]){ __VA_ARGS__ }, sizeof((const guint8[]){ __VA_ARGS__ }) + +static const SamsungCmd init_cmds[] = { + { CMD(0x4f, 0x80) }, + { CMD(0xa9, 0x4f, 0x80) }, + { CMD(0xa9, 0xb9, 0x00) }, + { CMD(0xa9, 0x60, 0x1b, 0x00) }, + { CMD(0xa9, 0x50, 0x21, 0x00) }, + { CMD(0xa9, 0x61, 0x00, 0x00) }, + { CMD(0xa9, 0x62, 0x00, 0x1a) }, + { CMD(0xa9, 0x63, 0x00, 0x1a) }, + { CMD(0xa9, 0x64, 0x04, 0x0a) }, + { CMD(0xa9, 0x66, 0x0f, 0x80) }, + { CMD(0xa9, 0x67, 0x1b, 0x00) }, + { CMD(0xa9, 0x68, 0x00, 0x0f) }, + { CMD(0xa9, 0x69, 0x00, 0x14) }, + { CMD(0xa9, 0x6a, 0x00, 0x19) }, + { CMD(0xa9, 0x6c, 0x00, 0x19) }, + { CMD(0xa9, 0x40, 0x43, 0x00) }, + { CMD(0xa9, 0x41, 0x6f, 0x00) }, + { CMD(0xa9, 0x55, 0x20, 0x00) }, + { CMD(0xa9, 0x5f, 0x00, 0x00) }, + { CMD(0xa9, 0x52, 0x27, 0x00) }, + { CMD(0xa9, 0x09, 0x00, 0x00) }, + { CMD(0xa9, 0x5d, 0x4d, 0x00) }, + { CMD(0xa9, 0x51, 0xa8, 0x25) }, + { CMD(0xa9, 0x03, 0x00) }, + { CMD(0xa9, 0x38, 0x01, 0x00) }, + { CMD(0xa9, 0x3d, 0xff, 0x0f) }, + { CMD(0xa9, 0x10, 0x60, 0x00) }, + { CMD(0xa9, 0x3b, 0x14, 0x00) }, + { CMD(0xa9, 0x2f, 0xf6, 0xff) }, + { CMD(0xa9, 0x09, 0x00, 0x00) }, + { CMD(0xa9, 0x0c, 0x00) }, + { CMD(0xa8, 0x20, 0x00, 0x00) }, + { CMD(0xa9, 0x04, 0x00) }, + { CMD(0xa8, 0x08, 0x00) }, + { CMD(0xa9, 0x09, 0x00, 0x00) }, + { CMD(0xa8, 0x3e, 0x00, 0x00) }, + { CMD(0xa9, 0x03, 0x00, 0x00) }, + { CMD(0xa8, 0x20, 0x00, 0x00) }, + { CMD(0xa9, 0x10, 0x00, 0x01) }, + { CMD(0xa9, 0x2f, 0xef, 0x00) }, + { CMD(0xa9, 0x09, 0x00, 0x00) }, + { CMD(0xa9, 0x5d, 0x4d, 0x00) }, + { CMD(0xa9, 0x51, 0x3a, 0x25) }, + { CMD(0xa9, 0x0c, 0x00) }, + { CMD(0xa8, 0x20, 0x00, 0x00) }, + { CMD(0xa9, 0x04, 0x00, 0x00) }, + { CMD(0xa9, 0x09, 0x00, 0x00) }, }; -static const guint8 cmd0[] = { 0x4f, 0x80 }; -static const guint8 cmd1[] = { 0xa9, 0x4f, 0x80 }; -static const guint8 cmd2[] = { 0xa9, 0xb9, 0x00 }; -static const guint8 cmd3[] = { 0xa9, 0x60, 0x1b, 0x00 }; -static const guint8 cmd4[] = { 0xa9, 0x50, 0x21, 0x00 }; -static const guint8 cmd5[] = { 0xa9, 0x61, 0x00, 0x00 }; -static const guint8 cmd6[] = { 0xa9, 0x62, 0x00, 0x1a }; -static const guint8 cmd7[] = { 0xa9, 0x63, 0x00, 0x1a }; -static const guint8 cmd8[] = { 0xa9, 0x64, 0x04, 0x0a }; -static const guint8 cmd9[] = { 0xa9, 0x66, 0x0f, 0x80 }; -static const guint8 cmd10[] = { 0xa9, 0x67, 0x1b, 0x00 }; -static const guint8 cmd11[] = { 0xa9, 0x68, 0x00, 0x0f }; -static const guint8 cmd12[] = { 0xa9, 0x69, 0x00, 0x14 }; -static const guint8 cmd13[] = { 0xa9, 0x6a, 0x00, 0x19 }; -static const guint8 cmd14[] = { 0xa9, 0x6c, 0x00, 0x19 }; -static const guint8 cmd15[] = { 0xa9, 0x40, 0x43, 0x00 }; -static const guint8 cmd16[] = { 0xa9, 0x41, 0x6f, 0x00 }; -static const guint8 cmd17[] = { 0xa9, 0x55, 0x20, 0x00 }; -static const guint8 cmd18[] = { 0xa9, 0x5f, 0x00, 0x00 }; -static const guint8 cmd19[] = { 0xa9, 0x52, 0x27, 0x00 }; -static const guint8 cmd20[] = { 0xa9, 0x09, 0x00, 0x00 }; -static const guint8 cmd21[] = { 0xa9, 0x5d, 0x4d, 0x00 }; -static const guint8 cmd22[] = { 0xa9, 0x51, 0xa8, 0x25 }; -static const guint8 cmd23[] = { 0xa9, 0x03, 0x00 }; -static const guint8 cmd24[] = { 0xa9, 0x38, 0x01, 0x00 }; -static const guint8 cmd25[] = { 0xa9, 0x3d, 0xff, 0x0f }; -static const guint8 cmd26[] = { 0xa9, 0x10, 0x60, 0x00 }; -static const guint8 cmd27[] = { 0xa9, 0x3b, 0x14, 0x00 }; -static const guint8 cmd28[] = { 0xa9, 0x2f, 0xf6, 0xff }; -static const guint8 cmd29[] = { 0xa9, 0x09, 0x00, 0x00 }; -static const guint8 cmd30[] = { 0xa9, 0x0c, 0x00 }; -static const guint8 cmd31[] = { 0xa8, 0x20, 0x00, 0x00 }; -static const guint8 cmd32[] = { 0xa9, 0x04, 0x00 }; -static const guint8 cmd33[] = { 0xa8, 0x08, 0x00 }; -static const guint8 cmd34[] = { 0xa9, 0x09, 0x00, 0x00 }; -static const guint8 cmd35[] = { 0xa8, 0x3e, 0x00, 0x00 }; -static const guint8 cmd36[] = { 0xa9, 0x03, 0x00, 0x00 }; -static const guint8 cmd37[] = { 0xa8, 0x20, 0x00, 0x00 }; -static const guint8 cmd38[] = { 0xa9, 0x10, 0x00, 0x01 }; -static const guint8 cmd39[] = { 0xa9, 0x2f, 0xef, 0x00 }; -static const guint8 cmd40[] = { 0xa9, 0x09, 0x00, 0x00 }; -static const guint8 cmd41[] = { 0xa9, 0x5d, 0x4d, 0x00 }; -static const guint8 cmd42[] = { 0xa9, 0x51, 0x3a, 0x25 }; -static const guint8 cmd43[] = { 0xa9, 0x0c, 0x00 }; -static const guint8 cmd44[] = { 0xa8, 0x20, 0x00, 0x00 }; -static const guint8 cmd45[] = { 0xa9, 0x04, 0x00, 0x00 }; -static const guint8 cmd46[] = { 0xa9, 0x09, 0x00, 0x00 }; - -static const struct Samsung730bCmd samsung730b_init_cmds[] = { - { cmd0, sizeof (cmd0) }, - { cmd1, sizeof (cmd1) }, - { cmd2, sizeof (cmd2) }, - { cmd3, sizeof (cmd3) }, - { cmd4, sizeof (cmd4) }, - { cmd5, sizeof (cmd5) }, - { cmd6, sizeof (cmd6) }, - { cmd7, sizeof (cmd7) }, - { cmd8, sizeof (cmd8) }, - { cmd9, sizeof (cmd9) }, - { cmd10, sizeof (cmd10) }, - { cmd11, sizeof (cmd11) }, - { cmd12, sizeof (cmd12) }, - { cmd13, sizeof (cmd13) }, - { cmd14, sizeof (cmd14) }, - { cmd15, sizeof (cmd15) }, - { cmd16, sizeof (cmd16) }, - { cmd17, sizeof (cmd17) }, - { cmd18, sizeof (cmd18) }, - { cmd19, sizeof (cmd19) }, - { cmd20, sizeof (cmd20) }, - { cmd21, sizeof (cmd21) }, - { cmd22, sizeof (cmd22) }, - { cmd23, sizeof (cmd23) }, - { cmd24, sizeof (cmd24) }, - { cmd25, sizeof (cmd25) }, - { cmd26, sizeof (cmd26) }, - { cmd27, sizeof (cmd27) }, - { cmd28, sizeof (cmd28) }, - { cmd29, sizeof (cmd29) }, - { cmd30, sizeof (cmd30) }, - { cmd31, sizeof (cmd31) }, - { cmd32, sizeof (cmd32) }, - { cmd33, sizeof (cmd33) }, - { cmd34, sizeof (cmd34) }, - { cmd35, sizeof (cmd35) }, - { cmd36, sizeof (cmd36) }, - { cmd37, sizeof (cmd37) }, - { cmd38, sizeof (cmd38) }, - { cmd39, sizeof (cmd39) }, - { cmd40, sizeof (cmd40) }, - { cmd41, sizeof (cmd41) }, - { cmd42, sizeof (cmd42) }, - { cmd43, sizeof (cmd43) }, - { cmd44, sizeof (cmd44) }, - { cmd45, sizeof (cmd45) }, - { cmd46, sizeof (cmd46) }, +/* capture 인덱스: 파이썬 CAPTURE_START_INDEX/NUM_CHUNKS 기반 */ +static const guint16 capture_indices[CAPTURE_NUM_CHUNKS] = { + 0x032a, 0x042a, 0x052a, 0x062a, 0x072a, 0x082a, 0x092a, 0x0a2a, + 0x0b2a, 0x0c2a, 0x0d2a, 0x0e2a, 0x0f2a, 0x102a, 0x112a, 0x122a, + 0x132a, 0x142a, 0x152a, 0x162a, 0x172a, 0x182a, 0x192a, 0x1a2a, + 0x1b2a, 0x1c2a, 0x1d2a, 0x1e2a, 0x1f2a, 0x202a, 0x212a, 0x222a, + 0x232a, 0x242a, 0x252a, 0x262a, 0x272a, 0x282a, 0x292a, 0x2a2a, + 0x2b2a, 0x2c2a, 0x2d2a, 0x2e2a, 0x2f2a, 0x302a, 0x312a, 0x322a, + 0x332a, 0x342a, 0x352a, 0x362a, 0x372a, 0x382a, 0x392a, 0x3a2a, + 0x3b2a, 0x3c2a, 0x3d2a, 0x3e2a, 0x3f2a, 0x402a, 0x412a, 0x422a, + 0x432a, 0x442a, 0x452a, 0x462a, 0x472a, 0x482a, 0x492a, 0x4a2a, + 0x4b2a, 0x4c2a, 0x4d2a, 0x4e2a, 0x4f2a, 0x502a, 0x512a, 0x522a, + 0x532a, }; -static const gsize samsung730b_init_cmds_len = - G_N_ELEMENTS (samsung730b_init_cmds); - - -static const guint16 capture_indices[] = { - 0x032a, 0x042a, 0x052a, 0x062a, - 0x072a, 0x082a, 0x092a, 0x0a2a, - 0x0b2a, 0x0c2a, 0x0d2a, 0x0e2a, - 0x0f2a, 0x102a, 0x112a, 0x122a, - 0x132a, 0x142a, 0x152a, 0x162a, - 0x172a, 0x182a, 0x192a, 0x1a2a, - 0x1b2a, 0x1c2a, 0x1d2a, 0x1e2a, - 0x1f2a, 0x202a, 0x212a, 0x222a, - 0x232a, 0x242a, 0x252a, 0x262a, - 0x272a, 0x282a, 0x292a, 0x2a2a, - 0x2b2a, 0x2c2a, 0x2d2a, 0x2e2a, - 0x2f2a, 0x302a, 0x312a, 0x322a, - 0x332a, 0x342a, 0x352a, 0x362a, - 0x372a, 0x382a, 0x392a, 0x3a2a, - 0x3b2a, 0x3c2a, 0x3d2a, 0x3e2a, - 0x3f2a, 0x402a, 0x412a, 0x422a, - 0x432a, 0x442a, 0x452a, 0x462a, - 0x472a, 0x482a, 0x492a, 0x4a2a, - 0x4b2a, 0x4c2a, 0x4d2a, 0x4e2a, - 0x4f2a, 0x502a, 0x512a, 0x522a, - 0x532a, 0x542a, 0x552a, 0x562a, - 0x572a, -}; -static const size_t CAPTURE_NUM_CHUNKS = sizeof(capture_indices) / sizeof(capture_indices[0]); - - +/* ---------- 공용 USB 헬퍼 ---------- */ static gboolean -samsung730b_capture_frame (FpImageDevice *dev, - guint8 **out_buf, - gsize *out_len, - GError **error) +usb_ctrl_out (GUsbDevice *usb_dev, + guint8 req, + guint16 val, + guint16 idx, + const guint8 *data, + gsize len, + guint timeout_ms, + GError **error) { - GUsbDevice *usb_dev; - guint8 *buf; - gsize total_len = 0; - gsize capacity = (gsize) CAPTURE_NUM_CHUNKS * BULK_PACKET_SIZE + 1024; - gsize actual_length; - gboolean ok; - gsize i; + gsize actual = 0; - guint16 windex0; - guint16 windex; - - usb_dev = fpi_device_get_usb_device (FP_DEVICE (dev)); - - buf = g_malloc (capacity); - if (!buf) - { - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NO_SPACE, - "samsung730b: failed to allocate capture buffer"); - return FALSE; - } - - /* ---------- 1) 첫 chunk: 상태 응답만 처리 (데이터 누적 X) ---------- */ - windex0 = capture_indices[0]; - - ok = g_usb_device_control_transfer (usb_dev, - G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, - G_USB_DEVICE_REQUEST_TYPE_VENDOR, - G_USB_DEVICE_RECIPIENT_DEVICE, - 0xCA, /* request */ - 0x0003, /* value */ - windex0, /* idx */ - NULL, /* data */ - 0, /* length */ - &actual_length, - 500, - NULL, - error); - if (!ok) - goto out_fail; - - /* 캡처 시작 명령 (a8 06 00 00...) */ - { - guint8 start_cmd[256] = { 0 }; - - start_cmd[0] = 0xa8; - start_cmd[1] = 0x06; - start_cmd[2] = 0x00; - start_cmd[3] = 0x00; - - ok = g_usb_device_bulk_transfer (usb_dev, - SAMSUNG730B_BULK_EP_OUT, - start_cmd, - sizeof (start_cmd), - &actual_length, - 500, - NULL, - error); - if (!ok) - goto out_fail; - } - - /* 첫 IN: 짧은 상태 응답만 읽고 버림 */ - { - guint8 tmp0[256]; - - ok = g_usb_device_bulk_transfer (usb_dev, - SAMSUNG730B_BULK_EP_IN, - tmp0, - sizeof (tmp0), - &actual_length, - 500, - NULL, - error); - if (!ok) - goto out_fail; - } - - /* ---------- 2) 나머지 chunk: 실제 데이터 + ACK ---------- */ - for (i = 1; i < CAPTURE_NUM_CHUNKS; i++) - { - guint8 tmp[256]; - guint8 ack[256] = { 0 }; - - windex = capture_indices[i]; - - /* CONTROL 0xCA: 청크 설정 */ - ok = g_usb_device_control_transfer (usb_dev, - G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, - G_USB_DEVICE_REQUEST_TYPE_VENDOR, - G_USB_DEVICE_RECIPIENT_DEVICE, - 0xCA, - 0x0003, - windex, - NULL, - 0, - &actual_length, - 500, - NULL, - error); - if (!ok) - goto out_fail; - - /* 데이터 IN (256 bytes 기대) */ - ok = g_usb_device_bulk_transfer (usb_dev, - SAMSUNG730B_BULK_EP_IN, - tmp, - sizeof (tmp), - &actual_length, - 1000, - NULL, - error); - if (!ok) - goto out_fail; - - if (actual_length == 0) - break; - - if (total_len + actual_length > capacity) - { - capacity *= 2; - buf = g_realloc (buf, capacity); - if (!buf) - { - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NO_SPACE, - "samsung730b: realloc failed"); - return FALSE; - } - } - - memcpy (buf + total_len, tmp, actual_length); - total_len += actual_length; - - /* ACK (256 zeros) */ - ok = g_usb_device_bulk_transfer (usb_dev, - SAMSUNG730B_BULK_EP_OUT, - ack, - sizeof (ack), - &actual_length, - 500, - NULL, - error); - if (!ok) - goto out_fail; - } - - *out_buf = buf; - *out_len = total_len; - return TRUE; - -out_fail: - g_free (buf); - *out_buf = NULL; - *out_len = 0; - return FALSE; + return g_usb_device_control_transfer (usb_dev, + G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, + G_USB_DEVICE_REQUEST_TYPE_VENDOR, + G_USB_DEVICE_RECIPIENT_DEVICE, + req, val, idx, + (guchar *) data, len, + &actual, + timeout_ms, + NULL, + error); } -#include -static void -samsung730b_do_capture_and_report (FpImageDevice *dev) +static gboolean +usb_bulk_out (GUsbDevice *usb_dev, + guint8 ep, + const guint8 *data, + gsize len, + guint timeout_ms, + GError **error) { - guint8 *raw = NULL; - gsize raw_len = 0; - GError *error = NULL; - FpImage *image = NULL; - gsize needed; + gsize actual = 0; - needed = SAMSUNG730B_DATA_OFFSET + - (gsize) SAMSUNG730B_IMG_WIDTH * SAMSUNG730B_IMG_HEIGHT; - - if (!samsung730b_capture_frame (dev, &raw, &raw_len, &error)) - goto out_error; - - if (raw_len < needed) - { - g_clear_error (&error); - g_set_error (&error, G_IO_ERROR, G_IO_ERROR_FAILED, - "samsung730b: captured data too short (%"G_GSIZE_FORMAT")", raw_len); - goto out_error; - } - - image = fp_image_new (SAMSUNG730B_IMG_WIDTH, SAMSUNG730B_IMG_HEIGHT); - - { - gsize img_size; - guchar *data = (guchar *) fp_image_get_data (image, &img_size); - - if (img_size < SAMSUNG730B_IMG_WIDTH * SAMSUNG730B_IMG_HEIGHT) - { - g_set_error (&error, G_IO_ERROR, G_IO_ERROR_FAILED, - "samsung730b: image buffer too small (%"G_GSIZE_FORMAT")", img_size); - goto out_error; - } - - memcpy (data, - raw + SAMSUNG730B_DATA_OFFSET, - SAMSUNG730B_IMG_WIDTH * SAMSUNG730B_IMG_HEIGHT); - } - - g_free (raw); - raw = NULL; - - /* 디버그용 덤프 (원하면 유지) */ - g_message ("samsung730b: dumping captured image to /tmp/s730b.pgm"); - - { - FILE *f = fopen ("/tmp/s730b.pgm", "wb"); - if (f) - { - const guchar *data = fp_image_get_data (image, NULL); - - fprintf (f, "P5\n%d %d\n255\n", - SAMSUNG730B_IMG_WIDTH, SAMSUNG730B_IMG_HEIGHT); - fwrite (data, 1, - SAMSUNG730B_IMG_WIDTH * SAMSUNG730B_IMG_HEIGHT, - f); - fclose (f); - } - else - { - g_message ("samsung730b: failed to open /tmp/s730b.pgm for writing"); - } - } -g_object_unref (image); - -out_error: - if (raw) - g_free (raw); - if (image) - g_object_unref (image); - - if (error) - { - g_warning ("samsung730b: capture error: %s", error->message); - g_clear_error (&error); - } + return g_usb_device_bulk_transfer (usb_dev, + ep, + (guchar *) data, + len, + &actual, + timeout_ms, + NULL, + error); } - - -static void -samsung730b_dev_init (FpImageDevice *dev) +static gboolean +usb_bulk_in (GUsbDevice *usb_dev, + guint8 ep, + guint8 *buf, + gsize len, + guint timeout_ms, + gsize *actual_out, + GError **error) +{ + return g_usb_device_bulk_transfer (usb_dev, + ep, + buf, + len, + actual_out, + timeout_ms, + NULL, + error); +} + +/* ---------- 센서 init ---------- */ + +static gboolean +s730b_run_init (GUsbDevice *usb_dev, GError **error) { - GUsbDevice *usb_dev; - GError *error = NULL; guint8 c3_data[16] = { 0x80, 0x84, 0x1e, 0x00, 0x08, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00 }; - guint8 iface = 0; /* 테스트 코드에서 claim한 인터페이스 번호 (보통 0) */ - gboolean ok; - gsize actual_length; gsize i; - usb_dev = fpi_device_get_usb_device (FP_DEVICE (dev)); + if (!usb_ctrl_out (usb_dev, 0xC3, 0x0000, 0x0000, + c3_data, sizeof (c3_data), + 500, error)) + return FALSE; - /* 1. USB 인터페이스 claim */ - if (!g_usb_device_claim_interface (usb_dev, iface, 0, &error)) + for (i = 0; i < G_N_ELEMENTS (init_cmds); i++) { - fpi_image_device_open_complete (dev, error); - return; + if (!usb_bulk_out (usb_dev, SAMSUNG730B_EP_OUT, + init_cmds[i].data, init_cmds[i].len, + 500, error)) + return FALSE; } - /* 2. control 0xC3 전송 (libusb_control_transfer 대체) */ - ok = g_usb_device_control_transfer (usb_dev, - G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE, - G_USB_DEVICE_REQUEST_TYPE_VENDOR, - G_USB_DEVICE_RECIPIENT_DEVICE, - 0xC3, /* request */ - 0x0000, /* value */ - 0x0000, /* idx */ - c3_data, /* data */ - sizeof (c3_data), /* length */ - &actual_length, /* actual_length */ - 500, /* timeout ms */ - NULL, /* cancellable */ - &error); /* error */ - if (!ok || actual_length != sizeof (c3_data)) + return TRUE; +} + +/* ---------- finger detect용 probe 캡처 ---------- */ + +static guint8 * +s730b_detect_probe (GUsbDevice *usb_dev, gsize *out_len, GError **error) +{ + guint8 *buf = NULL; + gsize capacity = DETECT_PROBE_CHUNKS * BULK_PACKET_SIZE + 64; + gsize total = 0; + guint8 start_cmd[BULK_PACKET_SIZE] = { 0 }; + guint i; + + buf = g_malloc0 (capacity); + if (!buf) { - if (!error) - error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, - "samsung730b: control 0xC3 transfer failed " - "(actual_length=%"G_GSIZE_FORMAT")", - actual_length); - fpi_image_device_open_complete (dev, error); - return; + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NO_SPACE, + "s730b: detect probe alloc fail"); + return NULL; } - /* 3. init 명령들 bulk OUT 전송 (libusb_bulk_transfer 대체) */ - for (i = 0; i < samsung730b_init_cmds_len; i++) - { - const struct Samsung730bCmd *cmd = &samsung730b_init_cmds[i]; + g_message ("s730b: detect_probe: start"); + /* chunk 0: control 0xCA + start_cmd */ + if (!usb_ctrl_out (usb_dev, CAPTURE_CTRL_REQ, CAPTURE_CTRL_VAL, + CAPTURE_START_IDX, NULL, 0, + DETECT_INIT_TIMEOUT_MS, error)) + { + g_message ("s730b: detect_probe: ctrl_out failed"); + goto fail; + } - ok = g_usb_device_bulk_transfer (usb_dev, - SAMSUNG730B_BULK_EP_OUT, - (guint8 *) cmd->data, - cmd->len, - &actual_length, - 500, - NULL, /* cancellable */ - &error); /* error */ - if (!ok || actual_length != cmd->len) + start_cmd[0] = 0xa8; + start_cmd[1] = 0x06; + + if (!usb_bulk_out (usb_dev, SAMSUNG730B_EP_OUT, + start_cmd, sizeof (start_cmd), + DETECT_INIT_TIMEOUT_MS, error)) + { + g_message ("s730b: detect_probe: bulk_out(start_cmd) failed"); + goto fail; + } + + /* 초기 상태 응답 */ + { + guint8 tmp[BULK_PACKET_SIZE]; + gsize got = 0; + + if (!usb_bulk_in (usb_dev, SAMSUNG730B_EP_IN, + tmp, sizeof (tmp), + DETECT_INIT_TIMEOUT_MS, &got, error)) + { + g_message ("s730b: detect_probe: first bulk_in failed"); + goto fail; + } + g_message ("s730b: detect_probe: first bulk_in got=%"G_GSIZE_FORMAT, got); + + if (got > 0) + { + memcpy (buf + total, tmp, got); + total += got; + } + } + + /* chunk 1..N: 일부만 읽기 */ + for (i = 1; i < DETECT_PROBE_CHUNKS; i++) + { + guint8 tmp[BULK_PACKET_SIZE]; + guint8 ack[BULK_PACKET_SIZE] = { 0 }; + gsize got = 0; + guint16 widx = CAPTURE_START_IDX + i * BULK_PACKET_SIZE; + + if (!usb_ctrl_out (usb_dev, CAPTURE_CTRL_REQ, CAPTURE_CTRL_VAL, + widx, NULL, 0, + DETECT_INIT_TIMEOUT_MS, error)) + { + g_message ("s730b: detect_probe: ctrl_out chunk %u failed", i); + goto fail; + } + + if (!usb_bulk_in (usb_dev, SAMSUNG730B_EP_IN, + tmp, sizeof (tmp), + DETECT_BULK_TIMEOUT_MS, &got, error)) + { + g_message ("s730b: detect_probe: bulk_in chunk %u failed", i); + goto fail; + } + g_message ("s730b: detect_probe: bulk_in chunk %u got=%"G_GSIZE_FORMAT, i, got); + + if (got > 0) { - if (!error) - error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, - "samsung730b: init bulk transfer failed " - "idx=%"G_GSIZE_FORMAT" actual_length=%"G_GSIZE_FORMAT"/%"G_GSIZE_FORMAT, - i, actual_length, cmd->len); - fpi_image_device_open_complete (dev, error); - return; + memcpy (buf + total, tmp, got); + total += got; + } + + if (!usb_bulk_out (usb_dev, SAMSUNG730B_EP_OUT, + ack, sizeof (ack), + DETECT_INIT_TIMEOUT_MS, error)) + { + g_message ("s730b: detect_probe: ack out chunk %u failed", i); + goto fail; + } + } + g_message ("s730b: detect_probe: done total=%"G_GSIZE_FORMAT, total); + + *out_len = total; + return buf; + +fail: + g_message ("s730b: detect_probe: fail"); + g_free (buf); + *out_len = 0; + return NULL; +} + +/* ---------- ff 비율 기반 finger 존재 여부 ---------- */ + +static gboolean +s730b_has_finger (const guint8 *data, gsize len) +{ + if (!data || len < 512) + return FALSE; + + gsize total = MIN (len, (gsize) 4096); + gsize zeros = 0, ff = 0; + + for (gsize i = 0; i < total; i++) + { + guint8 v = data[i]; + if (v == 0x00) + zeros++; + else if (v == 0xFF) + ff++; + } + + double ff_ratio = (double) ff / (double) total; + + /* 파이썬 `_has_finger_in_detect`와 동일한 조건: + * - ff_ratio > 0.3 + * - zeros가 전체의 95% 미만 */ + if (ff_ratio > 0.30 && zeros < total * 0.95) + return TRUE; + + return FALSE; +} + +/* ---------- finger detect 루프 ---------- */ + +static gboolean +s730b_wait_finger (GUsbDevice *usb_dev) +{ + GError *local_error = NULL; + + for (guint r = 0; r < DETECT_MAX_ROUNDS; r++) + { + g_message ("s730b: detect round %u/%u", r + 1, DETECT_MAX_ROUNDS); + + for (guint i = 0; i < DETECT_PROBES_PER_ROUND; i++) + { + gsize probe_len = 0; + guint8 *probe; + + local_error = NULL; + probe = s730b_detect_probe (usb_dev, &probe_len, &local_error); + + if (!probe) + { + /* USB 에러/타임아웃은 soft-fail로 처리 */ + if (local_error) + { + g_message ("s730b: detect probe error (round=%u, probe=%u): %s", + r, i, local_error->message); + g_clear_error (&local_error); + } + /* 이 probe는 실패, 다음 probe로 넘어감 */ + g_usleep ((gulong) DETECT_INTERVAL_MS * 1000); + continue; + } + + gboolean finger = s730b_has_finger (probe, probe_len); + g_free (probe); + + g_message ("s730b: probe %u: finger=%d", i, finger); + if (finger) + return TRUE; + + g_usleep ((gulong) DETECT_INTERVAL_MS * 1000); + } + + /* 라운드 실패 시 센서 재초기화도 soft하게 처리 */ + g_message ("s730b: re-init after failed detect round"); + local_error = NULL; + if (!s730b_run_init (usb_dev, &local_error)) + { + if (local_error) + { + g_message ("s730b: re-init failed after detect round: %s", + local_error->message); + g_clear_error (&local_error); + } + /* init 실패해도 여기선 그냥 detect 실패로 간주 */ + break; } } - /* 4. 성공 */ + g_message ("s730b: finger detect timeout (no finger)"); + return FALSE; +} + +/* ---------- full frame 캡처 ---------- */ + +static gboolean +s730b_capture_frame (GUsbDevice *usb_dev, + guint8 **out_buf, + gsize *out_len, + GError **error) +{ + guint8 *buf = NULL; + gsize capacity = (gsize) CAPTURE_NUM_CHUNKS * BULK_PACKET_SIZE + 1024; + gsize total = 0; + + buf = g_malloc (capacity); + if (!buf) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NO_SPACE, + "s730b: capture alloc fail"); + return FALSE; + } + + /* chunk 0: control 0xCA + start_cmd */ + { + guint8 start_cmd[BULK_PACKET_SIZE] = { 0 }; + start_cmd[0] = 0xa8; + start_cmd[1] = 0x06; + + if (!usb_ctrl_out (usb_dev, CAPTURE_CTRL_REQ, CAPTURE_CTRL_VAL, + capture_indices[0], NULL, 0, + 500, error)) + goto fail; + + if (!usb_bulk_out (usb_dev, SAMSUNG730B_EP_OUT, + start_cmd, sizeof (start_cmd), + 500, error)) + goto fail; + + guint8 tmp[BULK_PACKET_SIZE]; + gsize got = 0; + + if (!usb_bulk_in (usb_dev, SAMSUNG730B_EP_IN, + tmp, sizeof (tmp), + 500, &got, error)) + goto fail; + + /* 파이썬처럼 chunk0 데이터는 버림 (상태 응답) */ + } + + /* chunk 1..N : 실제 이미지 데이터 + ACK */ + for (gsize i = 1; i < CAPTURE_NUM_CHUNKS; i++) + { + guint8 tmp[BULK_PACKET_SIZE]; + guint8 ack[BULK_PACKET_SIZE] = { 0 }; + gsize got = 0; + guint16 widx = capture_indices[i]; + + if (!usb_ctrl_out (usb_dev, CAPTURE_CTRL_REQ, CAPTURE_CTRL_VAL, + widx, NULL, 0, + 500, error)) + goto fail; + + if (!usb_bulk_in (usb_dev, SAMSUNG730B_EP_IN, + tmp, sizeof (tmp), + 1000, &got, error)) + goto fail; + + if (got == 0) + break; + + if (total + got > capacity) + { + capacity *= 2; + guint8 *tmp_buf = g_realloc (buf, capacity); + if (!tmp_buf) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NO_SPACE, + "s730b: capture realloc fail"); + goto fail; + } + buf = tmp_buf; + } + + memcpy (buf + total, tmp, got); + total += got; + + if (!usb_bulk_out (usb_dev, SAMSUNG730B_EP_OUT, + ack, sizeof (ack), + 500, error)) + goto fail; + } + + *out_buf = buf; + *out_len = total; + return TRUE; + +fail: + g_free (buf); + *out_buf = NULL; + *out_len = 0; + return FALSE; +} + +/* ---------- 이미지로 변환 + report ---------- */ + +static void +s730b_emit_image (FpImageDevice *dev, guint8 *raw, gsize raw_len) +{ + gsize needed = IMG_OFFSET + (gsize) IMG_W * IMG_H; + + if (raw_len < needed) + { + g_warning ("s730b: captured too short (%"G_GSIZE_FORMAT")", raw_len); + g_free (raw); + + fpi_image_device_report_finger_status (dev, FALSE); + fpi_image_device_image_captured (dev, NULL); + return; + } + + FpImage *img = fp_image_new (IMG_W, IMG_H); + gsize img_size = 0; + guchar *dst = (guchar *) fp_image_get_data (img, &img_size); + + if (img_size < (gsize) IMG_W * IMG_H) + { + g_warning ("s730b: image buffer too small (%"G_GSIZE_FORMAT")", img_size); + g_free (raw); + g_object_unref (img); + + fpi_image_device_report_finger_status (dev, FALSE); + fpi_image_device_image_captured (dev, NULL); + return; + } + + memcpy (dst, raw + IMG_OFFSET, IMG_W * IMG_H); + g_free (raw); + + fpi_image_device_report_finger_status (dev, TRUE); + fpi_image_device_image_captured (dev, img); + g_object_unref (img); +} + +/* ---------- libfprint 콜백들 ---------- */ + +static void +samsung730b_dev_init (FpImageDevice *dev) +{ + GUsbDevice *usb = fpi_device_get_usb_device (FP_DEVICE (dev)); + GError *error = NULL; + + if (!g_usb_device_claim_interface (usb, 0, 0, &error)) + { + fpi_image_device_open_complete (dev, error); + return; + } + + if (!s730b_run_init (usb, &error)) + { + fpi_image_device_open_complete (dev, error); + return; + } + fpi_image_device_open_complete (dev, NULL); } static void samsung730b_dev_deinit (FpImageDevice *dev) { - GUsbDevice *usb_dev; + GUsbDevice *usb = fpi_device_get_usb_device (FP_DEVICE (dev)); GError *error = NULL; - guint8 iface = 0; - usb_dev = fpi_device_get_usb_device (FP_DEVICE (dev)); - - if (!g_usb_device_release_interface (usb_dev, iface, 0, &error)) - { - fpi_image_device_close_complete (dev, error); - return; - } - - fpi_image_device_close_complete (dev, NULL); + g_usb_device_release_interface (usb, 0, 0, &error); + fpi_image_device_close_complete (dev, error); } static void samsung730b_dev_activate (FpImageDevice *dev) { - /* 장치 준비 완료 (CAPTURE state 진입) */ + GUsbDevice *usb = fpi_device_get_usb_device (FP_DEVICE (dev)); + GError *error = NULL; + guint8 *raw = NULL; + gsize raw_len = 0; + + /* 1) finger detect (이미지 기반) */ + if (!s730b_wait_finger (usb)) + { + /* 손가락 없음: 에러로 치지 않고 조용히 끝냄 */ + fpi_image_device_report_finger_status (dev, FALSE); + fpi_image_device_activate_complete (dev, NULL); + return; + } + + /* 2) detect 동안 센서 상태가 바뀌었을 수 있으니 init 재실행 */ + if (!s730b_run_init (usb, &error)) + { + fpi_image_device_activate_complete (dev, error); + return; + } + + /* 3) full capture */ + if (!s730b_capture_frame (usb, &raw, &raw_len, &error)) + { + fpi_image_device_activate_complete (dev, error); + return; + } + + /* activate 완료 알리고 이미지 전달 */ fpi_image_device_activate_complete (dev, NULL); - - /* 한 장 캡처해서 image_captured로 보고 */ - samsung730b_do_capture_and_report (dev); - - /* 단발 캡처 기기라서, 추가로 루프 안 돌고 바로 반환함 */ + s730b_emit_image (dev, raw, raw_len); } static void @@ -528,19 +631,15 @@ samsung730b_dev_deactivate (FpImageDevice *dev) fpi_image_device_deactivate_complete (dev, NULL); } -static const FpIdEntry samsung730b_id_table[] = { - { - .pid = SAMSUNG730B_PID, - .vid = SAMSUNG730B_VID, - .driver_data = 0, - }, - { - .pid = 0, - .vid = 0, - .driver_data = 0, - }, +/* ---------- ID 테이블 ---------- */ + +static const FpIdEntry samsung730b_ids[] = { + { .vid = SAMSUNG730B_VID, .pid = SAMSUNG730B_PID, .driver_data = 0 }, + { .vid = 0, .pid = 0, .driver_data = 0 }, }; +/* ---------- GObject boilerplate ---------- */ + static void fpi_device_samsung730b_init (FpiDeviceSamsung730b *self) { @@ -554,8 +653,8 @@ fpi_device_samsung730b_class_init (FpiDeviceSamsung730bClass *klass) dev_class->id = "samsung730b"; dev_class->full_name = "Samsung 730B (experimental)"; + dev_class->id_table = samsung730b_ids; dev_class->type = FP_DEVICE_TYPE_USB; - dev_class->id_table = samsung730b_id_table; dev_class->scan_type = FP_SCAN_TYPE_PRESS; img_class->img_open = samsung730b_dev_init; @@ -563,7 +662,7 @@ fpi_device_samsung730b_class_init (FpiDeviceSamsung730bClass *klass) img_class->activate = samsung730b_dev_activate; img_class->deactivate = samsung730b_dev_deactivate; + img_class->img_width = IMG_W; + img_class->img_height = IMG_H; img_class->bz3_threshold = 20; - img_class->img_width = -1; - img_class->img_height = -1; } \ No newline at end of file