GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 75.8% 680 / 0 / 897
Functions: 83.3% 40 / 0 / 48
Branches: 56.5% 216 / 0 / 382

libfprint/drivers/secugen.c
Line Branch Exec Source
1 /*
2 * SecuGen Hamster Pro 20 (FDU05) driver for libfprint
3 * Copyright (C) 2026
4 *
5 * Protocol reverse-engineered from USB packet captures.
6 *
7 * SIDO020A sensor configured via I2C-over-USB control transfers.
8 * Raw sensor output: 956x688 grayscale via USB bulk (657KB).
9 * Downsampled to 300x400 at 500 DPI for libfprint.
10 *
11 * This library is free software; you can redistribute it and/or
12 * modify it under the terms of the GNU Lesser General Public
13 * License as published by the Free Software Foundation; either
14 * version 2.1 of the License, or (at your option) any later version.
15 *
16 * This library is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 * Lesser General Public License for more details.
20 *
21 * You should have received a copy of the GNU Lesser General Public
22 * License along with this library; if not, write to the Free Software
23 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
24 */
25
26 #define FP_COMPONENT "secugen"
27
28 #include <string.h>
29
30 #include "drivers_api.h"
31
32 /* ---- Device constants ---- */
33
34 /* Final output image (what libfprint sees) */
35 #define SECUGEN_IMG_WIDTH 300
36 #define SECUGEN_IMG_HEIGHT 400
37 #define SECUGEN_IMG_SIZE (SECUGEN_IMG_WIDTH * SECUGEN_IMG_HEIGHT) /* 120000 */
38
39 /* Raw sensor array (full SIDO020A readout) */
40 #define SECUGEN_RAW_WIDTH 956
41 #define SECUGEN_RAW_HEIGHT 688
42 #define SECUGEN_RAW_SIZE (SECUGEN_RAW_WIDTH * SECUGEN_RAW_HEIGHT) /* 657728 */
43 #define SECUGEN_BULK_BUF_SIZE 657920 /* Actual USB stream: 688*956 + 192 trailing */
44 /* Per-URB read size for the frame stream. Kept below the usbmon capture cap
45 * (ring_size/5 ~= 245KB) and a multiple of the 512-byte bulk max-packet size,
46 * so the full frame is recorded by the standard pcap-based test capture as a
47 * sequence of complete URBs rather than one truncated transfer. 128 * 512. */
48 #define SECUGEN_BULK_CHUNK 65536
49 #define SECUGEN_DPI 500
50 #define SECUGEN_PPMM 19.685 /* 500 DPI / 25.4 mm/in */
51 #define SECUGEN_EP_DATA 0x82 /* Bulk IN endpoint */
52
53 /* ---- Vendor control transfer bRequest codes ---- */
54
55 #define SECUGEN_REQ_START_STREAM 1
56 #define SECUGEN_REQ_STOP_STREAM 2
57 #define SECUGEN_REQ_START_CAPTURE 5
58 #define SECUGEN_REQ_READ_FW_DATA 8
59 #define SECUGEN_REQ_LED_CONTROL 17
60 #define SECUGEN_REQ_GET_STATUS 22
61 #define SECUGEN_REQ_I2C_REG 34
62 #define SECUGEN_REQ_GET_DEVICE_ID 37
63 #define SECUGEN_REQ_SET_EXPOSURE 64
64
65 /* ---- I2C / SIDO020A sensor ---- */
66
67 #define SECUGEN_I2C_ADDR 0x0037 /* wValue for I2C transfers */
68
69 /* ---- Exposure values ---- */
70
71 #define SECUGEN_EXPOSURE_INIT 1000 /* Initial calibration exposure */
72 #define SECUGEN_EXPOSURE_NORMAL 1116 /* Normal capture exposure */
73
74 /* ---- Calibration / FW data ---- */
75
76 #define SECUGEN_FW_DATA_START 0x2000
77 #define SECUGEN_FW_DATA_CHUNK 4096
78 #define SECUGEN_FW_DATA_CHUNKS 6
79 #define SECUGEN_FW_DATA_LAST_LEN 2462 /* Last chunk is partial */
80 #define SECUGEN_FW_DATA_SIZE 22942 /* Total FW data: 4*4096 + 2*3072 ... */
81
82 /* Flat-field reference image stored in FW data at struct offset 0x1df8 */
83 #define SECUGEN_REF_WIDTH 150
84 #define SECUGEN_REF_HEIGHT 100
85 #define SECUGEN_REF_SIZE (SECUGEN_REF_WIDTH * SECUGEN_REF_HEIGHT) /* 15000 */
86 #define SECUGEN_REF_OFFSET 0x1df8 /* Offset of ref image in FW data */
87 #define SECUGEN_BLEND_CAL_VAL 240 /* Target uniform brightness */
88
89 /* FW data offsets for image processing parameters */
90 #define SECUGEN_BLC_OFFSETS_FW 0x0e /* 16 × int16 BLC region offsets */
91 #define SECUGEN_CAL_VALUE_FW 0x5892 /* uint16 blend target (usually 240) */
92 #define SECUGEN_SHARPEN_THRESH_FW 0x599C /* uint8 sharpening threshold */
93 #define SECUGEN_SHARPEN_AMOUNT_FW 0x599D /* uint8 sharpening amount */
94
95 /* ---- Timeouts (ms) ---- */
96
97 #define SECUGEN_CTRL_TIMEOUT 2000
98 #define SECUGEN_BULK_TIMEOUT 10000 /* Longer timeout for 657KB bulk read */
99 #define SECUGEN_FW_READ_TIMEOUT 5000
100 #define SECUGEN_FINGER_POLL_MS 200 /* Finger detection polling interval */
101 #define SECUGEN_FINGER_THRESHOLD 25 /* Mean brightness above this = finger present */
102
103 /* ---- I2C register init table entry ---- */
104
105 struct secugen_reg_entry
106 {
107 guint8 reg;
108 guint8 val;
109 };
110
111 /* ---- Driver instance ---- */
112
113 struct _FpiDeviceSecugen
114 {
115 FpImageDevice parent;
116
117 /* Initialization tracking */
118 int init_reg_idx; /* Current I2C register being written */
119 int fw_read_idx; /* Current calibration data chunk */
120 guint8 iface_num; /* Claimed USB interface number */
121
122 /* Finger detection */
123 GSource *finger_poll_source; /* Detection/finger-off timeout source */
124
125 /* Calibration / flat-field correction */
126 guint8 *cal_raw; /* Background frame at raw sensor res (956x688) */
127 guint8 *fw_data; /* Accumulated FW data from device (22942 bytes) */
128 gsize fw_data_len; /* Bytes accumulated so far */
129 guint8 *ref_image; /* Resized 300x400 flat-field reference image */
130 gboolean has_ref_image; /* Whether ref_image was successfully extracted */
131
132 /* BLC band compensation */
133 gint16 blc_offsets[16]; /* Factory-calibrated region offsets from FW */
134 gboolean has_blc_offsets; /* Whether BLC offsets were extracted */
135
136 /* Image processing params from FW */
137 guint16 cal_value; /* Blend target brightness (from FW, default 240) */
138 guint8 sharpen_threshold; /* Min gradient for sharpening */
139 guint8 sharpen_amount; /* Max gradient contribution */
140 guint8 sharpen_limit; /* Overall scaling factor (/10) */
141 gboolean sharpen_enabled; /* Whether post-resize sharpening is active */
142
143 /* Bulk read buffer for capture/detect frames */
144 guint8 *bulk_buffer; /* Reusable buffer for 657KB bulk reads */
145
146 /* Chunked frame-read state (see secugen_read_frame) */
147 guint8 *bulk_dest; /* Destination of the in-progress frame read */
148 gsize bulk_offset; /* Bytes accumulated into bulk_dest so far */
149
150 /* Activation / teardown state */
151 gboolean deactivating; /* Deactivate requested while an SSM runs */
152 int ssm_count; /* Number of init/detect/capture SSMs in flight.
153 * A counter (not a flag): the detect->capture
154 * handoff is synchronous, so a capture SSM is
155 * started before the detect SSM's completion
156 * handler runs - both must be tracked. */
157
158 /* Capture state */
159 guint16 exposure; /* Current exposure value */
160
161 /* Device info */
162 guint8 device_id[30];
163
164 /* Last register readback */
165 guint8 last_reg_rd;
166
167 /* Last status */
168 guint8 last_status[4];
169 };
170
171 G_DECLARE_FINAL_TYPE (FpiDeviceSecugen,
172 fpi_device_secugen,
173 FPI,
174 DEVICE_SECUGEN,
175 FpImageDevice);
176
177
4/6
fpi_device_secugen_class_intern_init:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 125 times.
fpi_device_secugen_get_type:
✓ Branch 2 → 3 taken 125 times.
✓ Branch 2 → 7 taken 24 times.
✓ Branch 4 → 5 taken 125 times.
✗ Branch 4 → 7 not taken.
399 G_DEFINE_TYPE (FpiDeviceSecugen,
178 fpi_device_secugen,
179 FP_TYPE_IMAGE_DEVICE);
180
181 /* ---- Blend curve LUT (256 bytes) ----
182 *
183 * Brightness-dependent correction weight for flat-field correction.
184 * Dark pixels (<16) get zero correction, bright pixels (>191) get full
185 * correction, middle range scales linearly. Identical to the table
186 * stored in the device's firmware data block.
187 */
188 static const guint8 SECUGEN_BLEND_CURVE[256] = {
189 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
190 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
191 16, 17, 19, 21, 23, 25, 27, 29, 31, 32, 34, 36, 38, 40, 42, 44,
192 46, 47, 49, 51, 53, 55, 57, 59, 61, 62, 64, 66, 68, 70, 72, 74,
193 76, 77, 79, 81, 83, 85, 87, 89, 91, 92, 94, 96, 98, 100, 102, 104,
194 106, 107, 109, 111, 113, 115, 117, 119, 121, 122, 124, 126, 128, 130, 132, 134,
195 136, 137, 138, 139, 141, 142, 143, 144, 146, 147, 148, 149, 151, 152, 153, 154,
196 156, 157, 158, 159, 161, 162, 163, 164, 166, 167, 168, 169, 171, 172, 173, 174,
197 176, 177, 178, 179, 181, 182, 183, 184, 186, 187, 188, 189, 191, 192, 193, 194,
198 196, 197, 198, 199, 201, 202, 203, 204, 206, 207, 208, 209, 211, 212, 213, 214,
199 216, 217, 218, 219, 221, 222, 223, 224, 226, 227, 228, 229, 231, 232, 233, 234,
200 236, 237, 238, 239, 240, 241, 243, 244, 245, 246, 247, 249, 250, 251, 252, 253,
201 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
202 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
203 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
204 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
205 };
206
207 static void secugen_resize_bilinear (const guint8 *src,
208 int src_w,
209 int src_h,
210 guint8 *dst,
211 int dst_w,
212 int dst_h);
213
214 /* ---- Bilinear resize 150x100 → 300x400 ----
215 *
216 * The FW stores a low-res (150x100) flat-field reference image captured
217 * during factory calibration. It is resized to 300x400 before being
218 * used for the blend correction.
219 */
220 static void
221 1 secugen_resize_ref_image (const guint8 *src, guint8 *dst)
222 {
223 1 secugen_resize_bilinear (src, SECUGEN_REF_WIDTH, SECUGEN_REF_HEIGHT,
224 dst, SECUGEN_IMG_WIDTH, SECUGEN_IMG_HEIGHT);
225 1 }
226
227 /* ---- SIDO020A I2C register init table ---- */
228
229 /*
230 * Phase 1: Main init registers (54 regs).
231 * Written first during init. 0xa5-0xa8 are NOT included here -- they are
232 * written later (after a 0x03 rewrite to enable the sensor).
233 * 0xb7-0xb9 come BEFORE 0xa5-0xa8, matching the captured init sequence.
234 */
235 static const struct secugen_reg_entry sido020a_init_regs[] = {
236 { 0x03, 0x02 }, /* Power/reset control - hold in reset */
237 { 0x04, 0x81 },
238 { 0x05, 0x0a },
239 { 0x08, 0x00 },
240 { 0x09, 0x11 },
241 { 0x0a, 0x11 },
242 { 0x10, 0x11 },
243 { 0x11, 0x23 },
244 { 0x12, 0x85 },
245 { 0x13, 0x00 },
246 { 0x14, 0x27 },
247 { 0x16, 0xb6 },
248 { 0x30, 0x01 },
249 { 0x31, 0xc0 },
250 { 0x32, 0x08 },
251 { 0x41, 0x00 },
252 { 0x42, 0x00 },
253 { 0x43, 0x06 },
254 { 0x44, 0x43 },
255 { 0x45, 0x00 },
256 { 0x46, 0x00 },
257 { 0x47, 0x04 },
258 { 0x48, 0xb3 },
259 { 0x49, 0x00 },
260 { 0x4a, 0x20 },
261 { 0x4b, 0x00 },
262 { 0x4c, 0x00 },
263 { 0x4d, 0x00 },
264 { 0x4e, 0x00 },
265 { 0x60, 0x0b },
266 { 0x61, 0x16 },
267 { 0x62, 0x32 },
268 { 0x63, 0x80 },
269 { 0x71, 0x08 },
270 { 0x80, 0xf8 },
271 { 0x81, 0x06 },
272 { 0x90, 0xaa },
273 { 0x91, 0x08 },
274 { 0x92, 0x10 },
275 { 0x93, 0x40 },
276 { 0x94, 0x04 },
277 { 0x95, 0x01 },
278 { 0x96, 0x02 },
279 { 0x97, 0x08 },
280 { 0x98, 0x10 },
281 { 0x99, 0x08 },
282 { 0x9a, 0x03 },
283 { 0x9b, 0xb0 },
284 { 0x9c, 0x08 },
285 { 0x9d, 0x24 },
286 { 0x9e, 0x30 },
287 { 0xb7, 0x15 },
288 { 0xb8, 0x28 },
289 { 0xb9, 0x04 },
290 };
291
292 #define N_INIT_REGS G_N_ELEMENTS (sido020a_init_regs)
293
294 /*
295 * Phase 2 late init: written AFTER 0x03 is rewritten to 0x05 (sensor enable),
296 * following the power-on transition.
297 */
298 static const struct secugen_reg_entry late_init_regs[] = {
299 { 0xa5, 0x00 },
300 { 0xa6, 0x00 },
301 { 0xa7, 0x00 },
302 { 0xa8, 0x00 },
303 };
304
305 #define N_LATE_INIT_REGS G_N_ELEMENTS (late_init_regs)
306
307 /* Capture window config (written after late init, before FW reads) */
308 static const struct secugen_reg_entry capture_window_regs[] = {
309 { 0x41, 0x00 }, { 0x42, 0xfa }, { 0x43, 0x04 }, { 0x44, 0x4f },
310 { 0x45, 0x00 }, { 0x46, 0x28 }, { 0x47, 0x03 }, { 0x48, 0x23 },
311 };
312
313 /* Frame window config (written after calibration capture) */
314 static const struct secugen_reg_entry frame_window_regs[] = {
315 { 0x41, 0x01 }, { 0x42, 0x48 }, { 0x43, 0x03 }, { 0x44, 0xbf },
316 { 0x45, 0x00 }, { 0x46, 0xcc }, { 0x47, 0x02 }, { 0x48, 0xb3 },
317 };
318
319 #define N_CAPTURE_WINDOW_REGS G_N_ELEMENTS (capture_window_regs)
320 #define N_FRAME_WINDOW_REGS G_N_ELEMENTS (frame_window_regs)
321
322 /* ================================================================
323 * USB Transfer Helpers
324 * ================================================================ */
325
326 /* Send a vendor control OUT transfer (no data) and advance SSM */
327 static void
328 11 secugen_ctrl_out (FpiSsm *ssm,
329 FpImageDevice *dev,
330 guint8 request,
331 guint16 value,
332 guint16 idx)
333 {
334 11 g_autoptr(FpiUsbTransfer) transfer = fpi_usb_transfer_new (FP_DEVICE (dev));
335
336 11 fpi_usb_transfer_fill_control (transfer,
337 G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
338 G_USB_DEVICE_REQUEST_TYPE_VENDOR,
339 G_USB_DEVICE_RECIPIENT_DEVICE,
340 request, value, idx, 0);
341 11 transfer->ssm = ssm;
342 11 fpi_usb_transfer_submit (g_steal_pointer (&transfer), SECUGEN_CTRL_TIMEOUT,
343 fpi_device_get_cancellable (FP_DEVICE (dev)),
344 fpi_ssm_usb_transfer_cb, NULL);
345 11 }
346
347 /* Send a vendor control OUT transfer with data payload and advance SSM */
348 static void
349 88 secugen_ctrl_out_data (FpiSsm *ssm,
350 FpImageDevice *dev,
351 guint8 request,
352 guint16 value,
353 guint16 idx,
354 const guint8 *data,
355 gsize len)
356 {
357 88 g_autoptr(FpiUsbTransfer) transfer = fpi_usb_transfer_new (FP_DEVICE (dev));
358
359 88 fpi_usb_transfer_fill_control (transfer,
360 G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
361 G_USB_DEVICE_REQUEST_TYPE_VENDOR,
362 G_USB_DEVICE_RECIPIENT_DEVICE,
363 request, value, idx, len);
364 88 memcpy (transfer->buffer, data, len);
365 88 transfer->ssm = ssm;
366 88 fpi_usb_transfer_submit (g_steal_pointer (&transfer), SECUGEN_CTRL_TIMEOUT,
367 fpi_device_get_cancellable (FP_DEVICE (dev)),
368 fpi_ssm_usb_transfer_cb, NULL);
369 88 }
370
371 /* Read via vendor control IN transfer, callback receives data */
372 static void
373 76 secugen_ctrl_in (FpiSsm *ssm,
374 FpImageDevice *dev,
375 guint8 request,
376 guint16 value,
377 guint16 idx,
378 gsize len,
379 FpiUsbTransferCallback callback)
380 {
381 76 g_autoptr(FpiUsbTransfer) transfer = fpi_usb_transfer_new (FP_DEVICE (dev));
382
383 76 fpi_usb_transfer_fill_control (transfer,
384 G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST,
385 G_USB_DEVICE_REQUEST_TYPE_VENDOR,
386 G_USB_DEVICE_RECIPIENT_DEVICE,
387 request, value, idx, len);
388 76 transfer->ssm = ssm;
389 76 fpi_usb_transfer_submit (g_steal_pointer (&transfer), SECUGEN_CTRL_TIMEOUT,
390 fpi_device_get_cancellable (FP_DEVICE (dev)),
391 callback, NULL);
392 76 }
393
394 /* Write a single I2C register on the SIDO020A sensor */
395 static void
396 84 secugen_i2c_write (FpiSsm *ssm,
397 FpImageDevice *dev,
398 guint8 reg,
399 guint8 val)
400 {
401 84 secugen_ctrl_out_data (ssm, dev, SECUGEN_REQ_I2C_REG,
402 SECUGEN_I2C_ADDR, reg, &val, 1);
403 }
404
405 /* Read back a single I2C register (1 byte) */
406 static void
407 74 secugen_i2c_read (FpiSsm *ssm,
408 FpImageDevice *dev,
409 guint8 reg,
410 FpiUsbTransferCallback callback)
411 {
412 74 secugen_ctrl_in (ssm, dev, SECUGEN_REQ_I2C_REG,
413 SECUGEN_I2C_ADDR, reg, 1, callback);
414 }
415
416 /* Set exposure value (big-endian 16-bit + 2 zero bytes) */
417 static void
418 4 secugen_set_exposure (FpiSsm *ssm,
419 FpImageDevice *dev,
420 guint16 exposure)
421 {
422 4 guint8 data[4] = {
423 (exposure >> 8) & 0xff,
424 exposure & 0xff,
425 0x00,
426 0x00
427 };
428
429 4 secugen_ctrl_out_data (ssm, dev, SECUGEN_REQ_SET_EXPOSURE,
430 0x0000, 0, data, 4);
431 4 }
432
433 /* ================================================================
434 * Init State Machine
435 *
436 * Matches the init sequence observed in USB captures:
437 * 1. GET_DEVICE_ID
438 * 2. 54 main I2C regs (with readback)
439 * 3. Rewrite 0x03 = 0x05 (enable sensor)
440 * 4. 4 late I2C regs: 0xa5-0xa8 (with readback)
441 * 5. 8 capture window regs: 0x41-0x48 (with readback)
442 * 6. START_CAPTURE (activates sensor subsystem)
443 * 7. 6 FW/calibration data reads
444 * 8. 0x32=0x00, SET_EXPOSURE(1000), START_CAPTURE, START_STREAM,
445 * bulk read 120KB calibration image, STOP_STREAM
446 * 9. 8 frame window regs: 0x41-0x48 (with readback)
447 * 10. Post-calibration tuning: 0x32/0x30/0x31 writes + SET_EXPOSURE(1116)
448 * ================================================================ */
449
450 enum init_states {
451 INIT_GET_DEVICE_ID = 0,
452 /* Phase 1: Main I2C register init (54 regs) */
453 INIT_I2C_WRITE,
454 INIT_I2C_READBACK,
455 /* Phase 2: Power enable + late regs */
456 INIT_POWER_ENABLE,
457 INIT_LATE_I2C_WRITE,
458 INIT_LATE_I2C_READBACK,
459 /* Phase 3: Capture window config */
460 INIT_WINDOW_I2C_WRITE,
461 INIT_WINDOW_I2C_READBACK,
462 /* Phase 4: START_CAPTURE + FW data reads */
463 INIT_PRE_FW_CAPTURE,
464 INIT_FW_READ,
465 /* Phase 5: Calibration capture */
466 INIT_CAL_REG32,
467 INIT_CAL_EXPOSURE,
468 INIT_CAL_START_CAPTURE,
469 INIT_CAL_START_STREAM,
470 INIT_CAL_BULK_READ,
471 INIT_CAL_STOP_STREAM,
472 /* Phase 6: Frame window config */
473 INIT_FRAME_I2C_WRITE,
474 INIT_FRAME_I2C_READBACK,
475 /* Phase 7: Post-calibration tuning */
476 INIT_POST_REG32_CLEAR,
477 INIT_POST_EXPOSURE,
478 INIT_POST_REG32_CLEAR2,
479 INIT_POST_REG30,
480 INIT_POST_REG31,
481 INIT_POST_REG32_CLEAR3,
482 INIT_POST_REG32_SET,
483 INIT_POST_EXPOSURE2,
484 /* Phase 8: Final calibration capture (operational settings) */
485 INIT_FINAL_REG32,
486 INIT_FINAL_START_CAPTURE,
487 INIT_FINAL_START_STREAM,
488 INIT_FINAL_BULK_READ,
489 INIT_FINAL_STOP_STREAM,
490 INIT_NUM_STATES,
491 };
492
493 static void
494 1 init_device_id_cb (FpiUsbTransfer *transfer,
495 FpDevice *dev,
496 gpointer user_data,
497 GError *error)
498 {
499 1 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
500
501
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 1 time.
1 if (error)
502 {
503 fpi_ssm_mark_failed (transfer->ssm, error);
504 return;
505 }
506
507 1 memcpy (self->device_id, transfer->buffer, MIN (transfer->actual_length, 30));
508 1 fp_dbg ("Device ID: %02x %02x %02x %02x ...",
509 self->device_id[0], self->device_id[1],
510 self->device_id[2], self->device_id[3]);
511 1 fpi_ssm_next_state (transfer->ssm);
512 }
513
514 /* Readback callback for main init regs (54 entries) */
515 static void
516 54 init_i2c_readback_cb (FpiUsbTransfer *transfer,
517 FpDevice *dev,
518 gpointer user_data,
519 GError *error)
520 {
521 54 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
522
523
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 54 times.
54 if (error)
524 {
525 fpi_ssm_mark_failed (transfer->ssm, error);
526 return;
527 }
528
529
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 54 times.
54 if (transfer->actual_length < 1)
530 {
531 fpi_ssm_mark_failed (transfer->ssm,
532 fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
533 return;
534 }
535
536 54 self->last_reg_rd = transfer->buffer[0];
537 54 self->init_reg_idx++;
538
539
2/2
✓ Branch 9 → 10 taken 53 times.
✓ Branch 9 → 11 taken 1 time.
54 if (self->init_reg_idx < (int) N_INIT_REGS)
540 {
541 53 fpi_ssm_jump_to_state (transfer->ssm, INIT_I2C_WRITE);
542 }
543 else
544 {
545 1 self->init_reg_idx = 0;
546 1 fpi_ssm_next_state (transfer->ssm);
547 }
548 }
549
550 /* Readback callback for late init regs (4 entries: 0xa5-0xa8) */
551 static void
552 4 init_late_readback_cb (FpiUsbTransfer *transfer,
553 FpDevice *dev,
554 gpointer user_data,
555 GError *error)
556 {
557 4 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
558
559
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 4 times.
4 if (error)
560 {
561 fpi_ssm_mark_failed (transfer->ssm, error);
562 return;
563 }
564
565
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 4 times.
4 if (transfer->actual_length < 1)
566 {
567 fpi_ssm_mark_failed (transfer->ssm,
568 fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
569 return;
570 }
571
572 4 self->last_reg_rd = transfer->buffer[0];
573 4 self->init_reg_idx++;
574
575
2/2
✓ Branch 9 → 10 taken 3 times.
✓ Branch 9 → 11 taken 1 time.
4 if (self->init_reg_idx < (int) N_LATE_INIT_REGS)
576 {
577 3 fpi_ssm_jump_to_state (transfer->ssm, INIT_LATE_I2C_WRITE);
578 }
579 else
580 {
581 1 self->init_reg_idx = 0;
582 1 fpi_ssm_next_state (transfer->ssm);
583 }
584 }
585
586 /* Readback callback for capture window regs (8 entries) */
587 static void
588 8 init_window_readback_cb (FpiUsbTransfer *transfer,
589 FpDevice *dev,
590 gpointer user_data,
591 GError *error)
592 {
593 8 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
594
595
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 8 times.
8 if (error)
596 {
597 fpi_ssm_mark_failed (transfer->ssm, error);
598 return;
599 }
600
601
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 8 times.
8 if (transfer->actual_length < 1)
602 {
603 fpi_ssm_mark_failed (transfer->ssm,
604 fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
605 return;
606 }
607
608 8 self->last_reg_rd = transfer->buffer[0];
609 8 self->init_reg_idx++;
610
611
2/2
✓ Branch 9 → 10 taken 7 times.
✓ Branch 9 → 11 taken 1 time.
8 if (self->init_reg_idx < (int) N_CAPTURE_WINDOW_REGS)
612 {
613 7 fpi_ssm_jump_to_state (transfer->ssm, INIT_WINDOW_I2C_WRITE);
614 }
615 else
616 {
617 1 self->init_reg_idx = 0;
618 1 fpi_ssm_next_state (transfer->ssm);
619 }
620 }
621
622 /* FW data read callback - accumulate data and extract reference image */
623 static void
624 6 init_fw_read_cb (FpiUsbTransfer *transfer,
625 FpDevice *dev,
626 gpointer user_data,
627 GError *error)
628 {
629 12 g_autoptr(GError) local_error = error;
630 6 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
631
632
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 6 taken 6 times.
6 if (local_error)
633 {
634 fp_warn ("FW data read failed (non-fatal): %s", local_error->message);
635 fpi_ssm_next_state (transfer->ssm);
636 return;
637 }
638
639 /* Accumulate FW data into contiguous buffer */
640 {
641 6 gsize to_copy = transfer->actual_length;
642
643
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 6 times.
6 if (self->fw_data_len + to_copy > SECUGEN_FW_DATA_SIZE)
644 to_copy = SECUGEN_FW_DATA_SIZE - self->fw_data_len;
645
2/4
✓ Branch 8 → 9 taken 6 times.
✗ Branch 8 → 11 not taken.
✓ Branch 9 → 10 taken 6 times.
✗ Branch 9 → 11 not taken.
6 if (to_copy > 0 && self->fw_data)
646 {
647 6 memcpy (self->fw_data + self->fw_data_len, transfer->buffer, to_copy);
648 6 self->fw_data_len += to_copy;
649 }
650 }
651
652 6 self->fw_read_idx++;
653
2/2
✓ Branch 11 → 12 taken 5 times.
✓ Branch 11 → 13 taken 1 time.
6 if (self->fw_read_idx < SECUGEN_FW_DATA_CHUNKS)
654 {
655 5 fpi_ssm_jump_to_state (transfer->ssm, INIT_FW_READ);
656 }
657 else
658 {
659 /* All FW data received - extract image processing parameters */
660
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 16 taken 1 time.
1 if (!self->fw_data)
661 {
662 fp_warn ("FW data buffer missing; skipping calibration extraction");
663 fpi_ssm_next_state (transfer->ssm);
664 return;
665 }
666
667 /* BLC region offsets: 16 × int16 at fw_data[0x0e] */
668
1/2
✓ Branch 16 → 18 taken 1 time.
✗ Branch 16 → 20 not taken.
1 if (self->fw_data_len >= SECUGEN_BLC_OFFSETS_FW + 32)
669 {
670 int i;
671
672
2/2
✓ Branch 18 → 17 taken 16 times.
✓ Branch 18 → 19 taken 1 time.
17 for (i = 0; i < 16; i++)
673 {
674 16 guint16 raw_val;
675
676 16 memcpy (&raw_val,
677 16 self->fw_data + SECUGEN_BLC_OFFSETS_FW + i * 2, 2);
678 16 self->blc_offsets[i] = (gint16) GUINT16_FROM_LE (raw_val);
679 }
680 1 self->has_blc_offsets = TRUE;
681 1 fp_dbg ("BLC offsets: %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d",
682 self->blc_offsets[0], self->blc_offsets[1],
683 self->blc_offsets[2], self->blc_offsets[3],
684 self->blc_offsets[4], self->blc_offsets[5],
685 self->blc_offsets[6], self->blc_offsets[7],
686 self->blc_offsets[8], self->blc_offsets[9],
687 self->blc_offsets[10], self->blc_offsets[11],
688 self->blc_offsets[12], self->blc_offsets[13],
689 self->blc_offsets[14], self->blc_offsets[15]);
690 }
691
692 /* Reference image: 150×100 at fw_data[0x1df8] */
693
1/2
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 26 not taken.
1 if (self->fw_data_len >= SECUGEN_REF_OFFSET + SECUGEN_REF_SIZE)
694 {
695 1 const guint8 *ref_src = self->fw_data + SECUGEN_REF_OFFSET;
696
697
1/2
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 24 not taken.
1 if (!self->ref_image)
698 1 self->ref_image = g_malloc (SECUGEN_IMG_SIZE);
699
700 1 secugen_resize_ref_image (ref_src, self->ref_image);
701 1 self->has_ref_image = TRUE;
702 1 fp_dbg ("Flat-field reference image extracted and resized to %dx%d",
703 SECUGEN_IMG_WIDTH, SECUGEN_IMG_HEIGHT);
704 }
705 else
706 {
707 fp_warn ("FW data too short for reference image: %zu < %d",
708 self->fw_data_len,
709 SECUGEN_REF_OFFSET + SECUGEN_REF_SIZE);
710 }
711
712 /* Blend cal_value: uint16 at fw_data[0x5892] */
713
1/2
✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 29 not taken.
1 if (self->fw_data_len >= SECUGEN_CAL_VALUE_FW + 2)
714 {
715 1 guint16 raw_val;
716
717 1 memcpy (&raw_val, self->fw_data + SECUGEN_CAL_VALUE_FW, 2);
718 1 self->cal_value = GUINT16_FROM_LE (raw_val);
719 1 fp_dbg ("Blend cal_value from FW: %u", self->cal_value);
720 }
721
722 /* Sharpen parameters */
723
1/2
✓ Branch 29 → 30 taken 1 time.
✗ Branch 29 → 34 not taken.
1 if (self->fw_data_len >= SECUGEN_SHARPEN_AMOUNT_FW + 1)
724 {
725 1 self->sharpen_threshold = self->fw_data[SECUGEN_SHARPEN_THRESH_FW];
726 1 self->sharpen_amount = self->fw_data[SECUGEN_SHARPEN_AMOUNT_FW];
727 1 self->sharpen_limit = 10; /* Default; exact FW offset is past our read */
728
2/4
✓ Branch 30 → 31 taken 1 time.
✗ Branch 30 → 32 not taken.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 1 time.
1 self->sharpen_enabled = (self->sharpen_threshold > 0 &&
729 self->sharpen_amount > 0);
730 1 fp_dbg ("Sharpen: threshold=%u amount=%u limit=%u enabled=%d",
731 self->sharpen_threshold, self->sharpen_amount,
732 self->sharpen_limit, self->sharpen_enabled);
733 }
734
735 /* The pipeline degrades gracefully when calibration data could not
736 * be extracted, but image quality suffers - say so once. */
737
2/4
✓ Branch 34 → 35 taken 1 time.
✗ Branch 34 → 36 not taken.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 41 taken 1 time.
1 if (!self->has_blc_offsets || !self->has_ref_image)
738 {
739 fp_warn ("Missing FW calibration data (BLC offsets: %s, reference "
740 "image: %s); captured images will have reduced quality",
741 self->has_blc_offsets ? "ok" : "missing",
742 self->has_ref_image ? "ok" : "missing");
743 }
744
745 1 fpi_ssm_next_state (transfer->ssm);
746 }
747 }
748
749 /* Readback callback for frame window regs (8 entries) */
750 static void
751 8 init_frame_readback_cb (FpiUsbTransfer *transfer,
752 FpDevice *dev,
753 gpointer user_data,
754 GError *error)
755 {
756 8 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
757
758
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 8 times.
8 if (error)
759 {
760 fpi_ssm_mark_failed (transfer->ssm, error);
761 return;
762 }
763
764
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 8 times.
8 if (transfer->actual_length < 1)
765 {
766 fpi_ssm_mark_failed (transfer->ssm,
767 fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
768 return;
769 }
770
771 8 self->last_reg_rd = transfer->buffer[0];
772 8 self->init_reg_idx++;
773
774
2/2
✓ Branch 9 → 10 taken 7 times.
✓ Branch 9 → 11 taken 1 time.
8 if (self->init_reg_idx < (int) N_FRAME_WINDOW_REGS)
775 {
776 7 fpi_ssm_jump_to_state (transfer->ssm, INIT_FRAME_I2C_WRITE);
777 }
778 else
779 {
780 1 self->init_reg_idx = 0;
781 1 fpi_ssm_next_state (transfer->ssm);
782 }
783 }
784
785 /*
786 * Chunked frame read.
787 *
788 * The SIDO020A streams a full ~657KB sensor frame over the bulk-IN endpoint.
789 * Reading it as a single USB transfer makes the frame impossible to record
790 * with the standard usbmon/pcap-based test capture: the kernel usbmon ring
791 * buffer caps per-URB payload at ring_size/5 (~245KB), so one 657KB URB is
792 * truncated on capture and the replay cannot reconstruct the frame.
793 *
794 * Instead the frame is read in SECUGEN_BULK_CHUNK-sized pieces (a multiple of
795 * the 512-byte bulk max-packet size, below the usbmon cap), accumulating into
796 * a caller-supplied destination buffer. Every URB is then small enough to be
797 * captured intact and the test capture works with the standard tooling. Once
798 * the whole frame is read the owning SSM advances to its next state.
799 */
800 static void
801 33 secugen_frame_chunk_cb (FpiUsbTransfer *transfer,
802 FpDevice *dev,
803 gpointer user_data,
804 GError *error)
805 {
806 66 g_autoptr(GError) local_error = error;
807 33 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
808 33 gsize remaining;
809 33 gsize copied;
810
811
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 6 taken 33 times.
33 if (local_error)
812 {
813 /* A deactivate may have cancelled the transfer; unwind quietly. */
814 if (self->deactivating)
815 fpi_ssm_mark_completed (transfer->ssm);
816 else
817 fpi_ssm_mark_failed (transfer->ssm, g_steal_pointer (&local_error));
818 return;
819 }
820
821 /* Abandon the read if a deactivate arrived mid-frame. */
822
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 9 taken 33 times.
33 if (self->deactivating)
823 {
824 fpi_ssm_mark_completed (transfer->ssm);
825 return;
826 }
827
828 /* Append this chunk to the frame buffer, guarding against overrun. */
829 33 copied = MIN ((gsize) transfer->actual_length,
830 SECUGEN_BULK_BUF_SIZE - self->bulk_offset);
831 33 memcpy (self->bulk_dest + self->bulk_offset, transfer->buffer, copied);
832 33 self->bulk_offset += copied;
833
834 33 remaining = SECUGEN_BULK_BUF_SIZE - self->bulk_offset;
835
836 /* A full-length chunk means the device may still have more frame data;
837 * a short read means the stream has ended. */
838
3/4
✓ Branch 9 → 10 taken 30 times.
✓ Branch 9 → 16 taken 3 times.
✓ Branch 10 → 11 taken 30 times.
✗ Branch 10 → 15 not taken.
33 if (remaining > 0 && transfer->actual_length == transfer->length)
839 {
840 60 g_autoptr(FpiUsbTransfer) next = fpi_usb_transfer_new (dev);
841
842 30 next->ssm = transfer->ssm;
843 30 fpi_usb_transfer_fill_bulk (next, SECUGEN_EP_DATA,
844 MIN ((gsize) SECUGEN_BULK_CHUNK, remaining));
845 30 fpi_usb_transfer_submit (g_steal_pointer (&next), SECUGEN_BULK_TIMEOUT,
846 fpi_device_get_cancellable (dev),
847 secugen_frame_chunk_cb, NULL);
848 30 return;
849 }
850
851 /* Zero any unread tail so a short frame never leaves stale data from a
852 * previous capture in the destination buffer. */
853 if (remaining > 0)
854 memset (self->bulk_dest + self->bulk_offset, 0, remaining);
855
856
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 21 taken 3 times.
3 if (self->bulk_offset < SECUGEN_RAW_SIZE)
857 {
858 fp_warn ("Short image data: got %" G_GSIZE_FORMAT ", expected %d",
859 self->bulk_offset, SECUGEN_RAW_SIZE);
860 fpi_ssm_mark_failed (transfer->ssm,
861 fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
862 return;
863 }
864
865 3 fp_dbg ("Read %" G_GSIZE_FORMAT " byte frame in chunks", self->bulk_offset);
866 3 fpi_ssm_next_state (transfer->ssm);
867 }
868
869 /* Begin a chunked read of a full sensor frame into dest. */
870 static void
871 3 secugen_read_frame (FpiSsm *ssm,
872 FpDevice *dev,
873 guint8 *dest)
874 {
875 6 g_autoptr(FpiUsbTransfer) transfer = NULL;
876 3 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
877
878 3 self->bulk_dest = dest;
879 3 self->bulk_offset = 0;
880
881 3 transfer = fpi_usb_transfer_new (dev);
882 3 transfer->ssm = ssm;
883 3 fpi_usb_transfer_fill_bulk (transfer, SECUGEN_EP_DATA,
884 MIN ((gsize) SECUGEN_BULK_CHUNK,
885 (gsize) SECUGEN_BULK_BUF_SIZE));
886 3 fpi_usb_transfer_submit (g_steal_pointer (&transfer), SECUGEN_BULK_TIMEOUT,
887 fpi_device_get_cancellable (dev),
888 secugen_frame_chunk_cb, NULL);
889 3 }
890
891 static void
892 176 init_run_state (FpiSsm *ssm, FpDevice *_dev)
893 {
894 176 FpImageDevice *dev = FP_IMAGE_DEVICE (_dev);
895 176 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (_dev);
896
897
31/32
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 54 times.
✓ Branch 3 → 8 taken 54 times.
✓ Branch 3 → 10 taken 1 time.
✓ Branch 3 → 12 taken 4 times.
✓ Branch 3 → 14 taken 4 times.
✓ Branch 3 → 16 taken 8 times.
✓ Branch 3 → 18 taken 8 times.
✓ Branch 3 → 20 taken 1 time.
✓ Branch 3 → 22 taken 6 times.
✓ Branch 3 → 29 taken 1 time.
✓ Branch 3 → 31 taken 1 time.
✓ Branch 3 → 33 taken 1 time.
✓ Branch 3 → 35 taken 1 time.
✓ Branch 3 → 37 taken 1 time.
✓ Branch 3 → 39 taken 1 time.
✓ Branch 3 → 41 taken 8 times.
✓ Branch 3 → 43 taken 8 times.
✓ Branch 3 → 45 taken 1 time.
✓ Branch 3 → 47 taken 1 time.
✓ Branch 3 → 49 taken 1 time.
✓ Branch 3 → 51 taken 1 time.
✓ Branch 3 → 53 taken 1 time.
✓ Branch 3 → 55 taken 1 time.
✓ Branch 3 → 57 taken 1 time.
✓ Branch 3 → 59 taken 1 time.
✓ Branch 3 → 61 taken 1 time.
✓ Branch 3 → 63 taken 1 time.
✓ Branch 3 → 65 taken 1 time.
✓ Branch 3 → 67 taken 1 time.
✓ Branch 3 → 69 taken 1 time.
✗ Branch 3 → 71 not taken.
176 switch (fpi_ssm_get_cur_state (ssm))
898 {
899 1 case INIT_GET_DEVICE_ID:
900 1 secugen_ctrl_in (ssm, dev, SECUGEN_REQ_GET_DEVICE_ID,
901 0x0000, 0, 30, init_device_id_cb);
902 1 break;
903
904 /* ---- Phase 1: Main I2C init (54 regs) ---- */
905
906 54 case INIT_I2C_WRITE:
907 {
908 54 const struct secugen_reg_entry *entry =
909 54 &sido020a_init_regs[self->init_reg_idx];
910 54 secugen_i2c_write (ssm, dev, entry->reg, entry->val);
911 }
912 54 break;
913
914 54 case INIT_I2C_READBACK:
915 {
916 54 const struct secugen_reg_entry *entry =
917 54 &sido020a_init_regs[self->init_reg_idx];
918 54 secugen_i2c_read (ssm, dev, entry->reg, init_i2c_readback_cb);
919 }
920 54 break;
921
922 /* ---- Phase 2: Power enable + late regs ---- */
923
924 1 case INIT_POWER_ENABLE:
925 /* Rewrite reg 0x03 from 0x02 (reset) to 0x05 (enable) */
926 1 secugen_i2c_write (ssm, dev, 0x03, 0x05);
927 1 break;
928
929 4 case INIT_LATE_I2C_WRITE:
930 {
931 4 const struct secugen_reg_entry *entry;
932
933 4 entry = &late_init_regs[self->init_reg_idx];
934 4 secugen_i2c_write (ssm, dev, entry->reg, entry->val);
935 }
936 4 break;
937
938 4 case INIT_LATE_I2C_READBACK:
939 {
940 4 const struct secugen_reg_entry *entry =
941 4 &late_init_regs[self->init_reg_idx];
942 4 secugen_i2c_read (ssm, dev, entry->reg, init_late_readback_cb);
943 }
944 4 break;
945
946 /* ---- Phase 3: Capture window config ---- */
947
948 8 case INIT_WINDOW_I2C_WRITE:
949 {
950 8 const struct secugen_reg_entry *entry;
951
952 8 entry = &capture_window_regs[self->init_reg_idx];
953 8 secugen_i2c_write (ssm, dev, entry->reg, entry->val);
954 }
955 8 break;
956
957 8 case INIT_WINDOW_I2C_READBACK:
958 {
959 8 const struct secugen_reg_entry *entry =
960 8 &capture_window_regs[self->init_reg_idx];
961 8 secugen_i2c_read (ssm, dev, entry->reg, init_window_readback_cb);
962 }
963 8 break;
964
965 /* ---- Phase 4: START_CAPTURE + FW data reads ---- */
966
967 1 case INIT_PRE_FW_CAPTURE:
968 1 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_CAPTURE, 0x0001, 0);
969 1 break;
970
971 6 case INIT_FW_READ:
972 {
973 6 guint16 offset = SECUGEN_FW_DATA_START +
974 6 (self->fw_read_idx * SECUGEN_FW_DATA_CHUNK);
975 5 gsize len = (self->fw_read_idx == SECUGEN_FW_DATA_CHUNKS - 1) ?
976
2/2
✓ Branch 22 → 23 taken 5 times.
✓ Branch 22 → 24 taken 1 time.
6 SECUGEN_FW_DATA_LAST_LEN :
977 SECUGEN_FW_DATA_CHUNK;
978
979 {
980 6 g_autoptr(FpiUsbTransfer) transfer = fpi_usb_transfer_new (FP_DEVICE (dev));
981
982 6 fpi_usb_transfer_fill_control (transfer,
983 G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST,
984 G_USB_DEVICE_REQUEST_TYPE_VENDOR,
985 G_USB_DEVICE_RECIPIENT_DEVICE,
986 SECUGEN_REQ_READ_FW_DATA,
987 0x0000, offset, len);
988 6 transfer->ssm = ssm;
989 6 fpi_usb_transfer_submit (g_steal_pointer (&transfer), SECUGEN_FW_READ_TIMEOUT,
990 fpi_device_get_cancellable (FP_DEVICE (dev)),
991 init_fw_read_cb, NULL);
992 }
993 }
994 6 break;
995
996 /* ---- Phase 5: Calibration capture ---- */
997
998 1 case INIT_CAL_REG32:
999 /* Must clear reg 0x32 before each image capture */
1000 1 secugen_i2c_write (ssm, dev, 0x32, 0x00);
1001 1 break;
1002
1003 1 case INIT_CAL_EXPOSURE:
1004 1 self->exposure = SECUGEN_EXPOSURE_INIT;
1005 1 secugen_set_exposure (ssm, dev, SECUGEN_EXPOSURE_INIT);
1006 1 break;
1007
1008 1 case INIT_CAL_START_CAPTURE:
1009 1 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_CAPTURE, 0x0001, 0);
1010 1 break;
1011
1012 1 case INIT_CAL_START_STREAM:
1013 1 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_STREAM, 0x0000, 0);
1014 1 break;
1015
1016 1 case INIT_CAL_BULK_READ:
1017 1 secugen_read_frame (ssm, _dev, self->cal_raw);
1018 1 break;
1019
1020 1 case INIT_CAL_STOP_STREAM:
1021 1 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_STOP_STREAM, 0x0000, 0);
1022 1 break;
1023
1024 /* ---- Phase 6: Frame window config ---- */
1025
1026 8 case INIT_FRAME_I2C_WRITE:
1027 {
1028 8 const struct secugen_reg_entry *entry;
1029
1030 8 entry = &frame_window_regs[self->init_reg_idx];
1031 8 secugen_i2c_write (ssm, dev, entry->reg, entry->val);
1032 }
1033 8 break;
1034
1035 8 case INIT_FRAME_I2C_READBACK:
1036 {
1037 8 const struct secugen_reg_entry *entry =
1038 8 &frame_window_regs[self->init_reg_idx];
1039 8 secugen_i2c_read (ssm, dev, entry->reg, init_frame_readback_cb);
1040 }
1041 8 break;
1042
1043 /* ---- Phase 7: Post-calibration tuning ---- */
1044
1045 1 case INIT_POST_REG32_CLEAR:
1046 1 secugen_i2c_write (ssm, dev, 0x32, 0x00);
1047 1 break;
1048
1049 1 case INIT_POST_EXPOSURE:
1050 1 self->exposure = SECUGEN_EXPOSURE_NORMAL;
1051 1 secugen_set_exposure (ssm, dev, SECUGEN_EXPOSURE_NORMAL);
1052 1 break;
1053
1054 1 case INIT_POST_REG32_CLEAR2:
1055 1 secugen_i2c_write (ssm, dev, 0x32, 0x00);
1056 1 break;
1057
1058 1 case INIT_POST_REG30:
1059 /* Frame control transitions from init value 0x01 to 0x04 */
1060 1 secugen_i2c_write (ssm, dev, 0x30, 0x04);
1061 1 break;
1062
1063 1 case INIT_POST_REG31:
1064 /* Frame size transitions from init value 0xc0 to 0x5c */
1065 1 secugen_i2c_write (ssm, dev, 0x31, 0x5c);
1066 1 break;
1067
1068 1 case INIT_POST_REG32_CLEAR3:
1069 1 secugen_i2c_write (ssm, dev, 0x32, 0x00);
1070 1 break;
1071
1072 1 case INIT_POST_REG32_SET:
1073 /* Final 0x32 value for operational mode */
1074 1 secugen_i2c_write (ssm, dev, 0x32, 0x24);
1075 1 break;
1076
1077 1 case INIT_POST_EXPOSURE2:
1078 1 secugen_set_exposure (ssm, dev, SECUGEN_EXPOSURE_NORMAL);
1079 1 break;
1080
1081 /* ---- Phase 8: Final calibration capture (operational settings) ---- */
1082
1083 1 case INIT_FINAL_REG32:
1084 /* Clear reg 0x32 before capture (same as every capture) */
1085 1 secugen_i2c_write (ssm, dev, 0x32, 0x00);
1086 1 break;
1087
1088 1 case INIT_FINAL_START_CAPTURE:
1089 1 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_CAPTURE, 0x0001, 0);
1090 1 break;
1091
1092 1 case INIT_FINAL_START_STREAM:
1093 1 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_STREAM, 0x0000, 0);
1094 1 break;
1095
1096 1 case INIT_FINAL_BULK_READ:
1097 1 secugen_read_frame (ssm, _dev, self->cal_raw);
1098 1 break;
1099
1100 1 case INIT_FINAL_STOP_STREAM:
1101 1 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_STOP_STREAM, 0x0000, 0);
1102 1 break;
1103 }
1104 176 }
1105
1106 /* ================================================================
1107 * Image Processing Pipeline
1108 *
1109 * 1. Band compensation at 956x688
1110 * 2. Edge-aware unsharp mask at 956x688
1111 * 3. Bilinear downsample to 300x400
1112 * 4. Flat-field (blend) correction at 300x400
1113 * 5. 6-directional sharpening at 300x400
1114 * 6. Invert - bitwise NOT (~pixel)
1115 *
1116 * The raw SIDO020A readout has strong fixed-pattern noise (per-region
1117 * brightness bands, optical vignetting) that NBIS minutiae detection
1118 * does not compensate for; stages 1-5 replicate the capture processing
1119 * the sensor needs to produce a usable ridge image. Stages 1, 4 and 5
1120 * are driven by per-device factory calibration data read from the FW
1121 * (see init_fw_read_cb) and degrade gracefully when it is unavailable.
1122 * ================================================================ */
1123
1124 /* Brightness-dependent offset scaling for band compensation.
1125 * Correction strength steps down at pixel values 200, 160, 100, 70, 26. */
1126 static inline int
1127 328864 secugen_blc_scale_offset (int pixel, int offset)
1128 {
1129
2/2
✓ Branch 2 → 3 taken 327926 times.
✓ Branch 2 → 11 taken 938 times.
328864 if (pixel > 200)
1130 return offset;
1131
2/2
✓ Branch 3 → 4 taken 3643 times.
✓ Branch 3 → 5 taken 324283 times.
327926 else if (pixel > 160)
1132 3643 return (offset * 3) >> 2;
1133
2/2
✓ Branch 5 → 6 taken 21776 times.
✓ Branch 5 → 7 taken 302507 times.
324283 else if (pixel > 100)
1134 21776 return offset >> 1;
1135
2/2
✓ Branch 7 → 8 taken 15731 times.
✓ Branch 7 → 9 taken 286776 times.
302507 else if (pixel > 70)
1136 15731 return offset >> 2;
1137
2/2
✓ Branch 9 → 10 taken 73752 times.
✓ Branch 9 → 11 taken 213024 times.
286776 else if (pixel >= 26)
1138 73752 return offset >> 3;
1139 else
1140 return 0;
1141 }
1142
1143 /* Per-region band compensation.
1144 * Region layout: 2 rows × 8 columns (16 regions).
1145 * Processes every other row with 3-phase dithering pattern. */
1146 static void
1147 1 secugen_blc_compensate (guint8 *image,
1148 int width,
1149 int height,
1150 const gint16 *offsets)
1151 {
1152 1 static const int phase_table[12] = {
1153 7, 11, 13, 14, 5, 3, 6, 9, 4, 8, 2, 1
1154 };
1155 1 int half_h = height / 2;
1156 1 int eighth_w = width / 8;
1157 1 int region;
1158
1159
2/2
✓ Branch 27 → 3 taken 16 times.
✓ Branch 27 → 28 taken 1 time.
17 for (region = 0; region < 16; region++)
1160 {
1161 16 int rc = region % 8;
1162 16 int rr = region / 8;
1163 16 int x0 = rc * eighth_w;
1164
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 14 times.
16 int x1 = (rc == 7) ? width - 1 : (rc + 1) * eighth_w - 1;
1165 16 int y0 = rr * half_h;
1166
2/2
✓ Branch 6 → 7 taken 8 times.
✓ Branch 6 → 8 taken 8 times.
16 int y1 = (rr == 1) ? height - 1 : half_h - 1;
1167 16 int offset = offsets[region];
1168 16 int phase_counter = -1;
1169 16 int row, col;
1170
1171
2/2
✓ Branch 25 → 10 taken 2752 times.
✓ Branch 25 → 26 taken 16 times.
2768 for (row = y0 + 1; row <= y1; row += 2)
1172 {
1173 2752 phase_counter++;
1174
2/2
✓ Branch 10 → 11 taken 912 times.
✓ Branch 10 → 12 taken 1840 times.
2752 if (phase_counter > 2)
1175 912 phase_counter = 0;
1176
1177
2/2
✓ Branch 23 → 13 taken 328864 times.
✓ Branch 23 → 24 taken 2752 times.
331616 for (col = x0; col <= x1; col++)
1178 {
1179 328864 int idx = row * width + col;
1180 328864 int pixel = image[idx];
1181 328864 int scaled = secugen_blc_scale_offset (pixel, offset);
1182 328864 int quot = scaled / 100;
1183 328864 int remainder = scaled - quot * 100;
1184 328864 int abs_rem = remainder < 0 ? -remainder : remainder;
1185 328864 int extra = 0;
1186 328864 int bit_idx = (col - x0) & 3;
1187 328864 int result;
1188
1189
2/2
✓ Branch 13 → 14 taken 19277 times.
✓ Branch 13 → 15 taken 309587 times.
328864 if (abs_rem > 74)
1190 19277 extra = (phase_table[phase_counter] >> bit_idx) & 1;
1191
2/2
✓ Branch 15 → 16 taken 76113 times.
✓ Branch 15 → 17 taken 233474 times.
309587 else if (abs_rem > 49)
1192 76113 extra = (phase_table[phase_counter + 4] >> bit_idx) & 1;
1193
2/2
✓ Branch 17 → 18 taken 16029 times.
✓ Branch 17 → 19 taken 217445 times.
233474 else if (abs_rem > 24)
1194 16029 extra = (phase_table[phase_counter + 8] >> bit_idx) & 1;
1195
1196
1/2
✓ Branch 19 → 20 taken 328864 times.
✗ Branch 19 → 21 not taken.
328864 if (scaled >= 0)
1197 328864 result = pixel - (quot + extra);
1198 else
1199 result = pixel - (quot - extra);
1200
1201 328864 if (result < 0)
1202 result = 0;
1203 328864 else if (result > 255)
1204 result = 255;
1205 328864 image[idx] = (guint8) result;
1206 }
1207 }
1208 }
1209 1 }
1210
1211 /* Edge-aware unsharp mask.
1212 * 3×3 box filter, threshold = pixel/8 + 2.
1213 * Only sharpens pixels where any neighbor differs by more than threshold.
1214 * Boost = (center - averaged) / 2, only for bright-side edges. */
1215 static void
1216 1 secugen_unsharp_mask (guint8 *image, int width, int height)
1217 {
1218 1 int total = width * height;
1219 2 g_autofree guint8 *original = NULL;
1220 1 g_autofree guint8 *averaged = NULL;
1221 1 int r, c;
1222
1223 1 original = g_malloc (total);
1224 1 memcpy (original, image, total);
1225 1 averaged = g_malloc (total);
1226 1 memcpy (averaged, original, total);
1227
1228 /* 3×3 box filter (AverageFilter with radius=1) */
1229
2/2
✓ Branch 8 → 9 taken 686 times.
✓ Branch 8 → 10 taken 1 time.
687 for (r = 1; r < height - 1; r++)
1230
2/2
✓ Branch 6 → 5 taken 654444 times.
✓ Branch 6 → 7 taken 686 times.
655130 for (c = 1; c < width - 1; c++)
1231 {
1232 654444 int sum = original[(r - 1) * width + c - 1]
1233 654444 + original[(r - 1) * width + c]
1234 654444 + original[(r - 1) * width + c + 1]
1235 654444 + original[r * width + c - 1]
1236 654444 + original[r * width + c]
1237 654444 + original[r * width + c + 1]
1238 654444 + original[(r + 1) * width + c - 1]
1239 654444 + original[(r + 1) * width + c]
1240 654444 + original[(r + 1) * width + c + 1];
1241
1242 654444 averaged[r * width + c] = (guint8) (sum / 9);
1243 }
1244
1245 /* Edge-aware sharpening (skips top 2 rows, bottom 1 row,
1246 * left/right 1 column) */
1247
2/2
✓ Branch 20 → 21 taken 685 times.
✓ Branch 20 → 22 taken 1 time.
686 for (r = 2; r < height - 1; r++)
1248
2/2
✓ Branch 18 → 11 taken 653490 times.
✓ Branch 18 → 19 taken 685 times.
654175 for (c = 1; c < width - 1; c++)
1249 {
1250 653490 int idx = r * width + c;
1251 653490 int center = original[idx];
1252 653490 int threshold = (center >> 3) + 2;
1253
1254
2/2
✓ Branch 11 → 12 taken 597797 times.
✓ Branch 11 → 15 taken 55693 times.
653490 if (abs (center - original[idx - 1]) > threshold ||
1255
2/2
✓ Branch 12 → 13 taken 568348 times.
✓ Branch 12 → 15 taken 29449 times.
597797 abs (center - original[idx + 1]) > threshold ||
1256
2/2
✓ Branch 13 → 14 taken 527483 times.
✓ Branch 13 → 15 taken 40865 times.
568348 abs (center - original[idx - width]) > threshold ||
1257
2/2
✓ Branch 14 → 15 taken 18804 times.
✓ Branch 14 → 17 taken 508679 times.
527483 abs (center - original[idx + width]) > threshold)
1258 {
1259 144811 int avg = averaged[idx];
1260
1261
2/2
✓ Branch 15 → 16 taken 49027 times.
✓ Branch 15 → 17 taken 95784 times.
144811 if (center > avg + 1)
1262 {
1263 49027 int boost = (center - avg) / 2;
1264 49027 int result = center + boost;
1265
1266 49027 if (result > 255)
1267 result = 255;
1268 49027 image[idx] = (guint8) result;
1269 }
1270 /* else: keep original (already in image) */
1271 }
1272 /* else: keep original */
1273 }
1274
1275 1 }
1276
1277 /*
1278 * Bilinear downsample from raw sensor to output dimensions.
1279 *
1280 * Uses 8-bit fixed-point weights rather than floating point so the decoded
1281 * image is bit-identical across compilers and architectures: the umockdev
1282 * test compares the output against a committed reference image pixel-for-pixel
1283 * (with no tolerance), and float rounding would diverge between build
1284 * environments. Interpolation weights sum to FRAC*FRAC, so the weighted sum is
1285 * rounded back down by that factor with round-to-nearest.
1286 */
1287 #define SECUGEN_RESIZE_FRAC_BITS 8
1288 #define SECUGEN_RESIZE_FRAC (1 << SECUGEN_RESIZE_FRAC_BITS)
1289
1290 static void
1291 2 secugen_resize_bilinear (const guint8 *src, int src_w, int src_h,
1292 guint8 *dst, int dst_w, int dst_h)
1293 {
1294 2 int r, c;
1295
1296
2/2
✓ Branch 13 → 3 taken 800 times.
✓ Branch 13 → 14 taken 2 times.
802 for (r = 0; r < dst_h; r++)
1297 {
1298 /* Source row position in fixed point (scaled by SECUGEN_RESIZE_FRAC). */
1299 800 int src_r = (int) ((gint64) r * (src_h - 1) * SECUGEN_RESIZE_FRAC /
1300 800 (dst_h - 1));
1301 800 int r0 = src_r >> SECUGEN_RESIZE_FRAC_BITS;
1302 800 int r1 = r0 + 1;
1303 800 int wr1 = src_r & (SECUGEN_RESIZE_FRAC - 1);
1304 800 int wr0 = SECUGEN_RESIZE_FRAC - wr1;
1305
1306
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 798 times.
800 if (r1 >= src_h)
1307 2 r1 = src_h - 1;
1308
1309
2/2
✓ Branch 11 → 6 taken 240000 times.
✓ Branch 11 → 12 taken 800 times.
240800 for (c = 0; c < dst_w; c++)
1310 {
1311 240000 int src_c = (int) ((gint64) c * (src_w - 1) * SECUGEN_RESIZE_FRAC /
1312 240000 (dst_w - 1));
1313 240000 int c0 = src_c >> SECUGEN_RESIZE_FRAC_BITS;
1314 240000 int c1 = c0 + 1;
1315 240000 int wc1 = src_c & (SECUGEN_RESIZE_FRAC - 1);
1316 240000 int wc0 = SECUGEN_RESIZE_FRAC - wc1;
1317 240000 guint32 acc;
1318 240000 int pixel;
1319
1320
2/2
✓ Branch 6 → 7 taken 800 times.
✓ Branch 6 → 8 taken 239200 times.
240000 if (c1 >= src_w)
1321 800 c1 = src_w - 1;
1322
1323 240000 acc = (guint32) wr0 * wc0 * src[r0 * src_w + c0]
1324 240000 + (guint32) wr0 * wc1 * src[r0 * src_w + c1]
1325 240000 + (guint32) wr1 * wc0 * src[r1 * src_w + c0]
1326 240000 + (guint32) wr1 * wc1 * src[r1 * src_w + c1];
1327
1328 240000 pixel = (int) ((acc + (SECUGEN_RESIZE_FRAC * SECUGEN_RESIZE_FRAC) / 2)
1329 240000 >> (2 * SECUGEN_RESIZE_FRAC_BITS));
1330
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 240000 times.
240000 if (pixel > 255)
1331 pixel = 255;
1332 240000 dst[r * dst_w + c] = (guint8) pixel;
1333 }
1334 }
1335 2 }
1336
1337 /* Flat-field (blend) correction.
1338 * Formula: result = pixel + ((cal_val - ref[i]) * curve[pixel]) >> 8
1339 * The curve is an S-curve that protects dark pixels and applies
1340 * full correction to bright pixels. */
1341 static void
1342 1 secugen_blend (guint8 *image,
1343 int width,
1344 int height,
1345 const guint8 *ref_image,
1346 int cal_val,
1347 const guint8 *curve)
1348 {
1349 1 int r, c;
1350
1351
2/2
✓ Branch 6 → 7 taken 400 times.
✓ Branch 6 → 8 taken 1 time.
401 for (r = 0; r < height; r++)
1352
2/2
✓ Branch 4 → 3 taken 120000 times.
✓ Branch 4 → 5 taken 400 times.
120400 for (c = 0; c < width; c++)
1353 {
1354 120000 int idx = r * width + c;
1355 120000 int pixel = image[idx];
1356 120000 int ref_val = ref_image[idx];
1357 120000 int curve_val = curve[pixel];
1358 120000 int correction = ((cal_val - ref_val) * curve_val) >> 8;
1359 120000 int result = pixel + correction;
1360
1361 120000 if (result > 255)
1362 result = 255;
1363 120000 else if (result < 0)
1364 result = 0;
1365 120000 image[idx] = (guint8) result;
1366 }
1367 1 }
1368
1369 /* 6-directional gradient sharpening.
1370 * Only enhances pixels where ALL 6 gradients agree on edge direction.
1371 * Case 1: all gradients > threshold (peak) → positive boost.
1372 * Case 2: all gradients < -threshold (valley) → negative boost. */
1373 static void
1374 1 secugen_sharpen (const guint8 *src,
1375 guint8 *dst,
1376 int width,
1377 int height,
1378 int threshold,
1379 int amount,
1380 int limit)
1381 {
1382 1 int neg_threshold = -threshold;
1383 1 int neg_amount = -amount;
1384 1 int r, c;
1385
1386
2/2
✓ Branch 20 → 21 taken 400 times.
✓ Branch 20 → 22 taken 1 time.
401 for (r = 0; r < height; r++)
1387
2/2
✓ Branch 18 → 3 taken 120000 times.
✓ Branch 18 → 19 taken 400 times.
120400 for (c = 0; c < width; c++)
1388 {
1389 120000 int idx = r * width + c;
1390
1391
8/8
✓ Branch 3 → 4 taken 119700 times.
✓ Branch 3 → 7 taken 300 times.
✓ Branch 4 → 5 taken 119400 times.
✓ Branch 4 → 7 taken 300 times.
✓ Branch 5 → 6 taken 119002 times.
✓ Branch 5 → 7 taken 398 times.
✓ Branch 6 → 7 taken 398 times.
✓ Branch 6 → 8 taken 118604 times.
120000 if (r == 0 || r == height - 1 || c == 0 || c == width - 1)
1392 {
1393 1396 dst[idx] = src[idx];
1394 1396 continue;
1395 }
1396
1397 {
1398 118604 int center = src[idx];
1399 118604 int d_up = center - src[(r - 1) * width + c];
1400 118604 int d_down = center - src[(r + 1) * width + c];
1401 118604 int d_left = center - src[r * width + c - 1];
1402 118604 int d_right = center - src[r * width + c + 1];
1403 118604 int d_ul = src[r * width + c - 1] -
1404 118604 src[(r - 1) * width + c - 1];
1405 118604 int d_dr = src[r * width + c + 1] -
1406 118604 src[(r + 1) * width + c + 1];
1407
1408
2/2
✓ Branch 8 → 9 taken 68 times.
✓ Branch 8 → 12 taken 118536 times.
118604 if (d_up > threshold && d_down > threshold &&
1409
2/2
✓ Branch 9 → 10 taken 6 times.
✓ Branch 9 → 12 taken 62 times.
68 d_left > threshold && d_right > threshold &&
1410
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 6 times.
6 d_ul > threshold && d_dr > threshold)
1411 {
1412 int g1 = MIN (MIN (d_up, d_left), amount);
1413 int g2 = MIN (MIN (d_down, d_right), amount);
1414 int g3 = MIN (MIN (d_ul, d_dr), amount);
1415 int avg = (g1 + g2 + g3) / 3;
1416 int boost = (avg - threshold) * limit / 10;
1417 int result = center + boost;
1418
1419 if (result > 255)
1420 result = 255;
1421 else if (result < 0)
1422 result = 0;
1423 dst[idx] = (guint8) result;
1424 }
1425
2/2
✓ Branch 12 → 13 taken 14 times.
✓ Branch 12 → 16 taken 118590 times.
118604 else if (d_up < neg_threshold && d_down < neg_threshold &&
1426
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 16 taken 14 times.
14 d_left < neg_threshold && d_right < neg_threshold &&
1427 d_ul < neg_threshold && d_dr < neg_threshold)
1428 {
1429 int g1 = MAX (MAX (d_up, d_left), neg_amount);
1430 int g2 = MAX (MAX (d_down, d_right), neg_amount);
1431 int g3 = MAX (MAX (d_ul, d_dr), neg_amount);
1432 int avg = (g1 + g2 + g3) / 3;
1433 int boost = (avg + threshold) * limit / 10;
1434 int result = center + boost;
1435
1436 if (result > 255)
1437 result = 255;
1438 else if (result < 0)
1439 result = 0;
1440 dst[idx] = (guint8) result;
1441 }
1442 else
1443 {
1444 118604 dst[idx] = src[idx];
1445 }
1446 }
1447 }
1448 1 }
1449
1450 typedef struct
1451 {
1452 guint8 *raw_frame;
1453 gboolean has_blc_offsets;
1454 gint16 blc_offsets[16];
1455 gboolean has_ref_image;
1456 guint8 *ref_image;
1457 guint16 cal_value;
1458 gboolean sharpen_enabled;
1459 guint8 sharpen_threshold;
1460 guint8 sharpen_amount;
1461 guint8 sharpen_limit;
1462 } SecugenCaptureTaskData;
1463
1464 typedef struct
1465 {
1466 guint8 *frame;
1467 guint8 *cal_raw;
1468 } SecugenDetectTaskData;
1469
1470 typedef struct
1471 {
1472 guint mean;
1473 gboolean finger_present;
1474 gboolean update_calibration;
1475 guint8 *new_cal_raw;
1476 } SecugenDetectTaskResult;
1477
1478 static void
1479 1 secugen_capture_task_data_free (SecugenCaptureTaskData *data)
1480 {
1481
1/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 4 not taken.
1 g_clear_pointer (&data->raw_frame, g_free);
1482
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 6 not taken.
1 g_clear_pointer (&data->ref_image, g_free);
1483 1 g_free (data);
1484 1 }
1485
1486 static void
1487 secugen_detect_task_data_free (SecugenDetectTaskData *data)
1488 {
1489 g_clear_pointer (&data->frame, g_free);
1490 g_clear_pointer (&data->cal_raw, g_free);
1491 g_free (data);
1492 }
1493
1494 static void
1495 secugen_detect_task_result_free (SecugenDetectTaskResult *result)
1496 {
1497 g_clear_pointer (&result->new_cal_raw, g_free);
1498 g_free (result);
1499 }
1500
1501 1 G_DEFINE_AUTOPTR_CLEANUP_FUNC (SecugenCaptureTaskData, secugen_capture_task_data_free)
1502 G_DEFINE_AUTOPTR_CLEANUP_FUNC (SecugenDetectTaskData, secugen_detect_task_data_free)
1503 G_DEFINE_AUTOPTR_CLEANUP_FUNC (SecugenDetectTaskResult, secugen_detect_task_result_free)
1504
1505 static void
1506 1 secugen_capture_process_thread (GTask *task,
1507 gpointer source_object G_GNUC_UNUSED,
1508 gpointer task_data,
1509 GCancellable *cancellable G_GNUC_UNUSED)
1510 {
1511 1 SecugenCaptureTaskData *data = task_data;
1512 2 g_autofree guint8 *image = g_malloc (SECUGEN_IMG_SIZE);
1513
1514
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 26 not taken.
1 if (g_task_return_error_if_cancelled (task))
1515 return;
1516
1517 /*
1518 * Image processing pipeline:
1519 * 1. Band compensation at 956x688
1520 * 2. Edge-aware unsharp mask at 956x688
1521 * 3. Bilinear downsample 956x688 → 300x400
1522 * 4. Flat-field blend at 300x400
1523 * 5. Directional sharpening at 300x400 (conditional)
1524 * 6. Bitwise NOT invert
1525 */
1526
1527 /* Step 1: BLC band compensation */
1528
1/2
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 7 not taken.
1 if (data->has_blc_offsets)
1529 1 secugen_blc_compensate (data->raw_frame, SECUGEN_RAW_WIDTH,
1530 1 SECUGEN_RAW_HEIGHT, data->blc_offsets);
1531
1532 /* Step 2: Edge-aware unsharp mask */
1533 1 secugen_unsharp_mask (data->raw_frame, SECUGEN_RAW_WIDTH, SECUGEN_RAW_HEIGHT);
1534
1535 /* Step 3: Bilinear downsample to 300x400 */
1536 1 secugen_resize_bilinear (data->raw_frame,
1537 SECUGEN_RAW_WIDTH, SECUGEN_RAW_HEIGHT,
1538 image,
1539 SECUGEN_IMG_WIDTH, SECUGEN_IMG_HEIGHT);
1540
1541
1/2
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 26 not taken.
1 if (g_task_return_error_if_cancelled (task))
1542 return;
1543
1544 /* Step 4: Blend flat-field correction */
1545
1/2
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 13 not taken.
1 if (data->has_ref_image)
1546 {
1547 1 secugen_blend (image,
1548 SECUGEN_IMG_WIDTH, SECUGEN_IMG_HEIGHT,
1549 1 data->ref_image,
1550 1 data->cal_value,
1551 SECUGEN_BLEND_CURVE);
1552 }
1553
1554
1/2
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 26 not taken.
1 if (g_task_return_error_if_cancelled (task))
1555 return;
1556
1557 /* Step 5: Directional sharpening (conditional) */
1558
1/2
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 19 not taken.
1 if (data->sharpen_enabled)
1559 {
1560 2 g_autofree guint8 *sharpened = g_malloc (SECUGEN_IMG_SIZE);
1561
1562 1 secugen_sharpen (image, sharpened,
1563 SECUGEN_IMG_WIDTH, SECUGEN_IMG_HEIGHT,
1564 1 data->sharpen_threshold,
1565 1 data->sharpen_amount,
1566 1 data->sharpen_limit);
1567 1 memcpy (image, sharpened, SECUGEN_IMG_SIZE);
1568 }
1569
1570 /* Step 6: Invert (bitwise NOT) */
1571
2/2
✓ Branch 21 → 20 taken 120000 times.
✓ Branch 21 → 22 taken 1 time.
120001 for (int i = 0; i < SECUGEN_IMG_SIZE; i++)
1572 120000 image[i] = ~image[i];
1573
1574
1/2
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 26 not taken.
1 if (g_task_return_error_if_cancelled (task))
1575 return;
1576
1577 1 g_task_return_pointer (task, g_steal_pointer (&image), g_free);
1578 }
1579
1580 static void
1581 secugen_detect_analyze_thread (GTask *task,
1582 gpointer source_object G_GNUC_UNUSED,
1583 gpointer task_data,
1584 GCancellable *cancellable)
1585 {
1586 SecugenDetectTaskData *data = task_data;
1587
1588 g_autoptr(SecugenDetectTaskResult) result = g_new0 (SecugenDetectTaskResult, 1);
1589 guint64 sum = 0;
1590 int count = 0;
1591 int start_row = SECUGEN_RAW_HEIGHT / 4;
1592 int end_row = 3 * SECUGEN_RAW_HEIGHT / 4;
1593 int start_col = SECUGEN_RAW_WIDTH / 4;
1594 int end_col = 3 * SECUGEN_RAW_WIDTH / 4;
1595
1596 if (g_task_return_error_if_cancelled (task))
1597 return;
1598
1599 /* Compute mean brightness of central 50% region */
1600 for (int y = start_row; y < end_row; y++)
1601 {
1602 for (int x = start_col; x < end_col; x++)
1603 {
1604 int idx = y * SECUGEN_RAW_WIDTH + x;
1605 int val = (int) data->frame[idx] - (int) data->cal_raw[idx];
1606
1607 if (val < 0)
1608 val = 0;
1609 sum += val;
1610 count++;
1611 }
1612
1613 if (g_task_return_error_if_cancelled (task))
1614 return;
1615 }
1616
1617 result->mean = count > 0 ? (guint) (sum / count) : 0;
1618 result->finger_present = (result->mean >= SECUGEN_FINGER_THRESHOLD);
1619 result->update_calibration = (!result->finger_present && result->mean <= 5);
1620 if (result->update_calibration)
1621 result->new_cal_raw = g_memdup2 (data->frame, SECUGEN_BULK_BUF_SIZE);
1622
1623 g_task_return_pointer (task,
1624 g_steal_pointer (&result),
1625 (GDestroyNotify) secugen_detect_task_result_free);
1626 }
1627
1628 /* ================================================================
1629 * Capture State Machine
1630 *
1631 * Capture sequence observed in USB captures:
1632 * I2C 0x32 = 0x00 -> SET_EXPOSURE -> START_CAPTURE ->
1633 * START_STREAM -> bulk read 657KB -> STOP_STREAM -> GET_STATUS
1634 * ================================================================ */
1635
1636 enum capture_states {
1637 CAPTURE_REG32_CLEAR = 0,
1638 CAPTURE_LED_ON,
1639 CAPTURE_SET_EXPOSURE,
1640 CAPTURE_START,
1641 CAPTURE_STREAM_ON,
1642 CAPTURE_BULK_READ,
1643 CAPTURE_STREAM_OFF,
1644 CAPTURE_GET_STATUS,
1645 CAPTURE_DONE,
1646 CAPTURE_NUM_STATES,
1647 };
1648
1649 static void
1650 1 capture_process_done (GObject *source_object,
1651 GAsyncResult *result,
1652 gpointer user_data)
1653 {
1654 1 g_autoptr(FpImage) img = NULL;
1655 1 g_autoptr(GError) error = NULL;
1656
1/4
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 1 time.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 25 not taken.
1 g_autofree guint8 *processed = NULL;
1657 1 FpDevice *dev = FP_DEVICE (source_object);
1658 1 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
1659 1 FpiSsm *ssm = user_data;
1660
1661 1 processed = g_task_propagate_pointer (G_TASK (result), &error);
1662
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 11 taken 1 time.
1 if (error)
1663 {
1664 if (self->deactivating &&
1665 g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
1666 {
1667 fpi_ssm_mark_completed (ssm);
1668 return;
1669 }
1670
1671 fpi_ssm_mark_failed (ssm, g_steal_pointer (&error));
1672 return;
1673 }
1674
1675
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 14 taken 1 time.
1 if (self->deactivating)
1676 {
1677 fpi_ssm_mark_completed (ssm);
1678 return;
1679 }
1680
1681 1 img = fp_image_new (SECUGEN_IMG_WIDTH, SECUGEN_IMG_HEIGHT);
1682 1 img->ppmm = SECUGEN_PPMM;
1683 1 img->flags = FPI_IMAGE_V_FLIPPED | FPI_IMAGE_H_FLIPPED;
1684 1 memcpy (img->data, processed, SECUGEN_IMG_SIZE);
1685
1686 1 fpi_image_device_image_captured (FP_IMAGE_DEVICE (dev), g_steal_pointer (&img));
1687 1 fpi_ssm_mark_completed (ssm);
1688 }
1689
1690 static void
1691 1 capture_status_cb (FpiUsbTransfer *transfer,
1692 FpDevice *dev,
1693 gpointer user_data,
1694 GError *error)
1695 {
1696 1 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
1697
1698
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 1 time.
1 if (error)
1699 {
1700 fpi_ssm_mark_failed (transfer->ssm, error);
1701 return;
1702 }
1703
1704 1 memcpy (self->last_status, transfer->buffer,
1705 1 MIN (transfer->actual_length, 4));
1706 1 fpi_ssm_next_state (transfer->ssm);
1707 }
1708
1709 static void
1710 9 capture_run_state (FpiSsm *ssm, FpDevice *_dev)
1711 {
1712 9 FpImageDevice *dev = FP_IMAGE_DEVICE (_dev);
1713 9 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (_dev);
1714
1715
9/10
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 1 time.
✓ Branch 3 → 8 taken 1 time.
✓ Branch 3 → 10 taken 1 time.
✓ Branch 3 → 12 taken 1 time.
✓ Branch 3 → 14 taken 1 time.
✓ Branch 3 → 16 taken 1 time.
✓ Branch 3 → 18 taken 1 time.
✓ Branch 3 → 20 taken 1 time.
✗ Branch 3 → 33 not taken.
9 switch (fpi_ssm_get_cur_state (ssm))
1716 {
1717 1 case CAPTURE_REG32_CLEAR:
1718 /* Must clear reg 0x32 before each capture */
1719 1 secugen_i2c_write (ssm, dev, 0x32, 0x00);
1720 1 break;
1721
1722 1 case CAPTURE_LED_ON:
1723 1 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_LED_CONTROL, 0x0001, 0);
1724 1 break;
1725
1726 1 case CAPTURE_SET_EXPOSURE:
1727 1 secugen_set_exposure (ssm, dev, self->exposure);
1728 1 break;
1729
1730 1 case CAPTURE_START:
1731 1 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_CAPTURE, 0x0001, 0);
1732 1 break;
1733
1734 1 case CAPTURE_STREAM_ON:
1735 1 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_STREAM, 0x0000, 0);
1736 1 break;
1737
1738 1 case CAPTURE_BULK_READ:
1739 1 secugen_read_frame (ssm, _dev, self->bulk_buffer);
1740 1 break;
1741
1742 1 case CAPTURE_STREAM_OFF:
1743 1 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_STOP_STREAM, 0x0000, 0);
1744 1 break;
1745
1746 1 case CAPTURE_GET_STATUS:
1747 1 secugen_ctrl_in (ssm, dev, SECUGEN_REQ_GET_STATUS,
1748 0x0000, 0, 4, capture_status_cb);
1749 1 break;
1750
1751 case CAPTURE_DONE:
1752 {
1753 10 g_autoptr(GTask) task = NULL;
1754 2 g_autoptr(SecugenCaptureTaskData) task_data = NULL;
1755
1756 /* Snapshot frame + calibration parameters for worker-thread processing.
1757 * The shared driver buffers remain owned by the main thread. */
1758 1 task_data = g_new0 (SecugenCaptureTaskData, 1);
1759 1 task_data->raw_frame = g_memdup2 (self->bulk_buffer, SECUGEN_RAW_SIZE);
1760 1 task_data->has_blc_offsets = self->has_blc_offsets;
1761 1 memcpy (task_data->blc_offsets, self->blc_offsets, sizeof (task_data->blc_offsets));
1762 1 task_data->has_ref_image = self->has_ref_image;
1763
1764
1/2
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 25 not taken.
1 if (self->has_ref_image)
1765 1 task_data->ref_image = g_memdup2 (self->ref_image, SECUGEN_IMG_SIZE);
1766
1767 1 task_data->cal_value = self->cal_value;
1768 1 task_data->sharpen_enabled = self->sharpen_enabled;
1769 1 task_data->sharpen_threshold = self->sharpen_threshold;
1770 1 task_data->sharpen_amount = self->sharpen_amount;
1771 1 task_data->sharpen_limit = self->sharpen_limit;
1772
1773 1 task = g_task_new (FP_DEVICE (dev),
1774 fpi_device_get_cancellable (FP_DEVICE (dev)),
1775 capture_process_done,
1776 ssm);
1777 1 g_task_set_source_tag (task, secugen_capture_process_thread);
1778 1 g_task_set_check_cancellable (task, TRUE);
1779 1 g_task_set_task_data (task, g_steal_pointer (&task_data),
1780 (GDestroyNotify) secugen_capture_task_data_free);
1781 1 g_task_run_in_thread (task, secugen_capture_process_thread);
1782
1/2
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 33 not taken.
1 break;
1783 }
1784 }
1785 9 }
1786
1787 static void secugen_finish_deactivate (FpImageDevice *dev);
1788
1789 static void
1790 1 capture_ssm_complete (FpiSsm *ssm, FpDevice *dev, GError *error)
1791 {
1792 2 g_autoptr(GError) local_error = error;
1793 1 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
1794 1 FpImageDevice *imgdev = FP_IMAGE_DEVICE (dev);
1795
1796 1 self->ssm_count--;
1797
1798
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 7 taken 1 time.
1 if (self->deactivating)
1799 {
1800 if (self->ssm_count == 0)
1801 secugen_finish_deactivate (imgdev);
1802 return;
1803 }
1804
1805
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 1 time.
1 if (local_error)
1806 {
1807 fp_warn ("Capture failed: %s", local_error->message);
1808 fpi_image_device_session_error (imgdev, g_steal_pointer (&local_error));
1809 }
1810 }
1811
1812 /* No-op callback for fire-and-forget USB transfers (e.g. LED control) */
1813 static void
1814 1 secugen_noop_cb (FpiUsbTransfer *transfer,
1815 FpDevice *dev,
1816 gpointer user_data,
1817 GError *error)
1818 {
1819 2 g_autoptr(GError) local_error = error;
1820
1821
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 1 time.
1 if (local_error)
1822 fp_warn ("Fire-and-forget transfer failed: %s", local_error->message);
1823 1 }
1824
1825 /* ================================================================
1826 * Finger Detection (Image-Based)
1827 *
1828 * The SIDO020A has no touch/proximity chip. GET_STATUS always returns
1829 * zeros. Finger presence is instead detected by capturing preview
1830 * frames and checking mean brightness:
1831 * - No finger: dark image (mean ~20)
1832 * - Finger present: reflected LED light (mean ~70+)
1833 *
1834 * We implement this as a detect SSM that captures a frame, computes
1835 * mean brightness of the central region, and reports finger status.
1836 * If no finger, we schedule another poll after SECUGEN_FINGER_POLL_MS.
1837 * ================================================================ */
1838
1839 enum detect_states {
1840 DETECT_REG32_CLEAR = 0,
1841 DETECT_SET_EXPOSURE,
1842 DETECT_START_CAPTURE,
1843 DETECT_START_STREAM,
1844 DETECT_BULK_READ,
1845 DETECT_STOP_STREAM,
1846 DETECT_ANALYZE,
1847 DETECT_NUM_STATES,
1848 };
1849
1850 static void detect_start (FpImageDevice *dev);
1851 static void detect_retry_timeout (FpDevice *dev,
1852 gpointer user_data G_GNUC_UNUSED);
1853 static void detect_analyze_done (GObject *source_object,
1854 GAsyncResult *result,
1855 gpointer user_data);
1856
1857 static void
1858 detect_analyze_done (GObject *source_object,
1859 GAsyncResult *result,
1860 gpointer user_data)
1861 {
1862 FpDevice *dev = FP_DEVICE (source_object);
1863 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
1864 FpiSsm *ssm = user_data;
1865
1866 g_autoptr(GError) error = NULL;
1867 g_autoptr(SecugenDetectTaskResult) analysis = NULL;
1868
1869 analysis = g_task_propagate_pointer (G_TASK (result), &error);
1870 if (error)
1871 {
1872 if (self->deactivating &&
1873 g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
1874 {
1875 fpi_ssm_mark_completed (ssm);
1876 return;
1877 }
1878 fpi_ssm_mark_failed (ssm, g_steal_pointer (&error));
1879 return;
1880 }
1881
1882 if (self->deactivating)
1883 {
1884 fpi_ssm_mark_completed (ssm);
1885 return;
1886 }
1887
1888 fp_dbg ("Finger detect: mean brightness = %u (threshold %d)",
1889 analysis->mean, SECUGEN_FINGER_THRESHOLD);
1890
1891 if (analysis->finger_present)
1892 {
1893 fp_dbg ("Finger detected!");
1894 fpi_image_device_report_finger_status (FP_IMAGE_DEVICE (dev), TRUE);
1895 }
1896 else
1897 {
1898 if (analysis->update_calibration && analysis->new_cal_raw)
1899 memcpy (self->cal_raw, analysis->new_cal_raw, SECUGEN_BULK_BUF_SIZE);
1900
1901 self->finger_poll_source =
1902 fpi_device_add_timeout (FP_DEVICE (dev),
1903 SECUGEN_FINGER_POLL_MS,
1904 detect_retry_timeout,
1905 NULL,
1906 NULL);
1907 }
1908
1909 fpi_ssm_mark_completed (ssm);
1910 }
1911
1912 static void
1913 detect_run_state (FpiSsm *ssm, FpDevice *_dev)
1914 {
1915 FpImageDevice *dev = FP_IMAGE_DEVICE (_dev);
1916 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (_dev);
1917
1918 switch (fpi_ssm_get_cur_state (ssm))
1919 {
1920 case DETECT_REG32_CLEAR:
1921 secugen_i2c_write (ssm, dev, 0x32, 0x00);
1922 break;
1923
1924 case DETECT_SET_EXPOSURE:
1925 secugen_set_exposure (ssm, dev, self->exposure);
1926 break;
1927
1928 case DETECT_START_CAPTURE:
1929 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_CAPTURE, 0x0001, 0);
1930 break;
1931
1932 case DETECT_START_STREAM:
1933 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_START_STREAM, 0x0000, 0);
1934 break;
1935
1936 case DETECT_BULK_READ:
1937 secugen_read_frame (ssm, _dev, self->bulk_buffer);
1938 break;
1939
1940 case DETECT_STOP_STREAM:
1941 secugen_ctrl_out (ssm, dev, SECUGEN_REQ_STOP_STREAM, 0x0000, 0);
1942 break;
1943
1944 case DETECT_ANALYZE:
1945 {
1946 g_autoptr(GTask) task = NULL;
1947 g_autoptr(SecugenDetectTaskData) task_data = NULL;
1948
1949 /* If a deactivate arrived while this detect cycle ran, do not report
1950 * finger state or arm another poll - just unwind the SSM so the
1951 * deferred deactivation can complete. */
1952 if (self->deactivating)
1953 {
1954 fpi_ssm_mark_completed (ssm);
1955 break;
1956 }
1957
1958 /* Snapshot current frame/calibration for worker-thread analysis. */
1959 task_data = g_new0 (SecugenDetectTaskData, 1);
1960 task_data->frame = g_memdup2 (self->bulk_buffer, SECUGEN_BULK_BUF_SIZE);
1961 task_data->cal_raw = g_memdup2 (self->cal_raw, SECUGEN_BULK_BUF_SIZE);
1962
1963 task = g_task_new (FP_DEVICE (dev),
1964 fpi_device_get_cancellable (FP_DEVICE (dev)),
1965 detect_analyze_done,
1966 ssm);
1967 g_task_set_source_tag (task, secugen_detect_analyze_thread);
1968 g_task_set_check_cancellable (task, TRUE);
1969 g_task_set_task_data (task, g_steal_pointer (&task_data),
1970 (GDestroyNotify) secugen_detect_task_data_free);
1971 g_task_run_in_thread (task, secugen_detect_analyze_thread);
1972 }
1973 break;
1974 }
1975 }
1976
1977 static void
1978 detect_retry_timeout (FpDevice *dev, gpointer user_data G_GNUC_UNUSED)
1979 {
1980 FpImageDevice *img_dev = FP_IMAGE_DEVICE (dev);
1981 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (img_dev);
1982
1983 self->finger_poll_source = NULL;
1984 detect_start (img_dev);
1985 }
1986
1987 static void
1988 detect_ssm_complete (FpiSsm *ssm, FpDevice *dev, GError *error)
1989 {
1990 g_autoptr(GError) local_error = error;
1991 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
1992
1993 self->ssm_count--;
1994
1995 if (self->deactivating)
1996 {
1997 if (self->ssm_count == 0)
1998 secugen_finish_deactivate (FP_IMAGE_DEVICE (dev));
1999 return;
2000 }
2001
2002 if (local_error)
2003 {
2004 fp_warn ("Finger detect failed: %s", local_error->message);
2005 fpi_image_device_session_error (FP_IMAGE_DEVICE (dev),
2006 g_steal_pointer (&local_error));
2007 }
2008 }
2009
2010 static void
2011 detect_start (FpImageDevice *dev)
2012 {
2013 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
2014 FpiSsm *ssm;
2015
2016 self->ssm_count++;
2017 ssm = fpi_ssm_new (FP_DEVICE (dev), detect_run_state, DETECT_NUM_STATES);
2018 fpi_ssm_start (ssm, detect_ssm_complete);
2019 }
2020
2021 static void
2022 1 finger_off_timeout (FpDevice *dev, gpointer user_data G_GNUC_UNUSED)
2023 {
2024 1 FpImageDevice *img_dev = FP_IMAGE_DEVICE (dev);
2025 1 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (img_dev);
2026
2027 1 self->finger_poll_source = NULL;
2028 1 fpi_image_device_report_finger_status (img_dev, FALSE);
2029 1 }
2030
2031 /* ================================================================
2032 * Device Lifecycle
2033 * ================================================================ */
2034
2035 static void
2036 1 dev_init (FpImageDevice *dev)
2037 {
2038 1 g_autoptr(GError) error = NULL;
2039
1/4
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 1 time.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
1 g_autoptr(GPtrArray) interfaces = NULL;
2040 1 GUsbInterface *iface = NULL;
2041 1 int i;
2042
2043 1 interfaces = g_usb_device_get_interfaces (
2044 fpi_device_get_usb_device (FP_DEVICE (dev)), &error);
2045
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 13 taken 1 time.
1 if (error)
2046 {
2047 fpi_image_device_open_complete (dev, g_steal_pointer (&error));
2048 return;
2049 }
2050
2051 /* Find our vendor-specific interface (0xFF/0xFF/0xFF) */
2052
1/2
✓ Branch 13 → 6 taken 1 time.
✗ Branch 13 → 14 not taken.
1 for (i = 0; i < (int) interfaces->len; i++)
2053 {
2054 1 GUsbInterface *cur = g_ptr_array_index (interfaces, i);
2055
2056
2/4
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 12 not taken.
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 12 not taken.
2 if (g_usb_interface_get_class (cur) == 0xFF &&
2057
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 14 taken 1 time.
2 g_usb_interface_get_subclass (cur) == 0xFF &&
2058 1 g_usb_interface_get_protocol (cur) == 0xFF)
2059 {
2060 iface = cur;
2061 break;
2062 }
2063 }
2064
2065
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 18 taken 1 time.
1 if (!iface)
2066 {
2067 fpi_image_device_open_complete (dev,
2068 fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL,
2069 "Could not find fingerprint interface"));
2070 return;
2071 }
2072
2073 {
2074 1 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
2075
2076 1 self->iface_num = g_usb_interface_get_number (iface);
2077
2078
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 24 taken 1 time.
1 if (!g_usb_device_claim_interface (
2079 fpi_device_get_usb_device (FP_DEVICE (dev)),
2080 self->iface_num, 0, &error))
2081 {
2082 fpi_image_device_open_complete (dev, g_steal_pointer (&error));
2083 return;
2084 }
2085
2086 /* Allocate buffers */
2087 1 self->cal_raw = g_malloc0 (SECUGEN_BULK_BUF_SIZE);
2088 1 self->bulk_buffer = g_malloc0 (SECUGEN_BULK_BUF_SIZE);
2089 1 self->fw_data = g_malloc0 (SECUGEN_FW_DATA_SIZE);
2090 1 self->fw_data_len = 0;
2091 1 self->ref_image = NULL;
2092 1 self->has_ref_image = FALSE;
2093 1 self->has_blc_offsets = FALSE;
2094 1 self->cal_value = SECUGEN_BLEND_CAL_VAL; /* Default 240, overridden by FW */
2095 1 self->sharpen_enabled = FALSE;
2096 }
2097
2098 1 fpi_image_device_open_complete (dev, NULL);
2099 }
2100
2101 static void
2102 1 dev_deinit (FpImageDevice *dev)
2103 {
2104 2 g_autoptr(GError) error = NULL;
2105 1 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
2106
2107
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 g_clear_pointer (&self->finger_poll_source, g_source_destroy);
2108
2109
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 6 not taken.
1 g_clear_pointer (&self->cal_raw, g_free);
2110
1/2
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 8 not taken.
1 g_clear_pointer (&self->bulk_buffer, g_free);
2111
1/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 10 not taken.
1 g_clear_pointer (&self->fw_data, g_free);
2112
1/2
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 12 not taken.
1 g_clear_pointer (&self->ref_image, g_free);
2113 1 self->has_ref_image = FALSE;
2114
2115 1 g_usb_device_release_interface (
2116 fpi_device_get_usb_device (FP_DEVICE (dev)),
2117 1 self->iface_num, 0, &error);
2118
2119
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 1 time.
1 fpi_image_device_close_complete (dev, g_steal_pointer (&error));
2120 1 }
2121
2122 static void
2123 1 activate_complete (FpiSsm *ssm, FpDevice *dev, GError *error)
2124 {
2125 1 g_autoptr(GError) local_error = error;
2126 1 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
2127 1 FpImageDevice *imgdev = FP_IMAGE_DEVICE (dev);
2128
2129 1 self->ssm_count--;
2130
2131
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 7 taken 1 time.
1 if (self->deactivating)
2132 {
2133 if (self->ssm_count == 0)
2134 secugen_finish_deactivate (imgdev);
2135 return;
2136 }
2137
2138
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
1 if (local_error)
2139 fp_warn ("Activation failed: %s", local_error->message);
2140
2141 1 fpi_image_device_activate_complete (imgdev, g_steal_pointer (&local_error));
2142 }
2143
2144 static void
2145 1 dev_activate (FpImageDevice *dev)
2146 {
2147 1 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
2148 1 FpiSsm *ssm;
2149
2150 /* Reset init counters */
2151 1 self->init_reg_idx = 0;
2152 1 self->fw_read_idx = 0;
2153 1 self->exposure = SECUGEN_EXPOSURE_NORMAL;
2154 1 self->deactivating = FALSE;
2155 1 self->ssm_count++;
2156
2157 1 ssm = fpi_ssm_new (FP_DEVICE (dev), init_run_state, INIT_NUM_STATES);
2158 1 fpi_ssm_start (ssm, activate_complete);
2159 1 }
2160
2161 /* Turn the LED off and report deactivation complete. Called either directly
2162 * from dev_deactivate (nothing in flight) or, when a deactivate arrives while
2163 * an SSM is still running, from that SSM's completion handler once it unwinds. */
2164 static void
2165 1 secugen_finish_deactivate (FpImageDevice *dev)
2166 {
2167 2 g_autoptr(FpiUsbTransfer) transfer = NULL;
2168 1 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
2169
2170 1 self->deactivating = FALSE;
2171
2172 /* Turn off LED (fire and forget) */
2173 1 transfer = fpi_usb_transfer_new (FP_DEVICE (dev));
2174 1 fpi_usb_transfer_fill_control (transfer,
2175 G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
2176 G_USB_DEVICE_REQUEST_TYPE_VENDOR,
2177 G_USB_DEVICE_RECIPIENT_DEVICE,
2178 SECUGEN_REQ_LED_CONTROL,
2179 0x0000, 0, 0);
2180 1 fpi_usb_transfer_submit (g_steal_pointer (&transfer), SECUGEN_CTRL_TIMEOUT, NULL,
2181 secugen_noop_cb, NULL);
2182
2183 1 fpi_image_device_deactivate_complete (dev, NULL);
2184 1 }
2185
2186 static void
2187 1 dev_deactivate (FpImageDevice *dev)
2188 {
2189 1 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
2190
2191 /* Cancel any pending timeout */
2192
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 g_clear_pointer (&self->finger_poll_source, g_source_destroy);
2193
2194 /* If an init/detect/capture SSM is still in flight, defer completion until
2195 * it unwinds - its completion handler calls secugen_finish_deactivate(). This
2196 * guarantees no bulk transfer is in flight (and no chunk callback can run
2197 * against a reset buffer) by the time we report deactivation complete. */
2198
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
1 if (self->ssm_count > 0)
2199 {
2200 self->deactivating = TRUE;
2201 return;
2202 }
2203
2204 1 secugen_finish_deactivate (dev);
2205 }
2206
2207 static void
2208 8 dev_change_state (FpImageDevice *dev,
2209 FpiImageDeviceState state)
2210 {
2211 8 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (dev);
2212
2213
4/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 13 taken 1 time.
✓ Branch 2 → 19 taken 1 time.
✓ Branch 2 → 22 taken 5 times.
8 switch (state)
2214 {
2215 1 case FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_ON:
2216 1 fp_dbg ("Awaiting finger (image-based detection)");
2217
2218 /* Under fpi_device_emulation (set by the umockdev test harness so
2219 * drivers can adapt to replay; see tests/meson.build) skip the
2220 * timer-driven detect poll: each poll consumes a full ~658KB frame,
2221 * so exercising it in replay would require recording an unbounded
2222 * number of extra frames in the test fixture. The recorded capture
2223 * starts with the finger already present, so report it directly -
2224 * the same approach other drivers use for replay-incompatible
2225 * hardware behavior (e.g. elanspi's HID reset skip). */
2226
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 11 taken 1 time.
1 if (fpi_device_emulation_mode_enabled (FP_DEVICE (self)))
2227 {
2228 1 fpi_image_device_report_finger_status (dev, TRUE);
2229 1 break;
2230 }
2231
2232 /* Turn on LED */
2233 {
2234 g_autoptr(FpiUsbTransfer) transfer = fpi_usb_transfer_new (FP_DEVICE (dev));
2235
2236 fpi_usb_transfer_fill_control (transfer,
2237 G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
2238 G_USB_DEVICE_REQUEST_TYPE_VENDOR,
2239 G_USB_DEVICE_RECIPIENT_DEVICE,
2240 SECUGEN_REQ_LED_CONTROL,
2241 0x0001, 0, 0);
2242 fpi_usb_transfer_submit (g_steal_pointer (&transfer), SECUGEN_CTRL_TIMEOUT, NULL,
2243 secugen_noop_cb, NULL);
2244 }
2245
2246 /* Start image-based finger detection polling */
2247 detect_start (dev);
2248 break;
2249
2250 1 case FPI_IMAGE_DEVICE_STATE_CAPTURE:
2251 {
2252 1 FpiSsm *ssm;
2253
2254 1 fp_dbg ("Starting image capture");
2255
2256 /* Cancel timeout if still running */
2257
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1 time.
1 g_clear_pointer (&self->finger_poll_source, g_source_destroy);
2258
2259 1 self->ssm_count++;
2260 1 ssm = fpi_ssm_new (FP_DEVICE (dev), capture_run_state,
2261 CAPTURE_NUM_STATES);
2262 1 fpi_ssm_start (ssm, capture_ssm_complete);
2263 }
2264 1 break;
2265
2266 1 case FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_OFF:
2267 1 fp_dbg ("Waiting for finger off");
2268 1 self->finger_poll_source = fpi_device_add_timeout (FP_DEVICE (dev),
2269 500,
2270 finger_off_timeout,
2271 NULL,
2272 NULL);
2273 1 break;
2274
2275 case FPI_IMAGE_DEVICE_STATE_IDLE:
2276 case FPI_IMAGE_DEVICE_STATE_INACTIVE:
2277 case FPI_IMAGE_DEVICE_STATE_ACTIVATING:
2278 case FPI_IMAGE_DEVICE_STATE_DEACTIVATING:
2279 break;
2280 }
2281 8 }
2282
2283 /* ================================================================
2284 * Driver Registration
2285 * ================================================================ */
2286
2287 static const FpIdEntry id_table[] = {
2288 { .vid = 0x1162, .pid = 0x2200, }, /* SecuGen Hamster Pro 20 (FDU05) */
2289 { .vid = 0, .pid = 0, }, /* terminator */
2290 };
2291
2292 static void
2293 1 fpi_device_secugen_init (FpiDeviceSecugen *self)
2294 {
2295 1 self->init_reg_idx = 0;
2296 1 self->fw_read_idx = 0;
2297 1 self->finger_poll_source = NULL;
2298 1 self->exposure = SECUGEN_EXPOSURE_NORMAL;
2299 1 self->deactivating = FALSE;
2300 1 self->ssm_count = 0;
2301 1 }
2302
2303 static void
2304 1 fpi_device_secugen_finalize (GObject *object)
2305 {
2306 1 FpiDeviceSecugen *self = FPI_DEVICE_SECUGEN (object);
2307
2308
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
1 g_clear_pointer (&self->finger_poll_source, g_source_destroy);
2309
2310 /* Safety net: free heap buffers even if img_close never ran (e.g. an
2311 * activation failure tore the device down before dev_deinit). */
2312
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1 time.
1 g_clear_pointer (&self->cal_raw, g_free);
2313
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 1 time.
1 g_clear_pointer (&self->bulk_buffer, g_free);
2314
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
1 g_clear_pointer (&self->fw_data, g_free);
2315
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1 time.
1 g_clear_pointer (&self->ref_image, g_free);
2316
2317 1 G_OBJECT_CLASS (fpi_device_secugen_parent_class)->finalize (object);
2318 1 }
2319
2320 static void
2321 125 fpi_device_secugen_class_init (FpiDeviceSecugenClass *klass)
2322 {
2323 125 GObjectClass *obj_class = G_OBJECT_CLASS (klass);
2324 125 FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass);
2325 125 FpImageDeviceClass *img_class = FP_IMAGE_DEVICE_CLASS (klass);
2326
2327 125 obj_class->finalize = fpi_device_secugen_finalize;
2328
2329 125 dev_class->id = "secugen";
2330 125 dev_class->full_name = "SecuGen Hamster Pro 20";
2331 125 dev_class->type = FP_DEVICE_TYPE_USB;
2332 125 dev_class->id_table = id_table;
2333 125 dev_class->scan_type = FP_SCAN_TYPE_PRESS;
2334
2335 125 img_class->img_open = dev_init;
2336 125 img_class->img_close = dev_deinit;
2337 125 img_class->activate = dev_activate;
2338 125 img_class->deactivate = dev_deactivate;
2339 125 img_class->change_state = dev_change_state;
2340
2341 125 img_class->bz3_threshold = 24;
2342
2343 125 img_class->img_width = SECUGEN_IMG_WIDTH;
2344 125 img_class->img_height = SECUGEN_IMG_HEIGHT;
2345 }
2346