From fafe421abf5c51929a2cfe55c2701b0cdd651111 Mon Sep 17 00:00:00 2001 From: daemonhorn Date: Sat, 22 Aug 2026 20:51:02 -0400 Subject: [PATCH 01/12] Add opt-in 4-byte gain-aware mcu_get_image request 27c6:533c's mcu_get_image request is 4 bytes (flags, 0x06, gain, 0x00) rather than this family's usual 1-byte payload, with flags distinguishing a no-finger calibration frame (0x01) from a live capture (0x41) -- confirmed against driver_53xc.py, the proven Python reference for this device. Added goodix_send_mcu_get_image_gain()/goodix_tls_read_image_gain() alongside the existing 1-byte-payload functions (unchanged), and a new opt-in use_gain_image_request/image_gain pair on FpiDeviceGoodixTls5xxClass, defaulting to FALSE/0 so goodix511 and every other driver sharing this base class keeps its exact current behavior. goodix5xx.c's calibrate_run/scan_get_img now route through this opt-in at both image-request call sites. This does not make a working 533c driver on its own -- see findings/native-driver-architecture.md in the parent project for why goodix5xx.c's FDT mode-switching (a separate, structural gap -- static get_mcu_cfg() vs. this device's per-session dynamic FDT template) still needs a real fork of the shared SCAN state machine, not a patch. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y9XXWG7y2kfBEXJj3MS5yv --- libfprint/drivers/goodixtls/goodix.c | 40 +++++++++++++++++++++++++ libfprint/drivers/goodixtls/goodix.h | 38 +++++++++++++++++++++++ libfprint/drivers/goodixtls/goodix5xx.c | 23 ++++++++++++-- libfprint/drivers/goodixtls/goodix5xx.h | 11 +++++++ 4 files changed, 110 insertions(+), 2 deletions(-) diff --git a/libfprint/drivers/goodixtls/goodix.c b/libfprint/drivers/goodixtls/goodix.c index 236589f78..a35e09ca8 100644 --- a/libfprint/drivers/goodixtls/goodix.c +++ b/libfprint/drivers/goodixtls/goodix.c @@ -641,6 +641,32 @@ goodix_send_mcu_get_image (FpDevice *dev, GoodixImageCallback callback, NULL, NULL); } +void +goodix_send_mcu_get_image_gain (FpDevice *dev, guint8 flags, guint8 gain, + GoodixImageCallback callback, + gpointer user_data) +{ + guint8 payload[4] = {flags, 0x06, gain, 0x00}; + GoodixCallbackInfo *cb_info; + + if (callback) + { + cb_info = malloc (sizeof (GoodixCallbackInfo)); + + cb_info->callback = G_CALLBACK (callback); + cb_info->user_data = user_data; + + goodix_send_protocol (dev, GOODIX_CMD_MCU_GET_IMAGE, payload, + sizeof (payload), NULL, TRUE, GOODIX_TIMEOUT, TRUE, + goodix_receive_default, cb_info); + return; + } + + goodix_send_protocol (dev, GOODIX_CMD_MCU_GET_IMAGE, payload, + sizeof (payload), NULL, TRUE, GOODIX_TIMEOUT, TRUE, + NULL, NULL); +} + void goodix_send_mcu_switch_to_fdt_down (FpDevice *dev, const guint8 *mode, guint16 length, GDestroyNotify free_func, @@ -1489,6 +1515,20 @@ goodix_tls_read_image (FpDevice *dev, GoodixImageCallback callback, goodix_send_mcu_get_image (dev, goodix_tls_ready_image_handler, cb_info); } +void +goodix_tls_read_image_gain (FpDevice *dev, guint8 flags, guint8 gain, + GoodixImageCallback callback, gpointer user_data) +{ + g_assert (callback); + GoodixCallbackInfo *cb_info = malloc (sizeof (GoodixCallbackInfo)); + + cb_info->callback = G_CALLBACK (callback); + cb_info->user_data = user_data; + + goodix_send_mcu_get_image_gain (dev, flags, gain, + goodix_tls_ready_image_handler, cb_info); +} + // ---- TLS SECTION END ---- static void diff --git a/libfprint/drivers/goodixtls/goodix.h b/libfprint/drivers/goodixtls/goodix.h index 08bc254c9..2a3d7be2b 100644 --- a/libfprint/drivers/goodixtls/goodix.h +++ b/libfprint/drivers/goodixtls/goodix.h @@ -274,6 +274,27 @@ void goodix_send_mcu_get_image (FpDevice *dev, GoodixImageCallback callback, gpointer user_data); +/** + * @brief Like goodix_send_mcu_get_image(), but for devices whose + * mcu_get_image request is a 4-byte (flags, 0x06, gain, 0x00) payload + * instead of a bare 1-byte flag. @flags distinguishes a no-finger + * calibration request from a live capture request on these devices (the + * exact values are device-specific; see the driver that calls this). + * Checkout goodix_tls_read_image_gain() if you want an image from the + * device -- same reasoning as goodix_send_mcu_get_image()'s doc comment. + * + * @param dev + * @param flags + * @param gain + * @param callback + * @param user_data + */ +void goodix_send_mcu_get_image_gain (FpDevice *dev, + guint8 flags, + guint8 gain, + GoodixImageCallback callback, + gpointer user_data); + /** * @brief Tell the device we want to wait for the user to present their finger * @@ -567,4 +588,21 @@ void goodix_tls_read_image (FpDevice *dev, GoodixImageCallback callback, gpointer user_data); +/** + * @brief Like goodix_tls_read_image(), but using + * goodix_send_mcu_get_image_gain() instead of goodix_send_mcu_get_image() + * to request the frame. + * + * @param dev + * @param flags + * @param gain + * @param callback Called when the image is decrypted + * @param user_data + */ +void goodix_tls_read_image_gain (FpDevice *dev, + guint8 flags, + guint8 gain, + GoodixImageCallback callback, + gpointer user_data); + // ---- TLS SECTION END ---- diff --git a/libfprint/drivers/goodixtls/goodix5xx.c b/libfprint/drivers/goodixtls/goodix5xx.c index a75d28b5e..9cbedb75d 100644 --- a/libfprint/drivers/goodixtls/goodix5xx.c +++ b/libfprint/drivers/goodixtls/goodix5xx.c @@ -82,6 +82,25 @@ static void on_calibrate_scan(FpDevice* dev, guint8* data, guint16 len, gpointer fpi_ssm_next_state(ssm); } +/* mcu_get_image request flags for a no-finger calibration frame vs. a + * live/finger-present frame, for devices with use_gain_image_request set + * (see goodix5xx.h's doc comment on that field). Device-specific, not a + * generic protocol constant -- currently only meaningful for 533c. */ +#define GOODIX_IMAGE_FLAGS_CALIBRATE 0x01 +#define GOODIX_IMAGE_FLAGS_SCAN 0x41 + +static void +read_image (FpDevice *dev, guint8 flags, GoodixImageCallback callback, + gpointer user_data) +{ + FpiDeviceGoodixTls5xxClass *cls = FPI_DEVICE_GOODIXTLS5XX_GET_CLASS (dev); + + if (cls->use_gain_image_request) + goodix_tls_read_image_gain (dev, flags, cls->image_gain, callback, user_data); + else + goodix_tls_read_image (dev, callback, user_data); +} + static void calibrate_run(FpiSsm* ssm, FpDevice* dev) { switch (fpi_ssm_get_cur_state(ssm)) { case CALIBRATION_STAGE_FDT_UP: @@ -91,7 +110,7 @@ static void calibrate_run(FpiSsm* ssm, FpDevice* dev) { goodix_send_nav_0(dev, goodixtls5xx_check_none_cmd, ssm); break; case CALIBRATION_STAGE_GET_IMG: - goodix_tls_read_image(dev, on_calibrate_scan, ssm); + read_image (dev, GOODIX_IMAGE_FLAGS_CALIBRATE, on_calibrate_scan, ssm); } } @@ -360,7 +379,7 @@ query_mcu_state_cb (FpDevice * dev, guchar * mcu_state, guint16 len, static void scan_get_img (FpDevice * dev, FpiSsm * ssm) { - goodix_tls_read_image (dev, scan_on_read_img, ssm); + read_image (dev, GOODIX_IMAGE_FLAGS_SCAN, scan_on_read_img, ssm); } diff --git a/libfprint/drivers/goodixtls/goodix5xx.h b/libfprint/drivers/goodixtls/goodix5xx.h index 855996e07..9a37fc0c7 100644 --- a/libfprint/drivers/goodixtls/goodix5xx.h +++ b/libfprint/drivers/goodixtls/goodix5xx.h @@ -83,6 +83,17 @@ struct _FpiDeviceGoodixTls5xxClass const guint8 * psk; int reset_number; ///< only needed if goodixtls5xx_check_reset() is used + + /// Some devices (e.g. 533c) need a 4-byte mcu_get_image request + /// (flags, 0x06, gain, 0x00) rather than the 1-byte request the rest of + /// this family uses -- set TRUE to opt in. When TRUE, image_gain is the + /// gain byte used for both the calibration and the live capture request + /// (this device family flat-fields the live frame against a calibration + /// frame captured at the same gain, so a single fixed gain is used for + /// both). Defaults to FALSE / 0, which reproduces this class's exact + /// prior behavior for drivers that do not set these. + gboolean use_gain_image_request; + guint8 image_gain; }; /** From f6904c4420f6133b6859e56d10bd562acea9968c Mon Sep 17 00:00:00 2001 From: daemonhorn Date: Sat, 22 Aug 2026 21:27:28 -0400 Subject: [PATCH 02/12] Add goodix533c driver: open() + one-frame capture, verified on hardware New FpDevice-rooted driver (not FpImageDevice, not FpiDeviceGoodixTls) for 27c6:533c, living entirely under libfprint/drivers/goodix533c/. goodix5xx.c's shared FDT state machine assumes a static per-device config blob, but 533c's finger-detect baseline is read fresh from the device every session -- a structural mismatch documented in findings/native-driver-architecture.md -- so this driver bypasses goodix5xx.c and goodix.c entirely and talks to the device directly, reusing only the device-agnostic wire codec (goodix_proto.c/.h) and embedded TLS-PSK server (goodixtls.c/.h) from the sibling goodixtls/ driver, unmodified. The command layer, TLS handshake pump, and FDT/capture sequence are ported from goodix.c's logic and cross-checked stage for stage against vendor/goodix-fp-dump-nikicat/driver_53xc.py, which is authoritative for this exact silicon. Two real protocol shapes in goodix.c disagree with the Python driver and were NOT carried over: preset_psk_read's payload/reply shape (goodix.c's GoodixPresetPsk struct omits the offset field and reply parsing differs), and mcu_switch_to_fdt_down/up prepending an undocumented control byte that driver_53xc.py's wire format does not have. mcu_get_image's TLS_DATA reply (pack flags 0xb2) also isn't a case goodix.c's receive dispatcher recognizes at all; this driver adds it. Scope is deliberately narrow: open() (USB claim, read loop, nop, firmware regex check, PSK-hash verification) plus a single no-finger reference frame capture, decoded and squashed to 8-bit, exposed only through a test-only entry point (fpi_device_goodix533c_capture_test) since no enroll/verify/identify vfuncs exist yet. No flat-fielding against a second frame -- the goal here is proving the USB/TLS/protocol chain end-to-end, not ridge visibility, which needs a real finger. Verified against real 27c6:533c hardware via the new goodix533c-capture-test harness (libfprint/drivers/goodix533c/capture_test.c, built only when 'goodix533c' is in the enabled driver list): open() succeeds, TLS-PSK handshake completes, config uploads, FDT baseline reads 12 samples, and the captured frame decodes to a sane, non-degenerate 108x88 raw pixel range of roughly [0, 4060] out of a 12-bit sensor range, written out as a standard (non-transposed) binary PGM. Two bugs were found and fixed against real hardware during this pass: - preset_psk_read's reply carries the PSK's SHA-256 hash directly at offset 9; re-hashing it before comparing (as an initial draft did) can never match. - write_sensor_register and nop are ACK-only on this device (no second data reply); requesting one hangs until the command timeout. goodix.c's 10ms timeout on tls_successfully_established, which its own comment already flags as suspect ("always times out for some reason"), was replaced with the same generic timeout used elsewhere. Build wiring mirrors goodixtls511 exactly: a driver_sources entry, a default_drivers entry, and a driver_helper_mapping entry pointing at the existing 'goodixtls' helper bundle, which already pulls in goodix_proto.c/goodixtls.c (plus goodix.c/goodix5xx.c, compiled but unused by this driver) and the openssl/threads dependencies. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y9XXWG7y2kfBEXJj3MS5yv --- libfprint/drivers/goodix533c/capture_test.c | 166 +++ libfprint/drivers/goodix533c/goodix533c.c | 1430 +++++++++++++++++++ libfprint/drivers/goodix533c/goodix533c.h | 81 ++ libfprint/meson.build | 10 + meson.build | 2 + 5 files changed, 1689 insertions(+) create mode 100644 libfprint/drivers/goodix533c/capture_test.c create mode 100644 libfprint/drivers/goodix533c/goodix533c.c create mode 100644 libfprint/drivers/goodix533c/goodix533c.h diff --git a/libfprint/drivers/goodix533c/capture_test.c b/libfprint/drivers/goodix533c/capture_test.c new file mode 100644 index 000000000..7e29add5d --- /dev/null +++ b/libfprint/drivers/goodix533c/capture_test.c @@ -0,0 +1,166 @@ +/* + * Standalone hardware test harness for the goodix533c driver. + * + * Not part of libfprint's public API or installed targets -- it reaches + * straight into the driver's test-only entry point + * (fpi_device_goodix533c_capture_test) because there is no enroll/verify + * vfunc wired up yet (out of scope for this task). Builds only when + * 'goodix533c' is in the enabled driver list (see libfprint/meson.build). + * + * Usage: goodix533c-capture-test [output.pgm] + */ + +#include +#include + +#include + +#include "fp-context.h" +#include "fp-device.h" + +#include "drivers/goodix533c/goodix533c.h" + +typedef struct +{ + GMainLoop *loop; + const char *output_path; + int exit_code; +} TestState; + +static gboolean +write_pgm (const char *path, const guint8 *pixels, int width, int height) +{ + FILE *f = fopen (path, "wb"); + size_t n; + + if (!f) + { + g_print ("Failed to open %s for writing: %s\n", path, g_strerror (errno)); + return FALSE; + } + + fprintf (f, "P5\n%d %d\n255\n", width, height); + n = fwrite (pixels, 1, (size_t) (width * height), f); + fclose (f); + + return n == (size_t) (width * height); +} + +static void +on_closed (FpDevice *dev, GAsyncResult *res, TestState *ts) +{ + g_autoptr(GError) error = NULL; + + fp_device_close_finish (dev, res, &error); + if (error) + g_print ("close() error: %s\n", error->message); + else + g_print ("close() OK\n"); + + g_main_loop_quit (ts->loop); +} + +static void +on_capture_done (FpDevice *dev, const guint16 *raw_pixels, + const guint8 *squashed, gpointer user_data, GError *error) +{ + TestState *ts = user_data; + int count = GOODIX533C_SENSOR_WIDTH * GOODIX533C_SENSOR_HEIGHT; + int i; + guint16 raw_min = 0xffff; + guint16 raw_max = 0; + + if (error) + { + g_print ("Capture FAILED: %s\n", error->message); + ts->exit_code = 1; + fp_device_close (dev, NULL, (GAsyncReadyCallback) on_closed, ts); + return; + } + + for (i = 0; i < count; i++) + { + if (raw_pixels[i] < raw_min) + raw_min = raw_pixels[i]; + if (raw_pixels[i] > raw_max) + raw_max = raw_pixels[i]; + } + + g_print ("Captured frame: %dx%d, raw pixel range [%u, %u]\n", + GOODIX533C_SENSOR_WIDTH, GOODIX533C_SENSOR_HEIGHT, raw_min, + raw_max); + + if (write_pgm (ts->output_path, squashed, GOODIX533C_SENSOR_WIDTH, + GOODIX533C_SENSOR_HEIGHT)) + g_print ("Wrote %s\n", ts->output_path); + else + { + g_print ("Failed to write %s\n", ts->output_path); + ts->exit_code = 1; + } + + fp_device_close (dev, NULL, (GAsyncReadyCallback) on_closed, ts); +} + +static void +on_opened (FpDevice *dev, GAsyncResult *res, TestState *ts) +{ + g_autoptr(GError) error = NULL; + + if (!fp_device_open_finish (dev, res, &error)) + { + g_print ("open() FAILED: %s\n", error ? error->message : "(no error set)"); + ts->exit_code = 1; + g_main_loop_quit (ts->loop); + return; + } + + g_print ("open() SUCCEEDED\n"); + fpi_device_goodix533c_capture_test (dev, on_capture_done, ts); +} + +int +main (int argc, char **argv) +{ + g_autoptr(FpContext) ctx = NULL; + GPtrArray *devices; + FpDevice *dev = NULL; + TestState ts = { 0 }; + guint i; + + ts.output_path = argc > 1 ? argv[1] : "goodix533c-capture.pgm"; + + ctx = fp_context_new (); + devices = fp_context_get_devices (ctx); + + if (!devices || devices->len == 0) + { + g_print ("No fingerprint devices found at all.\n"); + return 1; + } + + for (i = 0; i < devices->len; ++i) + { + FpDevice *d = g_ptr_array_index (devices, i); + + g_print ("Found: %s (%s) - driver %s\n", + fp_device_get_device_id (d), fp_device_get_name (d), + fp_device_get_driver (d)); + if (g_strcmp0 (fp_device_get_driver (d), "goodix533c") == 0) + dev = d; + } + + if (!dev) + { + g_print ("No goodix533c device found among the above.\n"); + return 1; + } + + ts.loop = g_main_loop_new (NULL, FALSE); + g_print ("Opening %s ...\n", fp_device_get_device_id (dev)); + fp_device_open (dev, NULL, (GAsyncReadyCallback) on_opened, &ts); + g_main_loop_run (ts.loop); + g_main_loop_unref (ts.loop); + + return ts.exit_code; +} diff --git a/libfprint/drivers/goodix533c/goodix533c.c b/libfprint/drivers/goodix533c/goodix533c.c new file mode 100644 index 000000000..3f7429c07 --- /dev/null +++ b/libfprint/drivers/goodix533c/goodix533c.c @@ -0,0 +1,1430 @@ +/* + * Goodix 27c6:533c native driver for libfprint + * + * Copyright (C) 2026 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define FP_COMPONENT "goodix533c" + +#include + +#include + +#include "drivers_api.h" +#include "fpi-ssm.h" +#include "fpi-usb-transfer.h" + +#include "../goodixtls/goodix_proto.h" +#include "../goodixtls/goodixtls.h" + +#include "goodix533c.h" + +/* ---- device-level constants (all hardware-verified, see + * findings/native-driver-architecture.md) ---- */ + +#define GOODIX533C_USB_INTERFACE (0) +#define GOODIX533C_EP_IN (0x83) +#define GOODIX533C_EP_OUT (0x01) + +#define GOODIX533C_TIMEOUT_MS (1000) + +/* mcu_get_image reply pack flags: encrypted TLS application data, distinct + * from GOODIX_FLAGS_TLS (raw handshake bytes). Not in goodix_proto.h -- + * that header only knows about 0xa0/0xb0. Matches goodix.py's + * FLAGS_TRANSPORT_LAYER_SECURITY_DATA. */ +#define GOODIX533C_FLAGS_TLS_DATA (0xb2) + +/* Number of bytes preceding the raw TLS record inside a TLS_DATA pack's + * payload. Reverse-engineered value from driver_53xc.py's capture(): + * `frame[9:]` before decrypt_record(). Not a generic protocol constant -- + * device/firmware specific, taken as-is from the proven Python driver. */ +#define GOODIX533C_IMAGE_REPLY_HEADER_LEN (9) + +#define GOODIX533C_IMAGE_FLAGS_CALIBRATE (0x01) +#define GOODIX533C_IMAGE_GAIN (0xc2) + +#define GOODIX533C_CAPTURE_REGISTER (0x022c) +static const guint8 capture_on[2] = { 0x0a, 0x03 }; +static const guint8 capture_off[2] = { 0x0a, 0x02 }; + +#define GOODIX533C_PSK_LENGTH (32) +#define GOODIX533C_PSK_FLAGS (0xbb020001) +/* sha256(bytes(32)) -- expected PSK hash, all-zero PSK per this whole + * device family's convention. See findings doc: this driver must never + * write a PSK, so we only ever compare. */ +#define GOODIX533C_PSK_SHA256 \ + "66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925" + +#define GOODIX533C_FIRMWARE_REGEX "^GF5288_GM168SEC_APP_1[0-9]{4}$" + +static const guint8 fdt_mode_idle[2] = { 0x0d, 0x01 }; + +/* Captured from the real vendor driver -- see findings doc. Not + * byte-identical to goodix53x5's default config. */ +static const guint8 device_config[256] = { + 0x40, 0x11, 0x6c, 0x7d, 0x28, 0xa5, 0x28, 0xcd, 0x1c, 0xe9, 0x10, 0xf9, + 0x00, 0xf9, 0x00, 0xf9, 0x00, 0x04, 0x02, 0x00, 0x00, 0x08, 0x00, 0x11, + 0x11, 0xba, 0x00, 0x01, 0x80, 0xca, 0x00, 0x07, 0x00, 0x84, 0x00, 0xbe, + 0xb2, 0x86, 0x00, 0xc5, 0xb9, 0x88, 0x00, 0xb5, 0xad, 0x8a, 0x00, 0x9d, + 0x95, 0x8c, 0x00, 0x00, 0xbe, 0x8e, 0x00, 0x00, 0xc5, 0x90, 0x00, 0x00, + 0xb5, 0x92, 0x00, 0x00, 0x9d, 0x94, 0x00, 0x00, 0xaf, 0x96, 0x00, 0x00, + 0xbf, 0x98, 0x00, 0x00, 0xb6, 0x9a, 0x00, 0x00, 0xa7, 0x30, 0x00, 0x6c, + 0x1c, 0x50, 0x00, 0x01, 0x05, 0xd0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, + 0x00, 0x72, 0x00, 0x78, 0x56, 0x74, 0x00, 0x34, 0x12, 0x26, 0x00, 0x00, + 0x12, 0x20, 0x00, 0x10, 0x40, 0x12, 0x00, 0x03, 0x04, 0x02, 0x02, 0x16, + 0x21, 0x2c, 0x02, 0x0a, 0x03, 0x2a, 0x01, 0x02, 0x00, 0x22, 0x00, 0x01, + 0x20, 0x24, 0x00, 0x32, 0x00, 0x80, 0x00, 0x05, 0x04, 0x5c, 0x00, 0x00, + 0x01, 0x56, 0x00, 0x28, 0x20, 0x58, 0x00, 0x01, 0x00, 0x32, 0x00, 0x24, + 0x02, 0x82, 0x00, 0x80, 0x0c, 0x20, 0x02, 0x88, 0x0d, 0x2a, 0x01, 0x92, + 0x07, 0x22, 0x00, 0x01, 0x20, 0x24, 0x00, 0x14, 0x00, 0x80, 0x00, 0x05, + 0x04, 0x5c, 0x00, 0x94, 0x00, 0x56, 0x00, 0x08, 0x20, 0x58, 0x00, 0x03, + 0x00, 0x32, 0x00, 0x08, 0x04, 0x82, 0x00, 0x80, 0x11, 0x20, 0x02, 0x28, + 0x0c, 0x2a, 0x01, 0x18, 0x04, 0x5c, 0x00, 0x94, 0x00, 0x54, 0x00, 0x00, + 0x01, 0x62, 0x00, 0x09, 0x03, 0x64, 0x00, 0x18, 0x00, 0x82, 0x00, 0x80, + 0x0c, 0x20, 0x02, 0x28, 0x0c, 0x2a, 0x01, 0x18, 0x04, 0x5c, 0x00, 0x94, + 0x00, 0x52, 0x00, 0x08, 0x00, 0x54, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x51, 0x13, +}; + +#define GOODIX533C_IMAGE_BYTES \ + (GOODIX533C_SENSOR_WIDTH * GOODIX533C_SENSOR_HEIGHT * 3 / 2) +#define GOODIX533C_IMAGE_PIXELS \ + (GOODIX533C_SENSOR_WIDTH * GOODIX533C_SENSOR_HEIGHT) + +/* ---- generic single-in-flight command callback shape, ported from + * goodix.c's GoodixCmdCallback ---- */ +typedef void (*Goodix533cCmdCallback)(FpDevice *dev, + guint8 *data, + guint16 length, + gpointer user_data, + GError *error); + +struct _FpiDeviceGoodix533c +{ + FpDevice parent_instance; + + GCancellable *transfer_cancel_tkn; + gboolean interface_claimed; + gboolean read_loop_started; + + /* reassembly buffer for the current incoming pack */ + guint8 *rx_buf; + guint32 rx_len; + + /* in-flight command state -- single command at a time, exactly like + * goodix.c's FpiDeviceGoodixTlsPrivate */ + guint8 cmd; + gboolean ack_pending; + gboolean reply_pending; + GSource *timeout_src; + Goodix533cCmdCallback callback; + gpointer user_data; + + /* embedded TLS-PSK server -- goodixtls.c, unmodified */ + GoodixTlsServer tls; + gboolean tls_active; + + /* per-session FDT baseline, read fresh every open per the findings doc; + * unused beyond this scope (no fdt_down/fdt_up arming here) but kept for + * fidelity to the golden capture sequence. */ + guint8 fdt_template[24]; + gboolean have_fdt_template; +}; + +G_DEFINE_TYPE (FpiDeviceGoodix533c, fpi_device_goodix533c, FP_TYPE_DEVICE) + +/* =========================================================================== + * Low level receive/dispatch, ported from goodix.c's + * goodix_receive_{data,data_cb,pack,protocol,ack,done} and + * goodix_start_read_loop / goodix_send_{data,pack,protocol}. + * ======================================================================= */ + +static void receive_data (FpDevice *dev); + +static void +deliver_reply (FpDevice *dev, guint8 *data, guint16 length, GError *error) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + Goodix533cCmdCallback callback = self->callback; + gpointer user_data = self->user_data; + + if (!(self->ack_pending || self->reply_pending)) + { + g_clear_error (&error); + return; + } + + if (self->timeout_src) + g_clear_pointer (&self->timeout_src, g_source_destroy); + self->ack_pending = FALSE; + self->reply_pending = FALSE; + self->callback = NULL; + self->user_data = NULL; + + if (callback) + callback (dev, data, length, user_data, error); + else + g_clear_error (&error); +} + +static void +handle_ack (FpDevice *dev, guint8 *payload, guint16 length) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + GoodixAck *ack = (GoodixAck *) payload; + + if (length != sizeof (GoodixAck)) + { + fp_warn ("Invalid ACK length: %d", length); + return; + } + + if (!ack->always_true) + { + fp_warn ("Invalid ACK flags: 0x%02x", payload[1]); + return; + } + + if (ack->has_no_config) + fp_warn ("MCU has no config"); + + if (self->cmd != ack->cmd) + { + fp_warn ("Invalid ACK command: 0x%02x (expected 0x%02x)", ack->cmd, + self->cmd); + return; + } + + if (!self->ack_pending) + { + fp_warn ("Didn't expect an ACK for command: 0x%02x", self->cmd); + return; + } + + if (!self->reply_pending) + { + deliver_reply (dev, NULL, 0, NULL); + return; + } + + self->ack_pending = FALSE; +} + +static void +handle_protocol_pack (FpDevice *dev, guint8 *payload, guint32 length) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + guint8 cmd; + g_autofree guint8 *inner = NULL; + guint16 inner_len; + gboolean valid_checksum, valid_null_checksum; + + if (!goodix_decode_protocol (payload, length, &cmd, &inner, &inner_len, + &valid_checksum, &valid_null_checksum)) + { + fp_warn ("Incomplete protocol message, size: %u", length); + return; + } + + if (cmd == GOODIX_CMD_ACK) + { + handle_ack (dev, inner, inner_len); + return; + } + + if (self->cmd != cmd) + { + fp_warn ("Unexpected protocol command: 0x%02x (expected 0x%02x)", cmd, + self->cmd); + return; + } + + if (!self->reply_pending) + { + fp_warn ("Didn't expect a reply for command: 0x%02x", self->cmd); + return; + } + + deliver_reply (dev, inner, inner_len, NULL); +} + +static void +receive_pack (FpDevice *dev, guint8 *data, guint32 length) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + guint8 flags; + g_autofree guint8 *payload = NULL; + guint16 payload_len; + gboolean valid_checksum; + + self->rx_buf = g_realloc (self->rx_buf, self->rx_len + length); + memcpy (self->rx_buf + self->rx_len, data, length); + self->rx_len += length; + + if (!goodix_decode_pack (self->rx_buf, self->rx_len, &flags, &payload, + &payload_len, &valid_checksum)) + { + /* Not a full pack yet -- wait for more data. */ + return; + } + + switch (flags) + { + case GOODIX_FLAGS_MSG_PROTOCOL: + handle_protocol_pack (dev, payload, payload_len); + break; + + case GOODIX_FLAGS_TLS: + case GOODIX533C_FLAGS_TLS_DATA: + /* Raw payload, delivered unconditionally to whatever command is + * currently expecting a reply -- matches goodix.c's handling of + * GOODIX_FLAGS_TLS packs (used both for handshake bytes and, here, + * for TLS_DATA-flagged mcu_get_image replies). */ + deliver_reply (dev, payload, payload_len, NULL); + break; + + default: + fp_warn ("Unknown pack flags: 0x%02x", flags); + break; + } + + g_clear_pointer (&self->rx_buf, g_free); + self->rx_len = 0; +} + +static void +receive_data_cb (FpiUsbTransfer *transfer, FpDevice *dev, + gpointer user_data, GError *error) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + + if (g_cancellable_is_cancelled (self->transfer_cancel_tkn)) + return; + + if (error) + { + fp_warn ("Receive data error: %s", error->message); + g_error_free (error); + receive_data (dev); + return; + } + + receive_pack (dev, transfer->buffer, (guint32) transfer->actual_length); + receive_data (dev); +} + +static void +receive_data (FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + FpiUsbTransfer *transfer = fpi_usb_transfer_new (dev); + + transfer->short_is_error = FALSE; + fpi_usb_transfer_fill_bulk (transfer, GOODIX533C_EP_IN, + GOODIX_EP_IN_MAX_BUF_SIZE); + fpi_usb_transfer_submit (transfer, 0, self->transfer_cancel_tkn, + receive_data_cb, NULL); +} + +static void +start_read_loop (FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + + if (self->read_loop_started) + return; + + self->read_loop_started = TRUE; + if (g_cancellable_is_cancelled (self->transfer_cancel_tkn)) + g_cancellable_reset (self->transfer_cancel_tkn); + + receive_data (dev); +} + +static gboolean +send_data (FpDevice *dev, guint8 *data, guint32 length, + GDestroyNotify free_func, GError **error) +{ + for (guint32 i = 0; i < length; i += GOODIX_EP_OUT_MAX_BUF_SIZE) + { + FpiUsbTransfer *transfer = fpi_usb_transfer_new (dev); + + transfer->short_is_error = TRUE; + fpi_usb_transfer_fill_bulk_full (transfer, GOODIX533C_EP_OUT, data + i, + GOODIX_EP_OUT_MAX_BUF_SIZE, NULL); + + if (!fpi_usb_transfer_submit_sync (transfer, GOODIX533C_TIMEOUT_MS, + error)) + { + if (free_func) + free_func (data); + fpi_usb_transfer_unref (transfer); + return FALSE; + } + fpi_usb_transfer_unref (transfer); + } + + if (free_func) + free_func (data); + return TRUE; +} + +static gboolean +send_pack (FpDevice *dev, guint8 flags, guint8 *payload, guint16 length, + GDestroyNotify free_func, GError **error) +{ + guint8 *data; + guint32 data_len; + + goodix_encode_pack (flags, payload, length, TRUE, &data, &data_len); + if (free_func) + free_func (payload); + + return send_data (dev, data, data_len, g_free, error); +} + +static void +on_command_timeout (FpDevice *dev, gpointer user_data) +{ + GError *error = NULL; + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT, + "Command timed out: 0x%02x", self->cmd); + deliver_reply (dev, NULL, 0, error); +} + +static void +send_protocol (FpDevice *dev, guint8 cmd, const guint8 *payload, + guint16 length, gboolean calc_checksum, guint timeout_ms, + gboolean expect_ack, gboolean expect_reply, + Goodix533cCmdCallback callback, gpointer user_data) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + GError *error = NULL; + guint8 *data; + guint32 data_len; + + if (self->ack_pending || self->reply_pending) + { + fp_warn ("A command is already running: 0x%02x", self->cmd); + return; + } + + fp_dbg ("Running command: 0x%02x", cmd); + + if (timeout_ms) + self->timeout_src = fpi_device_add_timeout (dev, timeout_ms, + on_command_timeout, NULL, + NULL); + self->cmd = cmd; + self->ack_pending = expect_ack; + self->reply_pending = expect_reply; + self->callback = callback; + self->user_data = user_data; + + goodix_encode_protocol (cmd, payload, length, calc_checksum, FALSE, &data, + &data_len); + + if (!send_pack (dev, GOODIX_FLAGS_MSG_PROTOCOL, data, data_len, g_free, + &error)) + { + deliver_reply (dev, NULL, 0, error); + return; + } +} + +/* =========================================================================== + * Specific commands actually needed by the capture sequence in + * capture_golden_session.py. Every payload shape below is taken directly + * from driver_53xc.py / goodix.py, not from goodix.c (goodix.c's + * preset_psk_read and fdt_down/fdt_up payload shapes disagree with the + * Python driver -- see the discrepancies noted in the final report). + * ======================================================================= */ + +static void +cmd_nop (FpDevice *dev, Goodix533cCmdCallback callback, gpointer user_data) +{ + guint8 payload[4] = { 0x00, 0x00, 0x00, 0x00 }; + + /* Ack-only, no data reply -- matches goodix.py's nop(), which only ever + * calls _expect_ack(). Some sensors don't answer NOP at all, which the + * caller is expected to tolerate as a timeout, not an error. */ + send_protocol (dev, GOODIX_CMD_NOP, payload, sizeof (payload), FALSE, + GOODIX533C_TIMEOUT_MS, TRUE, FALSE, callback, user_data); +} + +static void +cmd_firmware_version (FpDevice *dev, Goodix533cCmdCallback callback, + gpointer user_data) +{ + guint8 payload[2] = { 0x00, 0x00 }; + + send_protocol (dev, GOODIX_CMD_FIRMWARE_VERSION, payload, sizeof (payload), + TRUE, GOODIX533C_TIMEOUT_MS, TRUE, TRUE, callback, + user_data); +} + +static void +cmd_preset_psk_read (FpDevice *dev, guint32 flags, guint32 length, + guint32 offset, Goodix533cCmdCallback callback, + gpointer user_data) +{ + guint8 payload[16]; + + *(guint32 *) (payload + 0) = GUINT32_TO_LE (length); + *(guint32 *) (payload + 4) = GUINT32_TO_LE (offset); + *(guint32 *) (payload + 8) = GUINT32_TO_LE (flags); + *(guint32 *) (payload + 12) = GUINT32_TO_LE (0); + + send_protocol (dev, GOODIX_CMD_PRESET_PSK_READ, payload, sizeof (payload), + TRUE, GOODIX533C_TIMEOUT_MS, TRUE, TRUE, callback, + user_data); +} + +static void +cmd_reset (FpDevice *dev, gboolean reset_sensor, gboolean soft_reset_mcu, + guint8 sleep_time, Goodix533cCmdCallback callback, + gpointer user_data) +{ + guint8 payload[2]; + + payload[0] = (reset_sensor ? 0x1 : 0x0) | (soft_reset_mcu ? 0x1 : 0x0) << 1 | + (reset_sensor ? 0x1 : 0x0) << 2; + payload[1] = sleep_time; + + send_protocol (dev, GOODIX_CMD_RESET, payload, sizeof (payload), TRUE, + GOODIX533C_TIMEOUT_MS, TRUE, TRUE, callback, user_data); +} + +static void +cmd_read_sensor_register (FpDevice *dev, guint16 address, guint8 length, + Goodix533cCmdCallback callback, gpointer user_data) +{ + guint8 payload[4]; + + payload[0] = 0x00; + *(guint16 *) (payload + 1) = GUINT16_TO_LE (address); + payload[3] = length; + + send_protocol (dev, GOODIX_CMD_READ_SENSOR_REGISTER, payload, + sizeof (payload), TRUE, GOODIX533C_TIMEOUT_MS, TRUE, TRUE, + callback, user_data); +} + +static void +cmd_write_sensor_register (FpDevice *dev, guint16 address, + const guint8 value[2], + Goodix533cCmdCallback callback, gpointer user_data) +{ + guint8 payload[5]; + + payload[0] = 0x00; + *(guint16 *) (payload + 1) = GUINT16_TO_LE (address); + payload[3] = value[0]; + payload[4] = value[1]; + + /* Ack-only, no data reply -- matches goodix.py's write_sensor_register(), + * which only ever calls _expect_ack(). */ + send_protocol (dev, GOODIX_CMD_WRITE_SENSOR_REGISTER, payload, + sizeof (payload), TRUE, GOODIX533C_TIMEOUT_MS, TRUE, FALSE, + callback, user_data); +} + +static void +cmd_read_otp (FpDevice *dev, Goodix533cCmdCallback callback, + gpointer user_data) +{ + guint8 payload[2] = { 0x00, 0x00 }; + + send_protocol (dev, GOODIX_CMD_READ_OTP, payload, sizeof (payload), TRUE, + GOODIX533C_TIMEOUT_MS, TRUE, TRUE, callback, user_data); +} + +static void +cmd_upload_config_mcu (FpDevice *dev, const guint8 *config, guint16 length, + Goodix533cCmdCallback callback, gpointer user_data) +{ + send_protocol (dev, GOODIX_CMD_UPLOAD_CONFIG_MCU, config, length, TRUE, + GOODIX533C_TIMEOUT_MS, TRUE, TRUE, callback, user_data); +} + +static void +cmd_mcu_switch_to_fdt_mode (FpDevice *dev, const guint8 *mode, guint16 length, + gboolean expect_reply, + Goodix533cCmdCallback callback, + gpointer user_data) +{ + send_protocol (dev, GOODIX_CMD_MCU_SWITCH_TO_FDT_MODE, mode, length, TRUE, + GOODIX533C_TIMEOUT_MS, TRUE, expect_reply, callback, + user_data); +} + +static void +cmd_mcu_get_image_gain (FpDevice *dev, guint8 flags, guint8 gain, + Goodix533cCmdCallback callback, gpointer user_data) +{ + guint8 payload[4] = { flags, 0x06, gain, 0x00 }; + + send_protocol (dev, GOODIX_CMD_MCU_GET_IMAGE, payload, sizeof (payload), + TRUE, GOODIX533C_TIMEOUT_MS, TRUE, TRUE, callback, + user_data); +} + +static void +cmd_request_tls_connection (FpDevice *dev, Goodix533cCmdCallback callback, + gpointer user_data) +{ + guint8 payload[2] = { 0x00, 0x00 }; + + /* No timeout, matching goodix.c: the handshake round trip through the + * embedded TLS server can legitimately take a little while. */ + send_protocol (dev, GOODIX_CMD_REQUEST_TLS_CONNECTION, payload, + sizeof (payload), TRUE, 0, TRUE, TRUE, callback, user_data); +} + +static void +cmd_tls_successfully_established (FpDevice *dev, + Goodix533cCmdCallback callback, + gpointer user_data) +{ + guint8 payload[2] = { 0x00, 0x00 }; + + /* goodix.c uses a 10ms timeout here and notes in a comment that it + * "always times out for some reason" on real hardware -- driver_53xc.py's + * _expect_ack() has no special-cased timeout for this command, so use the + * same generic one as everything else instead of that known-bad value. */ + send_protocol (dev, GOODIX_CMD_TLS_SUCCESSFULLY_ESTABLISHED, payload, + sizeof (payload), TRUE, GOODIX533C_TIMEOUT_MS, TRUE, FALSE, + callback, user_data); +} + +/* =========================================================================== + * TLS handshake pump -- ported near-verbatim from goodix.c's + * on_goodix_tls_read_handshake / tls_handshake_run / tls_handshake_done / + * do_tls_handshake / on_goodix_request_tls_connection, cross-checked stage + * for stage against driver_53xc.py's establish_tls(). Only the private + * struct access changed. + * ======================================================================= */ + +enum tls_handshake_stage { + TLS_STAGE_HELLO_S, + TLS_STAGE_KH_EXCHANGE, + TLS_STAGE_CHANGE_CIPHER_C, + TLS_STAGE_HANDSHAKE_C, + TLS_STAGE_CHANGE_CIPHER_S, + TLS_STAGE_NUM, +}; + +typedef struct +{ + Goodix533cCmdCallback callback; + gpointer user_data; +} TlsReadyData; + +static void +on_tls_raw_read (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + int sent; + + if (error) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + + sent = goodix_tls_client_write (&self->tls, data, length); + if (sent < 0) + { + fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, sent, + "failed to write to tls server")); + return; + } + fpi_ssm_next_state (ssm); +} + +static void +await_raw_pack (FpDevice *dev, Goodix533cCmdCallback callback, + gpointer user_data) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + + self->callback = callback; + self->user_data = user_data; + self->reply_pending = TRUE; + self->ack_pending = FALSE; + self->cmd = GOODIX_CMD_ACK; /* never matched directly; TLS packs bypass + * cmd matching entirely in receive_pack(). */ +} + +static void +tls_handshake_run (FpiSsm *ssm, FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + int stage = fpi_ssm_get_cur_state (ssm); + guint8 buff[2048]; + int size; + GError *error = NULL; + + switch (stage) + { + case TLS_STAGE_HELLO_S: + size = goodix_tls_client_read (&self->tls, buff, sizeof (buff)); + if (size < 0) + { + fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, size, + "failed to read tls server hello")); + return; + } + if (!send_pack (dev, GOODIX_FLAGS_TLS, buff, (guint16) size, NULL, + &error)) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + fpi_ssm_next_state (ssm); + break; + + case TLS_STAGE_KH_EXCHANGE: + case TLS_STAGE_CHANGE_CIPHER_C: + case TLS_STAGE_HANDSHAKE_C: + await_raw_pack (dev, on_tls_raw_read, ssm); + break; + + case TLS_STAGE_CHANGE_CIPHER_S: + size = goodix_tls_client_read (&self->tls, buff, sizeof (buff)); + if (size < 0) + { + fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, size, + "failed to read final server handshake")); + return; + } + if (!send_pack (dev, GOODIX_FLAGS_TLS, buff, (guint16) size, NULL, + &error)) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + fpi_ssm_next_state (ssm); + break; + + default: + g_assert_not_reached (); + } +} + +static void +on_tls_successfully_established (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + TlsReadyData *ready = user_data; + + ready->callback (dev, NULL, 0, ready->user_data, error); + g_free (ready); +} + +static void +tls_handshake_done (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + TlsReadyData *ready = fpi_ssm_get_data (ssm); + + if (error) + { + fp_warn ("TLS handshake failed: %s", error->message); + ready->callback (dev, NULL, 0, ready->user_data, error); + g_free (ready); + return; + } + + cmd_tls_successfully_established (dev, on_tls_successfully_established, + ready); +} + +static void +on_request_tls_connection_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + TlsReadyData *ready = user_data; + FpiSsm *ssm; + + if (error) + { + ready->callback (dev, NULL, 0, ready->user_data, error); + g_free (ready); + return; + } + + /* `data` is the device's raw ClientHello -- feed it into our embedded + * TLS server's client side, then pump the handshake. */ + goodix_tls_client_write (&self->tls, data, length); + + ssm = fpi_ssm_new (dev, tls_handshake_run, TLS_STAGE_NUM); + fpi_ssm_set_data (ssm, ready, NULL); + fpi_ssm_start (ssm, tls_handshake_done); +} + +/** + * tls_connect: full TLS bring-up -- init the embedded TLS-PSK server, + * request the connection from the device, pump the handshake, and tell the + * device TLS is established. Ported from goodix.c's goodix_tls_init() + + * goodix_tls_ready() + on_goodix_request_tls_connection(). + */ +static void +tls_connect (FpDevice *dev, Goodix533cCmdCallback callback, + gpointer user_data) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + TlsReadyData *ready; + GError *error = NULL; + + g_assert (!self->tls_active); + + if (!goodix_tls_server_init (&self->tls, &error)) + { + callback (dev, NULL, 0, user_data, error); + return; + } + self->tls_active = TRUE; + + ready = g_new0 (TlsReadyData, 1); + ready->callback = callback; + ready->user_data = user_data; + + cmd_request_tls_connection (dev, on_request_tls_connection_reply, ready); +} + +/* =========================================================================== + * Image decode -- ported math from goodixtls5xx_decode_frame() / + * goodixtls5xx_squash_frame_linear() in goodix5xx.c. Unlike goodix5xx.c's + * version (which skips an 8-byte header specific to that decrypted payload + * shape), driver_53xc.py's decode_image() operates on the decrypted + * payload starting at byte 0 with no header -- the Python file is + * authoritative for 533c, so no header skip here. + * ======================================================================= */ + +static void +decode_frame (guint16 *pixels, const guint8 *raw, guint32 raw_len) +{ + guint16 *pix = pixels; + guint32 i; + + for (i = 0; i + 6 <= raw_len; i += 6) + { + const guint8 *chunk = raw + i; + + *pix++ = (guint16) (((chunk[0] & 0xf) << 8) + chunk[1]); + *pix++ = (guint16) ((chunk[3] << 4) + (chunk[0] >> 4)); + *pix++ = (guint16) (((chunk[5] & 0xf) << 8) + chunk[2]); + *pix++ = (guint16) ((chunk[4] << 4) + (chunk[5] >> 4)); + } +} + +static void +squash_frame_linear (const guint16 *frame, guint8 *squashed, guint32 count) +{ + guint16 min = 0xffff; + guint16 max = 0; + guint32 i; + + for (i = 0; i < count; i++) + { + if (frame[i] < min) + min = frame[i]; + if (frame[i] > max) + max = frame[i]; + } + + for (i = 0; i < count; i++) + { + if (max == min) + squashed[i] = 0; + else + squashed[i] = (guint8) ((frame[i] - min) * 0xff / (max - min)); + } +} + +/* =========================================================================== + * Capture-test sequence -- new code, following capture_golden_session.py / + * driver_53xc.py's run_driver() exactly, simplified per this task's scope: + * a single no-finger reference frame is decoded and handed back as-is (no + * flat-fielding against a second frame, no finger-detect wait). See the + * driver header and the final report for why. + * ======================================================================= */ + +enum capture_stage { + CAPTURE_STAGE_RESET, + CAPTURE_STAGE_READ_CHIP_ID, + CAPTURE_STAGE_READ_OTP, + CAPTURE_STAGE_TLS, + CAPTURE_STAGE_UPLOAD_CONFIG, + CAPTURE_STAGE_FDT_BASELINE, + CAPTURE_STAGE_CAPTURE_ON, + CAPTURE_STAGE_GET_IMAGE, + CAPTURE_STAGE_CAPTURE_OFF, + CAPTURE_STAGE_NUM, +}; + +typedef struct +{ + Goodix533cCaptureDoneFunc callback; + gpointer user_data; + + guint16 *raw_pixels; + guint8 *squashed; +} CaptureData; + +static void +capture_data_free (CaptureData *data) +{ + g_free (data->raw_pixels); + g_free (data->squashed); + g_free (data); +} + +static void +on_capture_step_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + + if (error) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + fpi_ssm_next_state (ssm); +} + +static void +on_reset_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + + if (error) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + + if (length < 1 || data[0] != 0x01) + { + fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, + G_IO_ERROR_FAILED, + "reset failed (status=%d)", + length ? data[0] : -1)); + return; + } + + fp_dbg ("Reset OK"); + fpi_ssm_next_state (ssm); +} + +static void +on_tls_connected (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + + if (error) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + + fp_dbg ("TLS established"); + fpi_ssm_next_state (ssm); +} + +static void +on_upload_config_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + + if (error) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + + if (length < 1 || data[0] != 0x01) + { + fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, + "config upload rejected")); + return; + } + + fpi_ssm_next_state (ssm); +} + +static void +on_fdt_baseline_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + guint32 i; + guint32 sample_count; + + if (error) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + + /* Reply is a 4-byte header then 12-bit samples as 16-bit LE words. Vendor + * driver halves each sample and emits it twice as the FDT threshold + * template -- see fdt_template() in driver_53xc.py. Not consumed further + * in this capture-only build (no fdt_down/fdt_up arming in scope), kept + * only for fidelity to the golden sequence. */ + memset (self->fdt_template, 0, sizeof (self->fdt_template)); + sample_count = MIN ((guint32) (length > 4 ? (length - 4) / 2 : 0), 12u); + for (i = 0; i < sample_count; i++) + { + guint16 sample = (guint16) (data[4 + i * 2] | (data[4 + i * 2 + 1] << 8)); + self->fdt_template[i * 2] = self->fdt_template[i * 2 + 1] = + (guint8) (sample >> 1); + } + self->have_fdt_template = TRUE; + + fp_dbg ("FDT baseline measured (%u samples)", sample_count); + fpi_ssm_next_state (ssm); +} + +static void +on_get_image_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + CaptureData *cap = fpi_ssm_get_data (ssm); + guint8 decrypt_buf[65535]; + int decrypted; + GError *tls_error = NULL; + + if (error) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + + if (length <= GOODIX533C_IMAGE_REPLY_HEADER_LEN) + { + fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, + "image reply too short: %d", + length)); + return; + } + + /* Skip the pre-record header (see GOODIX533C_IMAGE_REPLY_HEADER_LEN's + * doc comment), feed the raw TLS record into the embedded server's + * client side, then read the decrypted plaintext back out. */ + goodix_tls_client_write (&self->tls, + data + GOODIX533C_IMAGE_REPLY_HEADER_LEN, + (guint16) (length - GOODIX533C_IMAGE_REPLY_HEADER_LEN)); + + decrypted = goodix_tls_server_read (&self->tls, decrypt_buf, + sizeof (decrypt_buf), &tls_error); + if (decrypted <= 0) + { + fpi_ssm_mark_failed (ssm, tls_error ? tls_error : + g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, + "TLS decrypt failed")); + return; + } + + if ((guint32) decrypted < GOODIX533C_IMAGE_BYTES) + { + fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, + "short decrypt: %d < %d", + decrypted, + GOODIX533C_IMAGE_BYTES)); + return; + } + + cap->raw_pixels = g_new0 (guint16, GOODIX533C_IMAGE_PIXELS); + cap->squashed = g_new0 (guint8, GOODIX533C_IMAGE_PIXELS); + decode_frame (cap->raw_pixels, decrypt_buf, GOODIX533C_IMAGE_BYTES); + squash_frame_linear (cap->raw_pixels, cap->squashed, + GOODIX533C_IMAGE_PIXELS); + + fp_dbg ("Decoded frame: %d bytes encrypted -> %d bytes plain -> %d pixels", + length, decrypted, GOODIX533C_IMAGE_PIXELS); + + fpi_ssm_next_state (ssm); +} + +static void +capture_run (FpiSsm *ssm, FpDevice *dev) +{ + switch (fpi_ssm_get_cur_state (ssm)) + { + case CAPTURE_STAGE_RESET: + cmd_reset (dev, TRUE, FALSE, 20, on_reset_reply, ssm); + break; + + case CAPTURE_STAGE_READ_CHIP_ID: + cmd_read_sensor_register (dev, 0x0000, 4, on_capture_step_reply, ssm); + break; + + case CAPTURE_STAGE_READ_OTP: + cmd_read_otp (dev, on_capture_step_reply, ssm); + break; + + case CAPTURE_STAGE_TLS: + tls_connect (dev, on_tls_connected, ssm); + break; + + case CAPTURE_STAGE_UPLOAD_CONFIG: + cmd_upload_config_mcu (dev, device_config, sizeof (device_config), + on_upload_config_reply, ssm); + break; + + case CAPTURE_STAGE_FDT_BASELINE: + { + guint8 mode[26]; + + memcpy (mode, fdt_mode_idle, sizeof (fdt_mode_idle)); + memset (mode + sizeof (fdt_mode_idle), 0, + sizeof (mode) - sizeof (fdt_mode_idle)); + cmd_mcu_switch_to_fdt_mode (dev, mode, sizeof (mode), TRUE, + on_fdt_baseline_reply, ssm); + } + break; + + case CAPTURE_STAGE_CAPTURE_ON: + cmd_write_sensor_register (dev, GOODIX533C_CAPTURE_REGISTER, + capture_on, on_capture_step_reply, ssm); + break; + + case CAPTURE_STAGE_GET_IMAGE: + cmd_mcu_get_image_gain (dev, GOODIX533C_IMAGE_FLAGS_CALIBRATE, + GOODIX533C_IMAGE_GAIN, on_get_image_reply, ssm); + break; + + case CAPTURE_STAGE_CAPTURE_OFF: + cmd_write_sensor_register (dev, GOODIX533C_CAPTURE_REGISTER, + capture_off, on_capture_step_reply, ssm); + break; + + default: + g_assert_not_reached (); + } +} + +static void +capture_done (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + CaptureData *cap = fpi_ssm_get_data (ssm); + + cap->callback (dev, cap->raw_pixels, cap->squashed, cap->user_data, error); +} + +void +fpi_device_goodix533c_capture_test (FpDevice *dev, + Goodix533cCaptureDoneFunc callback, + gpointer user_data) +{ + CaptureData *cap = g_new0 (CaptureData, 1); + FpiSsm *ssm; + + cap->callback = callback; + cap->user_data = user_data; + + ssm = fpi_ssm_new (dev, capture_run, CAPTURE_STAGE_NUM); + fpi_ssm_set_data (ssm, cap, (GDestroyNotify) capture_data_free); + fpi_ssm_start (ssm, capture_done); +} + +/* =========================================================================== + * open()/close() -- claims the interface, starts the read loop, then runs + * nop -> firmware_version -> preset_psk_read, mirroring driver_53xc.py's + * init_device(). Ported logic, new SSM (goodix.c has no equivalent + * standalone open sequence -- that's spread across goodix5xx.c's shared + * ACTIVATE state machine, which this driver deliberately does not use). + * ======================================================================= */ + +enum open_stage { + OPEN_STAGE_NOP, + OPEN_STAGE_FIRMWARE_VERSION, + OPEN_STAGE_PSK_READ, + OPEN_STAGE_NUM, +}; + +static void +on_open_nop_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + + /* Some sensors do not answer NOP at all -- goodix.c and driver_53xc.py + * both treat a NOP timeout as fine. Any other error is fatal. */ + if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + g_clear_error (&error); + fpi_ssm_next_state (ssm); +} + +static void +on_open_firmware_version_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + g_autofree gchar *firmware = NULL; + g_autoptr(GRegex) regex = NULL; + + if (error) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + + firmware = g_strndup ((const gchar *) data, length); + fp_info ("Firmware: %s", firmware); + + regex = g_regex_new (GOODIX533C_FIRMWARE_REGEX, 0, 0, NULL); + if (!g_regex_match (regex, firmware, 0, NULL)) + { + fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, + "Unsupported firmware: %s", + firmware)); + return; + } + + fpi_ssm_next_state (ssm); +} + +static void +on_open_psk_read_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + guint32 psk_length; + g_autofree gchar *hash = NULL; + + if (error) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + + if (length < 1 || data[0] != 0x00) + { + fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, + "preset_psk_read failed")); + return; + } + + if (length < 9) + { + fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, + "preset_psk_read reply too short")); + return; + } + + psk_length = GUINT32_FROM_LE (*(guint32 *) (data + 5)); + if (length < 9 + psk_length) + { + fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, + "preset_psk_read reply truncated")); + return; + } + + /* The device returns the SHA-256 hash of the PSK directly at offset 9 + * (driver_53xc.py's init_device() names this `psk_hash` and compares it + * as-is, with no extra hashing on our side -- PSK_LENGTH just happens to + * equal a SHA-256 digest length, 32 bytes, which is a red herring). */ + hash = g_malloc (psk_length * 2 + 1); + { + guint32 hi; + + for (hi = 0; hi < psk_length; hi++) + sprintf (hash + hi * 2, "%02x", (data + 9)[hi]); + hash[psk_length * 2] = '\0'; + } + if (g_strcmp0 (hash, GOODIX533C_PSK_SHA256) != 0) + { + fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, + "Sensor does not hold the " + "expected all-zero PSK; refusing " + "to provision one")); + return; + } + + fp_info ("PSK: all-zero, as expected"); + fpi_ssm_next_state (ssm); +} + +static void +open_run (FpiSsm *ssm, FpDevice *dev) +{ + switch (fpi_ssm_get_cur_state (ssm)) + { + case OPEN_STAGE_NOP: + cmd_nop (dev, on_open_nop_reply, ssm); + break; + + case OPEN_STAGE_FIRMWARE_VERSION: + cmd_firmware_version (dev, on_open_firmware_version_reply, ssm); + break; + + case OPEN_STAGE_PSK_READ: + cmd_preset_psk_read (dev, GOODIX533C_PSK_FLAGS, GOODIX533C_PSK_LENGTH, + 0, on_open_psk_read_reply, ssm); + break; + + default: + g_assert_not_reached (); + } +} + +static void +open_done (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + fpi_device_open_complete (dev, error); +} + +static void +goodix533c_open (FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + GError *error = NULL; + + /* Any leftover token from a previous open()/close() cycle is replaced + * here rather than in close() -- see the comment above the + * g_cancellable_cancel() call in goodix533c_close() for why it must stay + * alive (non-NULL) past close() itself. */ + g_clear_object (&self->transfer_cancel_tkn); + self->transfer_cancel_tkn = g_cancellable_new (); + + if (!g_usb_device_claim_interface (fpi_device_get_usb_device (dev), + GOODIX533C_USB_INTERFACE, 0, &error)) + { + fpi_device_open_complete (dev, error); + return; + } + self->interface_claimed = TRUE; + + start_read_loop (dev); + + fpi_ssm_start (fpi_ssm_new (dev, open_run, OPEN_STAGE_NUM), open_done); +} + +static void +goodix533c_close (FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + GError *error = NULL; + + /* Cancel, but deliberately do NOT clear/unref transfer_cancel_tkn here: + * the in-flight bulk IN transfer's completion callback + * (receive_data_cb()) can fire after this function returns (once the USB + * core actually tears the transfer down), and it identifies a + * post-close callback by checking g_cancellable_is_cancelled() on this + * same object. Nulling the pointer first would make that check silently + * pass a NULL cancellable (never "cancelled" per glib), so the stale + * callback would fall through to its error path and resubmit a new + * transfer on an already-closed device. open() replaces this token on + * the next open(); finalize() frees it for good. */ + if (self->transfer_cancel_tkn) + g_cancellable_cancel (self->transfer_cancel_tkn); + + if (self->tls_active) + { + goodix_tls_server_deinit (&self->tls, &error); + self->tls_active = FALSE; + g_clear_error (&error); + } + + if (self->timeout_src) + g_clear_pointer (&self->timeout_src, g_source_destroy); + g_clear_pointer (&self->rx_buf, g_free); + self->rx_len = 0; + self->ack_pending = FALSE; + self->reply_pending = FALSE; + self->callback = NULL; + self->user_data = NULL; + self->read_loop_started = FALSE; + + if (self->interface_claimed) + { + g_usb_device_release_interface (fpi_device_get_usb_device (dev), + GOODIX533C_USB_INTERFACE, 0, &error); + self->interface_claimed = FALSE; + } + + fpi_device_close_complete (dev, error); +} + +/* =========================================================================== + * GObject boilerplate + * ======================================================================= */ + +static void +fpi_device_goodix533c_init (FpiDeviceGoodix533c *self) +{ +} + +static void +fpi_device_goodix533c_finalize (GObject *object) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (object); + + g_clear_pointer (&self->rx_buf, g_free); + g_clear_object (&self->transfer_cancel_tkn); + + G_OBJECT_CLASS (fpi_device_goodix533c_parent_class)->finalize (object); +} + +static const FpIdEntry goodix533c_id_table[] = { + { .vid = 0x27c6, .pid = 0x533c, }, + { .vid = 0, .pid = 0, .driver_data = 0 }, +}; + +static void +fpi_device_goodix533c_class_init (FpiDeviceGoodix533cClass *klass) +{ + FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = fpi_device_goodix533c_finalize; + + dev_class->id = "goodix533c"; + dev_class->full_name = "Goodix 27c6:533c Fingerprint Sensor"; + dev_class->type = FP_DEVICE_TYPE_USB; + dev_class->scan_type = FP_SCAN_TYPE_PRESS; + dev_class->id_table = goodix533c_id_table; + dev_class->nr_enroll_stages = 1; + dev_class->temp_hot_seconds = -1; + /* FpDevice requires a non-NONE feature set (see fp_device_constructed()'s + * g_assert). FP_DEVICE_FEATURE_CAPTURE is the accurate declaration here + * and, unlike VERIFY/IDENTIFY, does not require those vfuncs to be set -- + * enroll/verify/identify are out of scope for this driver so far; open() + * + capture is exercised only via fpi_device_goodix533c_capture_test(). */ + dev_class->features = FP_DEVICE_FEATURE_CAPTURE; + + dev_class->open = goodix533c_open; + dev_class->close = goodix533c_close; +} diff --git a/libfprint/drivers/goodix533c/goodix533c.h b/libfprint/drivers/goodix533c/goodix533c.h new file mode 100644 index 000000000..ed890e3ee --- /dev/null +++ b/libfprint/drivers/goodix533c/goodix533c.h @@ -0,0 +1,81 @@ +/* + * Goodix 27c6:533c native driver for libfprint + * + * Copyright (C) 2026 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* + * This driver targets a single goal: FpDevice open() succeeding against + * real 27c6:533c hardware, followed by capture of one raw frame. It is + * rooted directly at FP_TYPE_DEVICE (not FpImageDevice, not + * FpiDeviceGoodixTls) because 533c's finger-detect/calibration sequence is + * fundamentally session-dynamic (see measure_baseline() in + * driver_53xc.py / findings/native-driver-architecture.md) and does not + * fit goodix5xx.c's shared FDT state machine, which assumes a static + * config blob sourced from a no-argument class vfunc. + * + * The wire-level checksum/framing codec (goodix_proto.c/.h) and the + * embedded TLS-PSK server (goodixtls.c/.h) are reused unmodified from the + * sibling goodixtls/ driver directory -- both are already device-agnostic. + * Everything else here is new, ported from the *logic* (not the compiled + * functions -- those are hard-tied to FpiDeviceGoodixTls) of goodix.c, + * cross-checked stage for stage against vendor/goodix-fp-dump-nikicat's + * driver_53xc.py, which is authoritative for this exact silicon. + */ + +#pragma once + +#include "fpi-device.h" + +G_DECLARE_FINAL_TYPE (FpiDeviceGoodix533c, fpi_device_goodix533c, FPI, + DEVICE_GOODIX533C, FpDevice) + +#define FPI_TYPE_DEVICE_GOODIX533C (fpi_device_goodix533c_get_type ()) + +#define GOODIX533C_SENSOR_WIDTH (108) +#define GOODIX533C_SENSOR_HEIGHT (88) + +/** + * Goodix533cCaptureDoneFunc: callback for the test-only capture entry + * point below. + * + * @raw_pixels: (nullable): GOODIX533C_SENSOR_WIDTH * GOODIX533C_SENSOR_HEIGHT + * 12-bit-ish samples (one guint16 per pixel, unpacked straight off the + * wire -- not squashed), owned by the callee, valid only for the + * duration of the callback. NULL on error. + * @squashed: (nullable): the same frame min-max stretched to 8 bits per + * pixel, row-major, GOODIX533C_SENSOR_WIDTH * GOODIX533C_SENSOR_HEIGHT + * bytes. NULL on error. + */ +typedef void (*Goodix533cCaptureDoneFunc)(FpDevice *dev, + const guint16 *raw_pixels, + const guint8 *squashed, + gpointer user_data, + GError *error); + +/** + * fpi_device_goodix533c_capture_test: + * + * Not public libfprint API -- a test-only entry point for driving the + * reset -> PSK/firmware check (already done by open()) -> TLS handshake -> + * config upload -> FDT baseline -> one-frame capture sequence, for use by + * a standalone test harness after fp_device_open() has completed. Must + * only be called once per open() session. + */ +void fpi_device_goodix533c_capture_test (FpDevice *dev, + Goodix533cCaptureDoneFunc callback, + gpointer user_data); diff --git a/libfprint/meson.build b/libfprint/meson.build index 6df412a34..35fcc28cf 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -141,6 +141,8 @@ driver_sources = { [ 'drivers/goodixmoc/goodix.c', 'drivers/goodixmoc/goodix_proto.c' ], 'goodixtls511' : [ 'drivers/goodixtls/goodix511.c' ], + 'goodix533c' : + [ 'drivers/goodix533c/goodix533c.c' ], 'fpcmoc' : [ 'drivers/fpcmoc/fpc.c' ], } @@ -305,6 +307,14 @@ libfprint_private_dep = declare_dependency( ] ) +if 'goodix533c' in drivers + goodix533c_capture_test = executable('goodix533c-capture-test', + 'drivers/goodix533c/capture_test.c', + dependencies: libfprint_private_dep, + link_with: libfprint_drivers, + install: false) +endif + udev_hwdb = executable('fprint-list-udev-hwdb', 'fprint-list-udev-hwdb.c', dependencies: libfprint_private_dep, diff --git a/meson.build b/meson.build index fa2750740..73ef903af 100644 --- a/meson.build +++ b/meson.build @@ -125,6 +125,7 @@ default_drivers = [ 'upekts', 'goodixmoc', 'goodixtls511', + 'goodix533c', 'nb1010', 'fpcmoc', @@ -160,6 +161,7 @@ driver_helper_mapping = { 'uru4000' : [ 'nss' ], 'elanspi' : [ 'udev' ], 'goodixtls511' : [ 'goodixtls' ], + 'goodix533c' : [ 'goodixtls' ], 'virtual_image' : [ 'virtual' ], 'virtual_device' : [ 'virtual' ], 'virtual_device_storage' : [ 'virtual' ], From 932b51c8e87c51858207ef125b5c8ed74e24af62 Mon Sep 17 00:00:00 2001 From: daemonhorn Date: Sat, 22 Aug 2026 21:51:04 -0400 Subject: [PATCH 03/12] Extend goodix533c capture with finger-wait, live frame, and flat-field Adds the second half of driver_53xc.py's run_driver() to the existing CAPTURE_STAGE_* SSM (one continuous state machine, not a parallel one): mcu_switch_to_sleep_mode/query_mcu_state after the reference frame, then arm finger detection (mcu_switch_to_fdt_down), wait for the device's asynchronous "touched" push (await_fdt_down_push, a bounded single read standing in for driver_53xc.py's PyUSB-timeout-driven polling loop), re-arm via mcu_switch_to_fdt_mode, capture a live (finger-present) frame, and mcu_switch_to_fdt_up. The reference frame is now kept in the private struct (self->reference_pixels) instead of only handed to a callback and discarded, so the live frame can be flat-fielded against it (ported least-squares scale+offset subtraction from flat_field() in driver_53xc.py, double precision) and min-max stretched to a spec-correct PGM. Live-capture gain is 0xc2, not driver_53xc.py's default 0x86 -- a deliberate, hardware-verified deviation documented in GOODIX533C_LIVE_IMAGE_GAIN's comment (0x86 clips ~47% of pixels on this unit; see NOTES.md's "Ridge visibility resolved" section). Verified against real 27c6:533c hardware without touching the sensor: all four new commands (sleep_mode 0x60, query_mcu_state 0xae, fdt_down 0x32 arm, fdt_up 0x34) ACK cleanly, the reference frame is still captured and written even though the sequence fails later, and the finger-wait stage times out cleanly at 30s with a specific "No finger detected within 30 seconds" error -- no hang, no crash, no generic error. The finger-detected/live-capture/flat-field success path is unverified (requires a physical touch during a follow-up run). capture_test.c is extended to drive the whole sequence, print a "Touch the sensor now" prompt right as finger-detection arms, and write both the reference PGM (always, if captured) and a flat-fielded "-live" PGM (only if a touch was detected). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y9XXWG7y2kfBEXJj3MS5yv --- libfprint/drivers/goodix533c/capture_test.c | 117 ++++- libfprint/drivers/goodix533c/goodix533c.c | 515 ++++++++++++++++++-- libfprint/drivers/goodix533c/goodix533c.h | 59 ++- 3 files changed, 609 insertions(+), 82 deletions(-) diff --git a/libfprint/drivers/goodix533c/capture_test.c b/libfprint/drivers/goodix533c/capture_test.c index 7e29add5d..cdc4466d5 100644 --- a/libfprint/drivers/goodix533c/capture_test.c +++ b/libfprint/drivers/goodix533c/capture_test.c @@ -7,11 +7,19 @@ * vfunc wired up yet (out of scope for this task). Builds only when * 'goodix533c' is in the enabled driver list (see libfprint/meson.build). * - * Usage: goodix533c-capture-test [output.pgm] + * Usage: goodix533c-capture-test [reference-output.pgm] + * + * Drives the full sequence: reset -> TLS -> config upload -> FDT baseline + * -> no-finger reference frame -> arm finger detection -> wait for a + * touch (up to GOODIX533C_FINGER_WAIT_TIMEOUT_MS) -> live frame -> flat + * field. The reference frame's PGM is always written if captured, whether + * or not a finger was ever touched to the sensor; the flat-fielded + * "-live" PGM is only written if a touch was actually detected in time. */ #include #include +#include #include @@ -46,6 +54,40 @@ write_pgm (const char *path, const guint8 *pixels, int width, int height) return n == (size_t) (width * height); } +/* Derives "-live.pgm" from the reference-frame output path (e.g. + * "capture.pgm" -> "capture-live.pgm"), so a single positional argument + * on the command line still names both output files predictably. */ +static gchar * +live_output_path (const char *reference_path) +{ + const char *dot = strrchr (reference_path, '.'); + + if (dot) + return g_strdup_printf ("%.*s-live%s", (int) (dot - reference_path), + reference_path, dot); + + return g_strdup_printf ("%s-live", reference_path); +} + +static void +print_pixel_range (const char *label, const guint16 *pixels, int count) +{ + guint16 min = 0xffff; + guint16 max = 0; + int i; + + for (i = 0; i < count; i++) + { + if (pixels[i] < min) + min = pixels[i]; + if (pixels[i] > max) + max = pixels[i]; + } + + g_print ("%s: %dx%d, raw pixel range [%u, %u]\n", label, + GOODIX533C_SENSOR_WIDTH, GOODIX533C_SENSOR_HEIGHT, min, max); +} + static void on_closed (FpDevice *dev, GAsyncResult *res, TestState *ts) { @@ -60,44 +102,68 @@ on_closed (FpDevice *dev, GAsyncResult *res, TestState *ts) g_main_loop_quit (ts->loop); } +static void +on_wait_for_finger (FpDevice *dev, gpointer user_data) +{ + g_print ("Touch the sensor now (%ds)...\n", + GOODIX533C_FINGER_WAIT_TIMEOUT_MS / 1000); +} + static void on_capture_done (FpDevice *dev, const guint16 *raw_pixels, - const guint8 *squashed, gpointer user_data, GError *error) + const guint8 *squashed, const guint16 *live_raw_pixels, + const guint8 *corrected, gpointer user_data, GError *error) { TestState *ts = user_data; int count = GOODIX533C_SENSOR_WIDTH * GOODIX533C_SENSOR_HEIGHT; - int i; - guint16 raw_min = 0xffff; - guint16 raw_max = 0; - if (error) + /* Write whatever frames actually came back before looking at @error -- + * a failure partway through (e.g. no finger touched within the + * timeout) must not throw away a reference frame that was already + * captured successfully earlier in the same sequence. */ + if (raw_pixels && squashed) { - g_print ("Capture FAILED: %s\n", error->message); - ts->exit_code = 1; - fp_device_close (dev, NULL, (GAsyncReadyCallback) on_closed, ts); - return; + print_pixel_range ("Reference frame", raw_pixels, count); + + if (write_pgm (ts->output_path, squashed, GOODIX533C_SENSOR_WIDTH, + GOODIX533C_SENSOR_HEIGHT)) + g_print ("Wrote %s\n", ts->output_path); + else + { + g_print ("Failed to write %s\n", ts->output_path); + ts->exit_code = 1; + } } - - for (i = 0; i < count; i++) + else { - if (raw_pixels[i] < raw_min) - raw_min = raw_pixels[i]; - if (raw_pixels[i] > raw_max) - raw_max = raw_pixels[i]; + g_print ("No reference frame captured.\n"); } - g_print ("Captured frame: %dx%d, raw pixel range [%u, %u]\n", - GOODIX533C_SENSOR_WIDTH, GOODIX533C_SENSOR_HEIGHT, raw_min, - raw_max); + if (live_raw_pixels && corrected) + { + g_autofree gchar *live_path = live_output_path (ts->output_path); + + print_pixel_range ("Live frame", live_raw_pixels, count); + + if (write_pgm (live_path, corrected, GOODIX533C_SENSOR_WIDTH, + GOODIX533C_SENSOR_HEIGHT)) + g_print ("Wrote %s (flat-fielded fingerprint)\n", live_path); + else + { + g_print ("Failed to write %s\n", live_path); + ts->exit_code = 1; + } + } - if (write_pgm (ts->output_path, squashed, GOODIX533C_SENSOR_WIDTH, - GOODIX533C_SENSOR_HEIGHT)) - g_print ("Wrote %s\n", ts->output_path); - else + if (error) { - g_print ("Failed to write %s\n", ts->output_path); + g_print ("Capture sequence FAILED: %s\n", error->message); ts->exit_code = 1; } + else + { + g_print ("Capture sequence completed successfully.\n"); + } fp_device_close (dev, NULL, (GAsyncReadyCallback) on_closed, ts); } @@ -116,7 +182,8 @@ on_opened (FpDevice *dev, GAsyncResult *res, TestState *ts) } g_print ("open() SUCCEEDED\n"); - fpi_device_goodix533c_capture_test (dev, on_capture_done, ts); + fpi_device_goodix533c_capture_test (dev, on_wait_for_finger, + on_capture_done, ts); } int diff --git a/libfprint/drivers/goodix533c/goodix533c.c b/libfprint/drivers/goodix533c/goodix533c.c index 3f7429c07..e8768db90 100644 --- a/libfprint/drivers/goodix533c/goodix533c.c +++ b/libfprint/drivers/goodix533c/goodix533c.c @@ -57,10 +57,45 @@ #define GOODIX533C_IMAGE_FLAGS_CALIBRATE (0x01) #define GOODIX533C_IMAGE_GAIN (0xc2) +/* Live (finger-present) frame: flags = 0x01 | 0x40 per the findings doc. + * Gain is a *deliberate deviation* from driver_53xc.py's default -- see + * GOODIX533C_LIVE_IMAGE_GAIN below. */ +#define GOODIX533C_IMAGE_FLAGS_LIVE (0x41) + +/* driver_53xc.py's run_driver() uses gain 0x86 for the live capture + * (tuned for nikicat's XPS 13 9310). This project's own empirical finding + * (NOTES.md, "Ridge visibility resolved: gain calibration, not protocol") + * is that 0x86 clips ~47% of pixels on the hardware this project tests + * against, while 0xc2 -- the same gain already used for the reference + * frame -- is headroom-safe (0 clipped pixels) for *both* frame types on + * this unit. Using 0xc2 here too, not 0x86, is intentional and + * hardware-verified for this unit, not an oversight. A production driver + * would need a per-unit gain check rather than a hardcoded value, since + * the safe gain is apparently unit-specific -- out of scope here. */ +#define GOODIX533C_LIVE_IMAGE_GAIN (0xc2) + #define GOODIX533C_CAPTURE_REGISTER (0x022c) static const guint8 capture_on[2] = { 0x0a, 0x03 }; static const guint8 capture_off[2] = { 0x0a, 0x02 }; +/* Not in goodix_proto.h (0x60) -- defined locally like + * GOODIX533C_FLAGS_TLS_DATA above. */ +#define GOODIX533C_CMD_MCU_SWITCH_TO_SLEEP_MODE (0x60) + +/* FDT command prefixes -- fixed 2-byte prefix, each suffixed with the same + * 24-byte per-session template read during CAPTURE_STAGE_FDT_BASELINE. + * fdt_mode_idle (above) is the fourth member of this family, used with 24 + * zero bytes to *measure* the template; these three arm/query it. */ +static const guint8 fdt_mode_armed[2] = { 0x8d, 0x01 }; +static const guint8 fdt_down_armed[2] = { 0x0c, 0x01 }; +static const guint8 fdt_up_armed[2] = { 0x0e, 0x01 }; + +/* driver_53xc.py reads mcu_switch_to_fdt_up()'s reply with timeout=None + * (block indefinitely) -- the sensor isn't waiting on any further + * external input at this point (finger already detected), so a generous + * bounded timeout stands in safely for "no timeout" here. */ +#define GOODIX533C_FDT_UP_TIMEOUT_MS (5000) + #define GOODIX533C_PSK_LENGTH (32) #define GOODIX533C_PSK_FLAGS (0xbb020001) /* sha256(bytes(32)) -- expected PSK hash, all-zero PSK per this whole @@ -139,10 +174,16 @@ struct _FpiDeviceGoodix533c gboolean tls_active; /* per-session FDT baseline, read fresh every open per the findings doc; - * unused beyond this scope (no fdt_down/fdt_up arming here) but kept for - * fidelity to the golden capture sequence. */ + * appended (with a distinct fixed prefix) to every FDT arm/query + * command below. */ guint8 fdt_template[24]; gboolean have_fdt_template; + + /* no-finger reference frame, kept around (not just handed to a callback + * and discarded) so a later live-frame capture in the same session can + * flat-field against it without re-measuring. */ + guint16 *reference_pixels; + gboolean have_reference; }; G_DEFINE_TYPE (FpiDeviceGoodix533c, fpi_device_goodix533c, FP_TYPE_DEVICE) @@ -585,6 +626,106 @@ cmd_mcu_get_image_gain (FpDevice *dev, guint8 flags, guint8 gain, user_data); } +static void +cmd_mcu_switch_to_sleep_mode (FpDevice *dev, Goodix533cCmdCallback callback, + gpointer user_data) +{ + guint8 payload[2] = { 0x01, 0x00 }; + + /* Ack-only -- matches goodix.py's mcu_switch_to_sleep_mode(), which only + * ever calls _expect_ack(). */ + send_protocol (dev, GOODIX533C_CMD_MCU_SWITCH_TO_SLEEP_MODE, payload, + sizeof (payload), TRUE, GOODIX533C_TIMEOUT_MS, TRUE, FALSE, + callback, user_data); +} + +static void +cmd_query_mcu_state (FpDevice *dev, const guint8 *payload, guint16 length, + Goodix533cCmdCallback callback, gpointer user_data) +{ + /* This driver's one call site (run_driver()'s query_mcu_state(b"\x01\x00 + * \x01", False) right after mcu_switch_to_sleep_mode()) always passes + * reply=False in driver_53xc.py -- ACK-only here, matching that. The + * reply=True data-read path (goodix.py's query_mcu_state()) is unused + * and not implemented. */ + send_protocol (dev, GOODIX_CMD_QUERY_MCU_STATE, payload, length, TRUE, + GOODIX533C_TIMEOUT_MS, TRUE, FALSE, callback, user_data); +} + +static void +cmd_mcu_switch_to_fdt_down (FpDevice *dev, const guint8 *mode, guint16 length, + Goodix533cCmdCallback callback, gpointer user_data) +{ + /* Ack-only -- arms finger detection. The actual touch notification + * arrives later as a separate, asynchronous protocol pack tagged with + * this same command (see await_fdt_down_push() below), not as a reply + * to this call. Matches driver_53xc.py's one call site, + * mcu_switch_to_fdt_down(mode, False). */ + send_protocol (dev, GOODIX_CMD_MCU_SWITCH_TO_FDT_DOWN, mode, length, TRUE, + GOODIX533C_TIMEOUT_MS, TRUE, FALSE, callback, user_data); +} + +static void +cmd_mcu_switch_to_fdt_up (FpDevice *dev, const guint8 *mode, guint16 length, + Goodix533cCmdCallback callback, gpointer user_data) +{ + /* ACK, then always a data reply -- see GOODIX533C_FDT_UP_TIMEOUT_MS. */ + send_protocol (dev, GOODIX_CMD_MCU_SWITCH_TO_FDT_UP, mode, length, TRUE, + GOODIX533C_FDT_UP_TIMEOUT_MS, TRUE, TRUE, callback, + user_data); +} + +/** + * await_fdt_down_push: wait for the device's unsolicited "finger touched" + * notification. + * + * No request is sent here -- the device pushes this pack on its own, some + * time after cmd_mcu_switch_to_fdt_down() armed detection, once (and only + * once) a finger actually lands. Manual protocol-reply state set, same + * shape as await_raw_pack() above (used for the TLS handshake's raw + * packs), just matched against a specific command byte instead of + * bypassing cmd matching entirely. + * + * driver_53xc.py's wait_for_finger() polls with a sequence of short (2s) + * blocking reads for up to 30s, working around a PyUSB limitation on long + * reads. FpiUsbTransfer has no such limitation -- the read loop + * (receive_data()) already has one bulk IN transfer permanently + * in-flight, so a single bounded timeout on the reply we're waiting for + * does the same job without polling. + */ +static void +await_fdt_down_push (FpDevice *dev, guint timeout_ms, + Goodix533cCmdCallback callback, gpointer user_data) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + + if (self->ack_pending || self->reply_pending) + { + /* Must not silently hang the caller -- report it as a real failure, + * same as send_protocol() would if it could (it can only fp_warn() + * and drop, since it has no callback contract for this case; here + * we do have one, so use it). */ + GError *error = NULL; + + fp_warn ("A command is already running: 0x%02x", self->cmd); + g_set_error (&error, G_IO_ERROR, G_IO_ERROR_BUSY, + "Cannot wait for finger: command 0x%02x still in flight", + self->cmd); + callback (dev, NULL, 0, user_data, error); + return; + } + + if (timeout_ms) + self->timeout_src = fpi_device_add_timeout (dev, timeout_ms, + on_command_timeout, NULL, + NULL); + self->cmd = GOODIX_CMD_MCU_SWITCH_TO_FDT_DOWN; + self->ack_pending = FALSE; + self->reply_pending = TRUE; + self->callback = callback; + self->user_data = user_data; +} + static void cmd_request_tls_connection (FpDevice *dev, Goodix533cCmdCallback callback, gpointer user_data) @@ -862,11 +1003,12 @@ squash_frame_linear (const guint16 *frame, guint8 *squashed, guint32 count) } /* =========================================================================== - * Capture-test sequence -- new code, following capture_golden_session.py / - * driver_53xc.py's run_driver() exactly, simplified per this task's scope: - * a single no-finger reference frame is decoded and handed back as-is (no - * flat-fielding against a second frame, no finger-detect wait). See the - * driver header and the final report for why. + * Capture sequence -- new code, following capture_golden_session.py / + * driver_53xc.py's run_driver() exactly: reset through the no-finger + * reference frame, then sleep/query -> arm finger detection -> wait for a + * touch -> live (finger-present) frame -> flat-field the live frame + * against the reference. One continuous FpiSsm, not two -- see the FDT + * arm/wait/capture stages appended below CAPTURE_STAGE_CAPTURE_OFF. * ======================================================================= */ enum capture_stage { @@ -876,19 +1018,32 @@ enum capture_stage { CAPTURE_STAGE_TLS, CAPTURE_STAGE_UPLOAD_CONFIG, CAPTURE_STAGE_FDT_BASELINE, - CAPTURE_STAGE_CAPTURE_ON, + CAPTURE_STAGE_CAPTURE_ON, /* reference frame, no finger */ CAPTURE_STAGE_GET_IMAGE, CAPTURE_STAGE_CAPTURE_OFF, + CAPTURE_STAGE_SLEEP, + CAPTURE_STAGE_QUERY_MCU_STATE, + CAPTURE_STAGE_FDT_ARM_DOWN, + CAPTURE_STAGE_WAIT_FOR_FINGER, + CAPTURE_STAGE_FDT_MODE_ARM, + CAPTURE_STAGE_CAPTURE_ON_LIVE, /* live (finger-present) frame */ + CAPTURE_STAGE_GET_IMAGE_LIVE, + CAPTURE_STAGE_CAPTURE_OFF_LIVE, + CAPTURE_STAGE_FDT_UP, CAPTURE_STAGE_NUM, }; typedef struct { + Goodix533cProgressFunc wait_for_finger_cb; Goodix533cCaptureDoneFunc callback; gpointer user_data; - guint16 *raw_pixels; + guint16 *raw_pixels; /* reference frame */ guint8 *squashed; + + guint16 *live_raw_pixels; /* live frame */ + guint8 *corrected; /* flat-fielded + squashed */ } CaptureData; static void @@ -896,6 +1051,8 @@ capture_data_free (CaptureData *data) { g_free (data->raw_pixels); g_free (data->squashed); + g_free (data->live_raw_pixels); + g_free (data->corrected); g_free (data); } @@ -993,9 +1150,28 @@ on_fdt_baseline_reply (FpDevice *dev, guint8 *data, guint16 length, /* Reply is a 4-byte header then 12-bit samples as 16-bit LE words. Vendor * driver halves each sample and emits it twice as the FDT threshold - * template -- see fdt_template() in driver_53xc.py. Not consumed further - * in this capture-only build (no fdt_down/fdt_up arming in scope), kept - * only for fidelity to the golden sequence. */ + * template -- see fdt_template() in driver_53xc.py. Appended (with a + * distinct fixed 2-byte prefix) to every later FDT arm/query command in + * this same session -- see fdt_mode_armed/fdt_down_armed/fdt_up_armed + * above and their use in capture_run() below. */ + /* Sample count: driver_53xc.py's fdt_template() computes this as + * len(range(4, length - 1, 2)), which looks off-by-one against the + * naive (length - 4) / 2 used below at first glance, but is not -- + * range(4, length-1, 2) has floor((length-6)/2)+1 terms (for length>=6, + * else 0), and floor(x)+1 == floor(x+1) for any real x when 1 is an + * integer, so that's floor((length-6)/2 + 1) == floor((length-4)/2) -- + * exactly the integer-division formula below. Verified algebraically, + * not just against this session's one hardware reply (which happened to + * land on the boundary case, length=28, 12 samples, where both + * formulas trivially agree). The MIN(..., 12u) cap has no Python + * equivalent -- Python's `samples` is an unbounded list, but this + * driver's fdt_template is a fixed 24-byte (12-sample) array because + * every FDT arm/query payload below is hardcoded to a fixed 2-byte + * prefix + 24-byte template (matching driver_53xc.py's own + * FDT_MODE_ARMED + template, etc., which are always built from a + * 24-byte template in practice); the cap only ever discards *extra* + * data past the first 12 samples, it does not change which of the + * first 12 samples are read. */ memset (self->fdt_template, 0, sizeof (self->fdt_template)); sample_count = MIN ((guint32) (length > 4 ? (length - 4) / 2 : 0), 12u); for (i = 0; i < sample_count; i++) @@ -1010,29 +1186,26 @@ on_fdt_baseline_reply (FpDevice *dev, guint8 *data, guint16 length, fpi_ssm_next_state (ssm); } -static void -on_get_image_reply (FpDevice *dev, guint8 *data, guint16 length, - gpointer user_data, GError *error) +/** + * decode_get_image_reply: shared by the reference-frame and live-frame + * GET_IMAGE stages -- decrypt a mcu_get_image reply and decode it to + * pixels, optionally also min-max squashing to 8 bits. @out_squashed may + * be NULL if the caller doesn't need that (the live frame is squashed + * only after flat-fielding, not here). + */ +static gboolean +decode_get_image_reply (FpiDeviceGoodix533c *self, guint8 *data, + guint16 length, guint16 **out_raw_pixels, + guint8 **out_squashed, GError **error) { - FpiSsm *ssm = user_data; - FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); - CaptureData *cap = fpi_ssm_get_data (ssm); guint8 decrypt_buf[65535]; int decrypted; - GError *tls_error = NULL; - - if (error) - { - fpi_ssm_mark_failed (ssm, error); - return; - } if (length <= GOODIX533C_IMAGE_REPLY_HEADER_LEN) { - fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, - "image reply too short: %d", - length)); - return; + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "image reply too short: %d", length); + return FALSE; } /* Skip the pre-record header (see GOODIX533C_IMAGE_REPLY_HEADER_LEN's @@ -1043,36 +1216,171 @@ on_get_image_reply (FpDevice *dev, guint8 *data, guint16 length, (guint16) (length - GOODIX533C_IMAGE_REPLY_HEADER_LEN)); decrypted = goodix_tls_server_read (&self->tls, decrypt_buf, - sizeof (decrypt_buf), &tls_error); + sizeof (decrypt_buf), error); if (decrypted <= 0) { - fpi_ssm_mark_failed (ssm, tls_error ? tls_error : - g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, - "TLS decrypt failed")); - return; + if (error && !*error) + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "TLS decrypt failed"); + return FALSE; } if ((guint32) decrypted < GOODIX533C_IMAGE_BYTES) { - fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, - "short decrypt: %d < %d", - decrypted, - GOODIX533C_IMAGE_BYTES)); - return; + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "short decrypt: %d < %d", decrypted, + GOODIX533C_IMAGE_BYTES); + return FALSE; } - cap->raw_pixels = g_new0 (guint16, GOODIX533C_IMAGE_PIXELS); - cap->squashed = g_new0 (guint8, GOODIX533C_IMAGE_PIXELS); - decode_frame (cap->raw_pixels, decrypt_buf, GOODIX533C_IMAGE_BYTES); - squash_frame_linear (cap->raw_pixels, cap->squashed, - GOODIX533C_IMAGE_PIXELS); + *out_raw_pixels = g_new0 (guint16, GOODIX533C_IMAGE_PIXELS); + decode_frame (*out_raw_pixels, decrypt_buf, GOODIX533C_IMAGE_BYTES); + + if (out_squashed) + { + *out_squashed = g_new0 (guint8, GOODIX533C_IMAGE_PIXELS); + squash_frame_linear (*out_raw_pixels, *out_squashed, + GOODIX533C_IMAGE_PIXELS); + } fp_dbg ("Decoded frame: %d bytes encrypted -> %d bytes plain -> %d pixels", length, decrypted, GOODIX533C_IMAGE_PIXELS); + return TRUE; +} + +static void +on_get_image_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + CaptureData *cap = fpi_ssm_get_data (ssm); + gboolean live = fpi_ssm_get_cur_state (ssm) == CAPTURE_STAGE_GET_IMAGE_LIVE; + guint16 *raw_pixels = NULL; + guint8 *squashed = NULL; + GError *decode_error = NULL; + + if (error) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + + if (!decode_get_image_reply (self, data, length, &raw_pixels, + live ? NULL : &squashed, &decode_error)) + { + fpi_ssm_mark_failed (ssm, decode_error); + return; + } + + if (live) + { + cap->live_raw_pixels = raw_pixels; + } + else + { + cap->raw_pixels = raw_pixels; + cap->squashed = squashed; + + /* Keep a copy in the driver's private struct (not just handed to + * the callback) so the live-frame stages further down this same + * SSM can flat-field against it without re-measuring. */ + g_clear_pointer (&self->reference_pixels, g_free); + self->reference_pixels = g_new (guint16, GOODIX533C_IMAGE_PIXELS); + memcpy (self->reference_pixels, raw_pixels, + GOODIX533C_IMAGE_PIXELS * sizeof (guint16)); + self->have_reference = TRUE; + } + + fpi_ssm_next_state (ssm); +} + +static void +on_wait_finger_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + + if (error) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) + { + /* Give the caller a specific, actionable error instead of a bare + * protocol-layer timeout -- this is the expected, well-behaved + * outcome of running the sequence with no finger on the sensor. */ + g_clear_error (&error); + error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "No finger detected within %d " + "seconds", + GOODIX533C_FINGER_WAIT_TIMEOUT_MS / 1000); + } + fpi_ssm_mark_failed (ssm, error); + return; + } + + fp_dbg ("Finger detected (fdt_down push, %d bytes)", length); fpi_ssm_next_state (ssm); } +/** + * flat_field_squash: port of flat_field() in driver_53xc.py (ordinary + * least-squares regression of the live frame against the reference frame, + * then subtract the fitted line) followed by a min-max stretch of the + * (possibly negative, possibly >12-bit) residual to 8 bits -- the same + * squash technique squash_frame_linear() above uses for a raw frame, just + * over a double-precision residual instead of guint16 samples. + */ +static void +flat_field_squash (const guint16 *frame, const guint16 *reference, + guint32 count, guint8 *out) +{ + double mean_frame = 0, mean_reference = 0; + double variance = 0, covariance = 0; + double a, b; + g_autofree double *residual = g_new (double, count); + double min = G_MAXDOUBLE, max = -G_MAXDOUBLE; + guint32 i; + + for (i = 0; i < count; i++) + { + mean_frame += frame[i]; + mean_reference += reference[i]; + } + mean_frame /= count; + mean_reference /= count; + + for (i = 0; i < count; i++) + { + double d = (double) reference[i] - mean_reference; + + variance += d * d; + covariance += ((double) frame[i] - mean_frame) * d; + } + if (variance == 0) + variance = 1; + + a = covariance / variance; + b = mean_frame - a * mean_reference; + + for (i = 0; i < count; i++) + { + residual[i] = (double) frame[i] - (a * (double) reference[i] + b); + if (residual[i] < min) + min = residual[i]; + if (residual[i] > max) + max = residual[i]; + } + + for (i = 0; i < count; i++) + { + if (max <= min) + out[i] = 0; + else + out[i] = (guint8) (((residual[i] - min) * 0xff) / (max - min)); + } +} + static void capture_run (FpiSsm *ssm, FpDevice *dev) { @@ -1126,6 +1434,95 @@ capture_run (FpiSsm *ssm, FpDevice *dev) capture_off, on_capture_step_reply, ssm); break; + case CAPTURE_STAGE_SLEEP: + cmd_mcu_switch_to_sleep_mode (dev, on_capture_step_reply, ssm); + break; + + case CAPTURE_STAGE_QUERY_MCU_STATE: + { + /* Payload taken verbatim from run_driver()'s + * query_mcu_state(b"\x01\x00\x01", False) call site. */ + static const guint8 payload[3] = { 0x01, 0x00, 0x01 }; + + cmd_query_mcu_state (dev, payload, sizeof (payload), + on_capture_step_reply, ssm); + } + break; + + case CAPTURE_STAGE_FDT_ARM_DOWN: + { + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + guint8 mode[26]; + + memcpy (mode, fdt_down_armed, sizeof (fdt_down_armed)); + memcpy (mode + sizeof (fdt_down_armed), self->fdt_template, + sizeof (self->fdt_template)); + cmd_mcu_switch_to_fdt_down (dev, mode, sizeof (mode), + on_capture_step_reply, ssm); + } + break; + + case CAPTURE_STAGE_WAIT_FOR_FINGER: + { + CaptureData *cap = fpi_ssm_get_data (ssm); + + if (cap->wait_for_finger_cb) + cap->wait_for_finger_cb (dev, cap->user_data); + await_fdt_down_push (dev, GOODIX533C_FINGER_WAIT_TIMEOUT_MS, + on_wait_finger_reply, ssm); + } + break; + + case CAPTURE_STAGE_FDT_MODE_ARM: + { + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + guint8 mode[26]; + + memcpy (mode, fdt_mode_armed, sizeof (fdt_mode_armed)); + memcpy (mode + sizeof (fdt_mode_armed), self->fdt_template, + sizeof (self->fdt_template)); + /* reply=True, matching driver_53xc.py's + * mcu_switch_to_fdt_mode(FDT_MODE_ARMED + template, True) call + * site -- but run_driver() never uses the returned payload + * either, it just re-arms, so on_capture_step_reply discarding + * it here is correct, not a shortcut. */ + cmd_mcu_switch_to_fdt_mode (dev, mode, sizeof (mode), TRUE, + on_capture_step_reply, ssm); + } + break; + + case CAPTURE_STAGE_CAPTURE_ON_LIVE: + cmd_write_sensor_register (dev, GOODIX533C_CAPTURE_REGISTER, + capture_on, on_capture_step_reply, ssm); + break; + + case CAPTURE_STAGE_GET_IMAGE_LIVE: + /* Gain 0xc2, not driver_53xc.py's default 0x86 for the live frame -- + * see GOODIX533C_LIVE_IMAGE_GAIN's doc comment above for why this is + * a deliberate, hardware-verified deviation on this unit. */ + cmd_mcu_get_image_gain (dev, GOODIX533C_IMAGE_FLAGS_LIVE, + GOODIX533C_LIVE_IMAGE_GAIN, on_get_image_reply, + ssm); + break; + + case CAPTURE_STAGE_CAPTURE_OFF_LIVE: + cmd_write_sensor_register (dev, GOODIX533C_CAPTURE_REGISTER, + capture_off, on_capture_step_reply, ssm); + break; + + case CAPTURE_STAGE_FDT_UP: + { + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + guint8 mode[26]; + + memcpy (mode, fdt_up_armed, sizeof (fdt_up_armed)); + memcpy (mode + sizeof (fdt_up_armed), self->fdt_template, + sizeof (self->fdt_template)); + cmd_mcu_switch_to_fdt_up (dev, mode, sizeof (mode), + on_capture_step_reply, ssm); + } + break; + default: g_assert_not_reached (); } @@ -1134,19 +1531,37 @@ capture_run (FpiSsm *ssm, FpDevice *dev) static void capture_done (FpiSsm *ssm, FpDevice *dev, GError *error) { + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); CaptureData *cap = fpi_ssm_get_data (ssm); - cap->callback (dev, cap->raw_pixels, cap->squashed, cap->user_data, error); + /* Flat-field only if the live frame actually got captured -- e.g. the + * finger-wait stage timing out (the only path exercised against real + * hardware this session, since it requires no physical touch) leaves + * live_raw_pixels NULL and error non-NULL, and cap->raw_pixels/squashed + * (the reference frame, captured earlier in this same sequence) are + * still handed back below regardless of @error, so a harness never + * loses a frame that did succeed just because a later stage failed. */ + if (cap->live_raw_pixels && self->have_reference) + { + cap->corrected = g_new0 (guint8, GOODIX533C_IMAGE_PIXELS); + flat_field_squash (cap->live_raw_pixels, self->reference_pixels, + GOODIX533C_IMAGE_PIXELS, cap->corrected); + } + + cap->callback (dev, cap->raw_pixels, cap->squashed, cap->live_raw_pixels, + cap->corrected, cap->user_data, error); } void fpi_device_goodix533c_capture_test (FpDevice *dev, + Goodix533cProgressFunc wait_for_finger_cb, Goodix533cCaptureDoneFunc callback, gpointer user_data) { CaptureData *cap = g_new0 (CaptureData, 1); FpiSsm *ssm; + cap->wait_for_finger_cb = wait_for_finger_cb; cap->callback = callback; cap->user_data = user_data; @@ -1368,6 +1783,15 @@ goodix533c_close (FpDevice *dev) self->user_data = NULL; self->read_loop_started = FALSE; + /* Session-scoped state: the FDT template and reference frame are only + * valid for the session that measured/captured them (see the comments + * on measure_baseline()/FDT template dynamism in the findings doc). + * Clearing them here forces a fresh open() to redo both before any live + * capture can flat-field against a stale reference. */ + g_clear_pointer (&self->reference_pixels, g_free); + self->have_reference = FALSE; + self->have_fdt_template = FALSE; + if (self->interface_claimed) { g_usb_device_release_interface (fpi_device_get_usb_device (dev), @@ -1393,6 +1817,7 @@ fpi_device_goodix533c_finalize (GObject *object) FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (object); g_clear_pointer (&self->rx_buf, g_free); + g_clear_pointer (&self->reference_pixels, g_free); g_clear_object (&self->transfer_cancel_tkn); G_OBJECT_CLASS (fpi_device_goodix533c_parent_class)->finalize (object); diff --git a/libfprint/drivers/goodix533c/goodix533c.h b/libfprint/drivers/goodix533c/goodix533c.h index ed890e3ee..2e6ef892a 100644 --- a/libfprint/drivers/goodix533c/goodix533c.h +++ b/libfprint/drivers/goodix533c/goodix533c.h @@ -49,21 +49,49 @@ G_DECLARE_FINAL_TYPE (FpiDeviceGoodix533c, fpi_device_goodix533c, FPI, #define GOODIX533C_SENSOR_WIDTH (108) #define GOODIX533C_SENSOR_HEIGHT (88) +/* Matches driver_53xc.py's wait_for_finger() overall deadline (30s). + * Public so a test harness can quote the same figure in its prompt + * instead of duplicating the number. */ +#define GOODIX533C_FINGER_WAIT_TIMEOUT_MS (30000) + +/** + * Goodix533cProgressFunc: called once, mid-sequence, right as the driver + * arms finger detection and starts waiting for a touch -- the harness's + * cue to prompt the user. No data, just a checkpoint. + */ +typedef void (*Goodix533cProgressFunc)(FpDevice *dev, + gpointer user_data); + /** * Goodix533cCaptureDoneFunc: callback for the test-only capture entry - * point below. + * point below. Called exactly once, whether the sequence ran to + * completion or failed partway through -- any frames already captured + * before the failure are still handed back (non-NULL), so a harness can + * keep whatever succeeded instead of discarding it just because a later + * stage (e.g. finger-wait) failed. * - * @raw_pixels: (nullable): GOODIX533C_SENSOR_WIDTH * GOODIX533C_SENSOR_HEIGHT - * 12-bit-ish samples (one guint16 per pixel, unpacked straight off the - * wire -- not squashed), owned by the callee, valid only for the - * duration of the callback. NULL on error. - * @squashed: (nullable): the same frame min-max stretched to 8 bits per - * pixel, row-major, GOODIX533C_SENSOR_WIDTH * GOODIX533C_SENSOR_HEIGHT - * bytes. NULL on error. + * @raw_pixels: (nullable): the no-finger reference frame, + * GOODIX533C_SENSOR_WIDTH * GOODIX533C_SENSOR_HEIGHT 12-bit-ish samples + * (one guint16 per pixel, unpacked straight off the wire -- not + * squashed), owned by the callee, valid only for the duration of the + * callback. NULL if the reference frame itself was never captured. + * @squashed: (nullable): the reference frame min-max stretched to 8 bits + * per pixel, row-major, GOODIX533C_SENSOR_WIDTH * GOODIX533C_SENSOR_HEIGHT + * bytes. NULL under the same condition as @raw_pixels. + * @live_raw_pixels: (nullable): the live (finger-present) frame, same + * shape/units as @raw_pixels. NULL unless a finger was detected and a + * live frame was successfully captured. + * @corrected: (nullable): the live frame flat-fielded against the + * reference frame (least-squares scale+offset subtracted, see + * flat_field() in driver_53xc.py) and then min-max stretched to 8 bits + * per pixel, same shape as @squashed. This is the PGM-ready fingerprint + * image. NULL under the same condition as @live_raw_pixels. */ -typedef void (*Goodix533cCaptureDoneFunc)(FpDevice *dev, +typedef void (*Goodix533cCaptureDoneFunc)(FpDevice *dev, const guint16 *raw_pixels, const guint8 *squashed, + const guint16 *live_raw_pixels, + const guint8 *corrected, gpointer user_data, GError *error); @@ -72,10 +100,17 @@ typedef void (*Goodix533cCaptureDoneFunc)(FpDevice *dev, * * Not public libfprint API -- a test-only entry point for driving the * reset -> PSK/firmware check (already done by open()) -> TLS handshake -> - * config upload -> FDT baseline -> one-frame capture sequence, for use by - * a standalone test harness after fp_device_open() has completed. Must - * only be called once per open() session. + * config upload -> FDT baseline -> reference-frame capture -> sleep/query + * -> arm finger detection -> wait for touch -> live-frame capture -> flat + * field sequence, for use by a standalone test harness after + * fp_device_open() has completed. Must only be called once per open() + * session. + * + * @wait_for_finger_cb: (nullable): invoked once finger detection is armed + * and the driver starts waiting for a touch, so the harness can prompt + * the user right before the bounded wait begins. May be NULL. */ void fpi_device_goodix533c_capture_test (FpDevice *dev, + Goodix533cProgressFunc wait_for_finger_cb, Goodix533cCaptureDoneFunc callback, gpointer user_data); From 33a37d9a46f848c4e19067d9a2aef72797da06e1 Mon Sep 17 00:00:00 2001 From: daemonhorn Date: Sun, 23 Aug 2026 07:12:19 -0400 Subject: [PATCH 04/12] Vendor SIGFM (SIFT+CLAHE) fingerprint matching library Vendored verbatim from AndyHazz's goodix53x5-libfprint fork (sibling 27c6:5335/5385/5395 driver, same 108x88 sensor resolution, same SIGFM approach, different/incompatible transport). This is a self-contained C++ library operating on raw pixel buffers with a plain extern "C" API (sigfm_extract/sigfm_free_info/sigfm_match_score/sigfm_serialize_binary/ sigfm_deserialize_binary/sigfm_keypoints_count/sigfm_copy_info) with no device/GObject coupling, so it can be reused as-is for goodix533c. Placed at the repo root's sigfm/ (not libfprint/sigfm/) per the task's literal instruction; verified separately that libfprint/meson.build's root_inc already puts the repo root on every driver's include path, so #include "sigfm/sigfm.hpp" resolves the same way regardless of which of the two locations is chosen. --- sigfm/binary.hpp | 257 ++++++++++++++++++++++++++++++++++ sigfm/img-info.hpp | 10 ++ sigfm/sigfm.cpp | 334 +++++++++++++++++++++++++++++++++++++++++++++ sigfm/sigfm.hpp | 98 +++++++++++++ 4 files changed, 699 insertions(+) create mode 100644 sigfm/binary.hpp create mode 100644 sigfm/img-info.hpp create mode 100644 sigfm/sigfm.cpp create mode 100644 sigfm/sigfm.hpp diff --git a/sigfm/binary.hpp b/sigfm/binary.hpp new file mode 100644 index 000000000..c9a0d9e9a --- /dev/null +++ b/sigfm/binary.hpp @@ -0,0 +1,257 @@ + +#pragma once + +#include "opencv2/core/mat.hpp" +#include +#include +#include +#include +#include + +namespace bin { +using byte = unsigned char; + +class stream; + +template +struct serializer : public std::false_type { + void serialize(const T& m, stream& out); +}; + +template +struct deserializer : public std::false_type { + T deserialize(stream& in); +}; +class stream { +public: + stream() = default; + + stream(const byte* begin, const byte* end) : view_{begin}, view_size_{static_cast(end - begin)} + { + } + + template< + typename Iter, + std::enable_if_t>::value_type, + byte>, + bool> = true> + stream(Iter begin, Iter end) : store_{begin, end} + { + } + + template::value, bool> = true> + constexpr stream& operator<<(T v) + { + serializer::serialize(v, *this); + return *this; + } + + template::value, bool> = true> + constexpr stream& operator>>(T& v) + { + v = deserializer::deserialize(*this); + return *this; + } + template, bool> = true> + constexpr stream& operator<<(T v) + { + using seg_store = std::array; + alignas(T) seg_store s = {}; + std::memcpy(s.data(), &v, sizeof(T)); + stream::write(s.begin(), s.end()); + return *this; + } + + template, bool> = true> + constexpr stream& operator>>(T& v) + { + using seg_store = std::array; + alignas(T) seg_store s = {}; + if (size() < s.size()) { + throw std::runtime_error{"tried to extract from too small stream"}; + } + stream::read(s.begin(), s.end()); + memcpy(&v, s.data(), sizeof(T)); + return *this; + } + template< + typename Iter, + std::enable_if_t>::value_type, + byte>, + bool> = true> + constexpr stream& write(Iter&& begin, Iter&& end) + { + compact(); + std::copy(std::forward(begin), std::forward(end), + std::back_inserter(store_)); + return *this; + } + + template::value, bool> = true> + stream& serialize(const T& m, stream& out) + { + serializer::serialize(m, out); + return out; + } + + template< + typename Iter, + std::enable_if_t>::value_type, + byte>, + bool> = true> + constexpr stream& read(Iter&& begin, Iter&& end) + { + const auto dist = std::distance(begin, end); + return stream::read(begin, dist); + } + + template< + typename Iter, + std::enable_if_t>::value_type, + byte>, + bool> = true> + constexpr stream& read(Iter&& begin, std::size_t dist) + { + if (size() < dist) { + throw std::runtime_error{"tried to read past end of stream"}; + } + + if (view_ != nullptr) { + std::copy(view_ + pos_, view_ + pos_ + dist, begin); + } else { + std::copy(store_.begin() + pos_, store_.begin() + pos_ + dist, begin); + } + pos_ += dist; + return *this; + } + byte* copy_buffer() const + { + const auto remaining = size(); + byte* raw = static_cast(malloc(remaining)); + if (view_ != nullptr) { + std::copy(view_ + pos_, view_ + view_size_, raw); + } else { + std::copy(store_.begin() + pos_, store_.end(), raw); + } + return raw; + } + std::size_t size() const + { + return view_ != nullptr ? view_size_ - pos_ : store_.size() - pos_; + } + +private: + void compact() + { + if (view_ != nullptr) { + store_.assign(view_ + pos_, view_ + view_size_); + view_ = nullptr; + view_size_ = 0; + pos_ = 0; + return; + } + + if (pos_ == 0) { + return; + } else { + store_.erase(store_.begin(), store_.begin() + pos_); + } + pos_ = 0; + } + + std::vector store_; + const byte* view_ = nullptr; + std::size_t view_size_ = 0; + std::size_t pos_ = 0; +}; + +template<> +struct serializer : public std::true_type { + static void serialize(const cv::Mat& m, stream& out) + { + out << m.type() << m.rows << m.cols; + out.write(m.datastart, m.dataend); + } +}; + +template<> +struct deserializer : public std::true_type { + static cv::Mat deserialize(stream& in) + { + int rows, cols, type; + in >> type >> rows >> cols; + cv::Mat m; + m.create(rows, cols, type); + in.read(m.data, std::distance(m.datastart, m.dataend)); + return m; + } +}; + +template +struct deserializer> : public std::true_type { + static cv::Point2f deserialize(stream& in) + { + cv::Point_ p; + in >> p.x >> p.y; + return p; + } +}; +template +struct serializer> : public std::true_type { + static void serialize(const cv::Point_& pt, stream& out) + { + out << pt.x << pt.y; + } +}; + +template<> +struct serializer : public std::true_type { + static void serialize(const cv::KeyPoint& pt, stream& out) + { + out << pt.class_id << pt.angle << pt.octave << pt.response << pt.size + << pt.pt; + } +}; + +template<> +struct deserializer : public std::true_type { + static cv::KeyPoint deserialize(stream& in) + { + cv::KeyPoint pt; + in >> pt.class_id >> pt.angle >> pt.octave >> pt.response >> pt.size >> + pt.pt; + return pt; + } +}; + +template +struct serializer> : public std::true_type { + static void serialize(const std::vector& vs, stream& out) + { + out << static_cast(vs.size()); + std::for_each(vs.begin(), vs.end(), + [&out](const auto& el) { out << el; }); + } +}; + +template +struct deserializer> : public std::true_type { + static std::vector deserialize(stream& in) + { + std::size_t size; + in >> size; + std::vector vs; + vs.reserve(size); + for (std::size_t n = 0; n != size; ++n) { + T v; + in >> v; + vs.emplace_back(std::move(v)); + } + return vs; + } +}; +} // namespace bin diff --git a/sigfm/img-info.hpp b/sigfm/img-info.hpp new file mode 100644 index 000000000..bf270ecba --- /dev/null +++ b/sigfm/img-info.hpp @@ -0,0 +1,10 @@ + +#pragma once + +#include +#include + +struct SigfmImgInfo { + std::vector keypoints; + cv::Mat descriptors; +}; \ No newline at end of file diff --git a/sigfm/sigfm.cpp b/sigfm/sigfm.cpp new file mode 100644 index 000000000..7216bd9c8 --- /dev/null +++ b/sigfm/sigfm.cpp @@ -0,0 +1,334 @@ +// SIGFM algorithm for libfprint + +// Copyright (C) 2022 Matthieu CHARETTE +// Copyright (c) 2022 Natasha England-Elbro +// Copyright (c) 2022 Timur Mangliev + +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. + +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. + +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +#include "sigfm.hpp" +#include "binary.hpp" +#include "img-info.hpp" + +#include "opencv2/core/persistence.hpp" +#include "opencv2/core/types.hpp" +#include "opencv2/features2d.hpp" +#include "opencv2/imgcodecs.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { +constexpr std::size_t serialized_keypoint_size = sizeof(int) * 2 + sizeof(float) * 5; +constexpr std::size_t max_serialized_keypoints = 2048; +constexpr int sift_descriptor_cols = 128; +constexpr int sift_descriptor_type = CV_32F; +} // namespace + +namespace bin { + +template<> +struct serializer : public std::true_type { + static void serialize(const SigfmImgInfo& info, stream& out) + { + out << info.keypoints << info.descriptors; + } +}; + +template<> +struct deserializer : public std::true_type { + static SigfmImgInfo deserialize(stream& in) + { + SigfmImgInfo info; + + std::size_t keypoint_count; + in >> keypoint_count; + if (keypoint_count > max_serialized_keypoints || + keypoint_count > in.size() / serialized_keypoint_size) { + throw std::runtime_error{"invalid SIGFM keypoint count"}; + } + + info.keypoints.reserve(keypoint_count); + for (std::size_t i = 0; i < keypoint_count; i++) { + cv::KeyPoint keypoint; + in >> keypoint; + info.keypoints.emplace_back(std::move(keypoint)); + } + + int type, rows, cols; + in >> type >> rows >> cols; + if (type != sift_descriptor_type || rows < 0 || cols != sift_descriptor_cols || + static_cast(rows) != keypoint_count) { + throw std::runtime_error{"invalid SIGFM descriptor metadata"}; + } + + const auto descriptor_bytes = keypoint_count * sift_descriptor_cols * sizeof(float); + if (descriptor_bytes > in.size()) { + throw std::runtime_error{"invalid SIGFM descriptor data"}; + } + + info.descriptors.create(rows, cols, type); + in.read(info.descriptors.data, descriptor_bytes); + return info; + } +}; +} // namespace bin + +namespace { +constexpr auto distance_match = 0.85; +constexpr auto length_match = 0.05; +constexpr auto angle_match = 0.05; +constexpr auto min_match = 5; +constexpr auto sift_nfeatures = 0; +constexpr auto sift_octave_layers = 3; +constexpr auto sift_contrast_threshold = 0.04; +constexpr auto sift_edge_threshold = 18.0; +constexpr auto sift_sigma = 2.0; +struct match { + cv::Point2i p1; + cv::Point2i p2; + match(cv::Point2i ip1, cv::Point2i ip2) : p1{ip1}, p2{ip2} {} + match() : p1{cv::Point2i(0, 0)}, p2{cv::Point2i(0, 0)} {} + bool operator==(const match& right) const + { + return std::tie(this->p1, this->p2) == std::tie(right.p1, right.p2); + } + bool operator<(const match& right) const + { + return std::tie(this->p1.y, this->p1.x, this->p2.y, this->p2.x) < + std::tie(right.p1.y, right.p1.x, right.p2.y, right.p2.x); + } +}; +struct angle { + double cos; + double sin; + match corr_matches[2]; + angle(double cos_, double sin_, match m1, match m2) + : cos{cos_}, sin{sin_}, corr_matches{m1, m2} + { + } +}; +} // namespace + +SigfmImgInfo* sigfm_copy_info(SigfmImgInfo* info) { return new SigfmImgInfo{*info}; } + +int sigfm_keypoints_count(SigfmImgInfo* info) +{ + /* sigfm_extract() reports failure with nullptr and the C callers in + * goodix53x5-match.c hand the result straight to this function before any + * null check, so treat it as "no keypoints" rather than dereferencing. */ + if (info == nullptr) { + return 0; + } + return info->keypoints.size(); +} + +unsigned char* sigfm_serialize_binary(SigfmImgInfo* info, int* outlen) +{ + bin::stream s; + s << *info; + *outlen = s.size(); + return s.copy_buffer(); +} + +SigfmImgInfo* sigfm_deserialize_binary(const unsigned char* bytes, int len) +{ + if (bytes == nullptr || len <= 0) { + return nullptr; + } + + try { + bin::stream s{bytes, bytes + len}; + auto info = std::make_unique(); + s >> *info; + if (s.size() != 0) { + return nullptr; + } + return info.release(); + } + catch (const std::exception&) { + return nullptr; + } +} + +SigfmImgInfo* sigfm_extract(const SigfmPix* pix, int width, int height) +{ + /* cv::Mat::create() accepts non-positive dimensions without complaint and + * leaves the Mat in a state where the memcpy below corrupts the heap; the + * throw only surfaces later, inside CLAHE. Reject the dimensions up front + * rather than relying on OpenCV to catch them. */ + if (pix == nullptr || width <= 0 || height <= 0) { + return nullptr; + } + + /* This function is called across the C ABI from the driver's C state-machine + * handlers (via goodix_match_extract()). An OpenCV cv::Exception or a + * std::bad_alloc unwinding through a C stack frame is undefined behaviour + * and reaches std::terminate(), killing the root fprintd process. Report + * failure with nullptr instead, matching sigfm_match_score() below. */ + try { + cv::Mat img; + img.create(height, width, CV_8UC1); + std::memcpy(img.data, pix, (std::size_t) width * (std::size_t) height); + + /* Apply CLAHE to enhance local contrast for better SIFT detection */ + auto clahe = cv::createCLAHE(4.0, cv::Size(4, 4)); + cv::Mat enhanced; + clahe->apply(img, enhanced); + + const auto roi = cv::Mat::ones(cv::Size{enhanced.size[1], enhanced.size[0]}, CV_8UC1); + std::vector pts; + + cv::Mat descs; + cv::SIFT::create(sift_nfeatures, + sift_octave_layers, + sift_contrast_threshold, + sift_edge_threshold, + sift_sigma) + ->detectAndCompute(enhanced, roi, pts, descs); + + auto* info = new SigfmImgInfo{pts, descs}; + return info; + } + catch (...) { + return nullptr; + } +} + +int sigfm_match_score(SigfmImgInfo* frame, SigfmImgInfo* enrolled) +{ + try { + std::vector> points; + auto bfm = cv::BFMatcher::create(); + bfm->knnMatch(frame->descriptors, enrolled->descriptors, points, 2); + std::vector candidate_positions(enrolled->descriptors.rows, -1); + std::vector candidate_indices; + cv::Mat candidate_descriptors; + + for (const auto& pts : points) { + if (pts.size() < 2) { + continue; + } + + const cv::DMatch& match_1 = pts.at(0); + if (match_1.distance < distance_match * pts.at(1).distance && + candidate_positions[match_1.trainIdx] < 0) { + candidate_positions[match_1.trainIdx] = candidate_indices.size(); + candidate_indices.push_back(match_1.trainIdx); + candidate_descriptors.push_back(enrolled->descriptors.row(match_1.trainIdx)); + } + } + + if (candidate_indices.size() < min_match) { + return 0; + } + + std::vector> backward; + bfm->knnMatch(candidate_descriptors, frame->descriptors, backward, 1); + std::set matches_unique; + int nb_matched = 0; + for (const auto& pts : points) { + if (pts.size() < 2) { + continue; + } + const cv::DMatch& match_1 = pts.at(0); + if (match_1.distance < distance_match * pts.at(1).distance) { + const int candidate_position = candidate_positions[match_1.trainIdx]; + if (candidate_position < 0 || backward[candidate_position].empty() || + backward[candidate_position][0].trainIdx != match_1.queryIdx) { + continue; + } + + matches_unique.emplace( + match{frame->keypoints.at(match_1.queryIdx).pt, + enrolled->keypoints.at(match_1.trainIdx).pt}); + nb_matched++; + } + } + if (nb_matched < min_match) { + return 0; + } + std::vector matches{matches_unique.begin(), + matches_unique.end()}; + + std::vector angles; + for (std::size_t j = 0; j < matches.size(); j++) { + match match_1 = matches[j]; + for (std::size_t k = j + 1; k < matches.size(); k++) { + match match_2 = matches[k]; + + int vec_1[2] = {match_1.p1.x - match_2.p1.x, + match_1.p1.y - match_2.p1.y}; + int vec_2[2] = {match_1.p2.x - match_2.p2.x, + match_1.p2.y - match_2.p2.y}; + + double length_1 = sqrt(pow(vec_1[0], 2) + pow(vec_1[1], 2)); + double length_2 = sqrt(pow(vec_2[0], 2) + pow(vec_2[1], 2)); + + if (1 - std::min(length_1, length_2) / + std::max(length_1, length_2) <= + length_match) { + + double product = length_1 * length_2; + angles.emplace_back(angle( + M_PI / 2 + + asin((vec_1[0] * vec_2[0] + vec_1[1] * vec_2[1]) / + product), + acos((vec_1[0] * vec_2[1] - vec_1[1] * vec_2[0]) / + product), + match_1, match_2)); + } + } + } + + if (angles.size() < min_match) { + return 0; + } + + int count = 0; + for (std::size_t j = 0; j < angles.size(); j++) { + angle angle_1 = angles[j]; + for (std::size_t k = j + 1; k < angles.size(); k++) { + angle angle_2 = angles[k]; + + if (1 - std::min(angle_1.sin, angle_2.sin) / + std::max(angle_1.sin, angle_2.sin) <= + angle_match && + 1 - std::min(angle_1.cos, angle_2.cos) / + std::max(angle_1.cos, angle_2.cos) <= + angle_match) { + + count += 1; + } + } + } + return count; + } + catch (...) { + return -1; + } +} + +void sigfm_free_info(SigfmImgInfo* info) { delete info; } diff --git a/sigfm/sigfm.hpp b/sigfm/sigfm.hpp new file mode 100644 index 000000000..671a2b6df --- /dev/null +++ b/sigfm/sigfm.hpp @@ -0,0 +1,98 @@ +// SIGFM algorithm for libfprint + +// Copyright (C) 2022 Matthieu CHARETTE +// Copyright (c) 2022 Natasha England-Elbro +// Copyright (c) 2022 Timur Mangliev + +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. + +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. + +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif +typedef unsigned char SigfmPix; +/** + * @brief Contains information used by the sigfm algorithm for matching + * @details Get one from sigfm_extract() and make sure to clean it up with sigfm_free_info() + * @struct SigfmImgInfo + */ +typedef struct SigfmImgInfo SigfmImgInfo; + +/** + * @brief Extracts information from an image for later use sigfm_match_score + * + * @param pix Pixels of the image must be width * height in length + * @param width Width of the image + * @param height Height of the image + * @return SigfmImgInfo* Info that can be used with the API + */ +SigfmImgInfo* sigfm_extract(const SigfmPix* pix, int width, int height); + +/** + * @brief Destroy an SigfmImgInfo + * @warning Call this instead of free() or you will get UB! + * @param info SigfmImgInfo to destroy + */ +void sigfm_free_info(SigfmImgInfo* info); + +/** + * @brief Score how closely a frame matches another + * + * @param frame Print to be checked + * @param enrolled Canonical print to verify against + * @return int Score of how closely they match, values <0 indicate error, 0 means always reject + */ +int sigfm_match_score(SigfmImgInfo* frame, SigfmImgInfo* enrolled); + +/** + * @brief Serialize an image info for storage + * + * @param info SigfmImgInfo to store + * @param outlen output: Length of the returned byte array + * @return unsigned* char byte array for storage, should be free'd by the callee + */ +unsigned char* sigfm_serialize_binary(SigfmImgInfo* info, int* outlen); +/** + * @brief Deserialize an SigfmImgInfo from storage + * + * @param bytes Byte array to deserialize from + * @param len Length of the byte array + * @return SigfmImgInfo* Deserialized info, or NULL if deserialization failed + */ +SigfmImgInfo* sigfm_deserialize_binary(const unsigned char* bytes, int len); + +/** + * @brief Keypoints for an image. Low keypoints generally means the image is + * low quality for matching + * + * @param info + * @return int + */ + +int sigfm_keypoints_count(SigfmImgInfo* info); + +/** + * @brief Copy an SigfmImgInfo + * + * @param info Source of copy + * @return SigfmImgInfo* Newly allocated and copied version of info + */ +SigfmImgInfo* sigfm_copy_info(SigfmImgInfo* info); + +#ifdef __cplusplus +} +#endif From da0853cb8bec8f711565b0d8471a092e0ac86727 Mon Sep 17 00:00:00 2001 From: daemonhorn Date: Sun, 23 Aug 2026 07:12:36 -0400 Subject: [PATCH 05/12] meson: wire OpenCV/SIGFM build only for goodix533c Add a 'sigfm' driver_helper (goodix533c -> ['goodixtls', 'sigfm']) using the existing driver_helper_mapping/optional_deps convention (same pattern already used for openssl/threads via the 'goodixtls' helper), so OpenCV is resolved and libsigfm only gets declared/linked when -Ddrivers includes goodix533c -- never unconditionally for the whole libfprint build. Tries opencv5 first, falls back to opencv4, and links only the specific opencv_core/opencv_features2d(-or-opencv_features on OpenCV 5)/opencv_flann/opencv_imgproc modules libsigfm actually needs, per AndyHazz's proven meson-integration-goodix533c.patch snippet (adapted here to be conditional rather than unconditional, and to declare libsigfm from the top-level meson.build so its 'sigfm/sigfm.cpp' source path resolves against the repo root where sigfm/ actually lives). libsigfm is threaded into libfprint_drivers, the final libfprint shared library, and libfprint_private_dep (which several test/tool executables consume via `dependencies:` rather than `link_with:`), all guarded by the same have_sigfm flag, so goodix533c-capture-test and fprint-list-udev-hwdb keep linking correctly now that goodix533c-match.c pulls in sigfm_* symbols. --- libfprint/meson.build | 34 +++++++++++++++++++++++++++++---- meson.build | 44 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/libfprint/meson.build b/libfprint/meson.build index 35fcc28cf..32b2ca38d 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -142,7 +142,10 @@ driver_sources = { 'goodixtls511' : [ 'drivers/goodixtls/goodix511.c' ], 'goodix533c' : - [ 'drivers/goodix533c/goodix533c.c' ], + [ 'drivers/goodix533c/goodix533c.c', + 'drivers/goodix533c/goodix533c-match.c', + 'drivers/goodix533c/goodix533c-enroll.c', + 'drivers/goodix533c/goodix533c-auth.c' ], 'fpcmoc' : [ 'drivers/fpcmoc/fpc.c' ], } @@ -160,6 +163,8 @@ helper_sources = { [ ], 'udev' : [ ], + 'sigfm' : + [ ], 'virtual' : [ 'drivers/virtual-device-listener.c' ], } @@ -261,16 +266,32 @@ libfprint_private = static_library('fprint-private', link_with: libnbis, install: false) +# libsigfm (declared in the top-level meson.build, only when a driver that +# needs it -- currently just goodix533c -- is enabled) has to be linked +# wherever driver code that calls into it ends up: the static driver +# archive, the final shared library, and (via libfprint_private_dep below) +# every executable that links libfprint_drivers directly, such as +# goodix533c-capture-test and fprint-list-udev-hwdb. +libfprint_drivers_link_with = [libfprint_private] +if have_sigfm + libfprint_drivers_link_with += libsigfm +endif + libfprint_drivers = static_library('fprint-drivers', sources: drivers_sources, c_args: drivers_cflags, dependencies: deps, - link_with: libfprint_private, + link_with: libfprint_drivers_link_with, install: false) mapfile = files('libfprint.ver') vflag = '-Wl,--version-script,@0@/@1@'.format(meson.source_root(), mapfile[0]) +libfprint_link_with = [libfprint_drivers, libfprint_private] +if have_sigfm + libfprint_link_with += libsigfm +endif + libfprint = shared_library(versioned_libname.split('lib')[1], sources: [ fp_enums, @@ -280,7 +301,7 @@ libfprint = shared_library(versioned_libname.split('lib')[1], version: libversion, link_args : vflag, link_depends : mapfile, - link_with: [libfprint_drivers, libfprint_private], + link_with: libfprint_link_with, dependencies: deps, install: true) @@ -298,9 +319,14 @@ install_headers(['fprint.h'] + libfprint_public_headers, subdir: versioned_libname ) +libfprint_private_dep_link_with = [libfprint_private] +if have_sigfm + libfprint_private_dep_link_with += libsigfm +endif + libfprint_private_dep = declare_dependency( include_directories: include_directories('.'), - link_with: libfprint_private, + link_with: libfprint_private_dep_link_with, dependencies: [ deps, libfprint_dep, diff --git a/meson.build b/meson.build index 73ef903af..44fe6068f 100644 --- a/meson.build +++ b/meson.build @@ -161,7 +161,7 @@ driver_helper_mapping = { 'uru4000' : [ 'nss' ], 'elanspi' : [ 'udev' ], 'goodixtls511' : [ 'goodixtls' ], - 'goodix533c' : [ 'goodixtls' ], + 'goodix533c' : [ 'goodixtls', 'sigfm' ], 'virtual_image' : [ 'virtual' ], 'virtual_device' : [ 'virtual' ], 'virtual_device_storage' : [ 'virtual' ], @@ -200,6 +200,12 @@ install_udev_rules = udev_rules.enabled() optional_deps = [] +# Set (and libsigfm declared) only when the 'sigfm' helper below actually +# runs, i.e. only when a driver that needs it (goodix533c) is enabled -- +# OpenCV must never become a dependency of the whole libfprint build just +# because *some* driver happens to use SIGFM matching. +have_sigfm = false + # Resolve extra dependencies foreach i : driver_helpers foreach d, helpers : driver_helper_mapping @@ -247,6 +253,42 @@ foreach i : driver_helpers libfprint_conf.set10('HAVE_UDEV', true) optional_deps += gudev_dep + elif i == 'sigfm' + have_sigfm = true + + # SIGFM: SIFT-based fingerprint matching for small sensors (used by + # goodix533c's enroll/verify/identify). Use pkg-config only for the + # include path; link only the specific OpenCV modules needed (the + # full opencv pkg-config pulls in modules like viz/hdf that can have + # missing transitive deps on some distros). + opencv_pc = dependency('opencv5', required: false) + if not opencv_pc.found() + opencv_pc = dependency('opencv4', required: false) + endif + if not opencv_pc.found() + error('opencv (opencv5 or opencv4) is required for @0@ and possibly others'.format(driver)) + endif + opencv_includes = opencv_pc.partial_dependency(compile_args: true, includes: true) + + opencv_core = cpp.find_library('opencv_core') + # OpenCV 5 renamed the features2d module to features. + opencv_features2d = cpp.find_library('opencv_features2d', required: false) + if not opencv_features2d.found() + opencv_features2d = cpp.find_library('opencv_features') + endif + opencv_flann = cpp.find_library('opencv_flann') + opencv_imgproc = cpp.find_library('opencv_imgproc') + + opencv_dep = declare_dependency( + dependencies: [opencv_includes, opencv_core, opencv_features2d, opencv_flann, opencv_imgproc], + ) + optional_deps += opencv_dep + + libsigfm = static_library('sigfm', + 'sigfm/sigfm.cpp', + dependencies: [opencv_dep], + cpp_args: ['-std=c++17'], + install: false) endif endforeach From 922c4209b1b4566feef88b7a62b3a737455d8d7f Mon Sep 17 00:00:00 2001 From: daemonhorn Date: Sun, 23 Aug 2026 07:13:03 -0400 Subject: [PATCH 06/12] goodix533c: wire real SIGFM enroll/verify/identify/cancel Implements FpDeviceClass's enroll, verify, identify, and cancel vfuncs, architected after AndyHazz's goodix53x5 driver (same SIGFM approach, same sensor resolution) but built entirely on this driver's own already-proven transport/crypto/capture code -- no goodix53x5 protocol code is reused, only the shape of its enroll/auth/match/scan modules. New files: - goodix533c-private.h: shared device struct (moved out of goodix533c.c) and sub-SSM entry-point declarations, so match/enroll/auth stay decoupled from the wire protocol. - goodix533c-match.{c,h}: driver-side wrapper around sigfm/sigfm.hpp (extract/serialize/deserialize/score), with this driver's own "G533"/v1 template magic (distinct from goodix53x5's "G53S" -- templates are never interchangeable, the preprocessing pipelines differ). - goodix533c-enroll.c/.h: enroll FpiSsm -- capture-reference -> wait-finger -> capture -> process (quality gates: reject low keypoint count via FP_DEVICE_RETRY_REMOVE_FINGER, reject excessive clipped/non-contact fraction via FP_DEVICE_RETRY_CENTER_FINGER, both without advancing the stage) -> wait-finger-up -> next-stage-or-done, repeated GOODIX533C_ENROLL_SAMPLES (8) times. Stores one GVariant "aay" of serialized SIGFM templates as the print's fpi-data. - goodix533c-auth.c/.h: shared verify/identify FpiSsm (dispatches on fpi_device_get_current_action()), same capture shape plus a match step; verify checks fpi_device_get_verify_data()'s stored samples, identify checks every sample of every fpi_device_get_identify_data() gallery print, tracking the best score. Accepts at GOODIX533C_SIGFM_BEST_MIN (150). Ports the queued-report pattern (goodix533c_queue_*_report / goodix533c_flush_pending_result_report) so a match verdict isn't flushed to libfprint until finger-up cleanup has run. Refactor of goodix533c.c: the single-shot capture_run() SSM used by fpi_device_goodix533c_capture_test() is split along a session-scoped vs. attempt-scoped line -- - Session-scoped (USB reset, TLS-PSK handshake, config upload, FDT threshold-template baseline) moves into open_run(), extended with OPEN_STAGE_RESET..OPEN_STAGE_FDT_BASELINE. This must happen once per fp_device_open() session, not once per enroll stage, or an 8-sample enrollment would mean 8 full USB re-handshakes instead of 8 fast touches. - Attempt-scoped (reference capture, finger wait, live capture, finger up) becomes four reusable sub-SSM starters declared in goodix533c-private.h: goodix533c_start_ref_capture_subsm(), _start_finger_wait_subsm(), _start_live_capture_subsm(), _start_finger_up_subsm(). fpi_device_goodix533c_capture_test() now just chains all four (regression-safe, same wire sequence as before); enroll/auth chain them their own way. - The live-capture sub-SSM gained a PROCESS state doing the flat-field-against-reference + clipped-fraction quality-metric work that capture_done() used to do only after the whole sequence finished -- enroll/verify need self->captured_image and self->captured_clipped_fraction ready immediately, before waiting for finger-up, not deferred to a final callback. - "Finger up" reuses mcu_switch_to_fdt_up as-is (hardware-verified this session to block until lift-off) rather than adding new detection logic, but its reply handler now tolerates a bare timeout as "assume lifted" instead of failing the whole action -- a single slow lift-off should not fail an 8-stage enrollment. - dev_class->features now comes from fpi_device_class_auto_initialize_features() (matching goodixtls511's convention) instead of a hand-set FP_DEVICE_FEATURE_CAPTURE, which was never backed by a real dev_class->capture vfunc to begin with. See the session report for the full list of deliberate deviations from the goodix53x5 reference shape (no REINIT/suspend story, no separate "deactivate" cleanup, GOODIX533C_RAW12_CLIP gate likely inert at this driver's headroom-safe gain) and what remains unverified against real hardware (the enroll-then-verify success path, which needs a human). --- .../drivers/goodix533c/goodix533c-auth.c | 491 ++++++++++++ .../drivers/goodix533c/goodix533c-auth.h | 33 + .../drivers/goodix533c/goodix533c-enroll.c | 234 ++++++ .../drivers/goodix533c/goodix533c-enroll.h | 30 + .../drivers/goodix533c/goodix533c-match.c | 161 ++++ .../drivers/goodix533c/goodix533c-match.h | 54 ++ .../drivers/goodix533c/goodix533c-private.h | 175 +++++ libfprint/drivers/goodix533c/goodix533c.c | 733 +++++++++++++----- 8 files changed, 1706 insertions(+), 205 deletions(-) create mode 100644 libfprint/drivers/goodix533c/goodix533c-auth.c create mode 100644 libfprint/drivers/goodix533c/goodix533c-auth.h create mode 100644 libfprint/drivers/goodix533c/goodix533c-enroll.c create mode 100644 libfprint/drivers/goodix533c/goodix533c-enroll.h create mode 100644 libfprint/drivers/goodix533c/goodix533c-match.c create mode 100644 libfprint/drivers/goodix533c/goodix533c-match.h create mode 100644 libfprint/drivers/goodix533c/goodix533c-private.h diff --git a/libfprint/drivers/goodix533c/goodix533c-auth.c b/libfprint/drivers/goodix533c/goodix533c-auth.c new file mode 100644 index 000000000..ab49ac8c2 --- /dev/null +++ b/libfprint/drivers/goodix533c/goodix533c-auth.c @@ -0,0 +1,491 @@ +/* + * Goodix 27c6:533c native driver for libfprint — Verify/identify flow + * + * Copyright (C) 2026 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* + * Verify/identify SSM shape and the queued-report pattern ported + * near-verbatim from goodix53x5-auth.c (sibling driver, same SIGFM + * approach). Two deliberate deviations from that reference, both because + * this driver has no equivalent of goodix53x5's EC-power-controlled + * "deactivate" primitive (a bounded sleep+EC-off cleanup that skips waiting + * for lift-off on a successful match): + * + * - GOODIX_VERIFY_FINISH there branches between waiting for finger-up and + * a cheap deactivate; here FINISH always waits for finger-up + * (goodix533c_start_finger_up_subsm()), matching what the single-capture + * test harness already does unconditionally on real hardware. Inventing + * a "skip cleanup on success" shortcut for a command sequence never + * exercised that way would be an unverified behavioral change to + * already-proven hardware interaction; a successful verify simply takes + * a little longer (until the user's own finger lift, which they were + * going to do anyway). + * - There is no REINIT/REINIT_DONE pair -- this driver has no suspend()/ + * resume() story yet, so there is nothing to reinitialize before an + * action. + */ + +#define FP_COMPONENT "goodix533c" + +#include "drivers_api.h" +#include "goodix533c-private.h" +#include "goodix533c-match.h" +#include "goodix533c-auth.h" + +#include + +static gboolean +goodix533c_match_scores_need_exhaustive_logging (void) +{ + return !g_log_writer_default_would_drop (G_LOG_LEVEL_DEBUG, G_LOG_DOMAIN); +} + +static gboolean +goodix533c_gallery_has_single_username (GPtrArray *gallery) +{ + const gchar *username; + + if (gallery->len == 0) + return FALSE; + + username = fp_print_get_username (g_ptr_array_index (gallery, 0)); + if (username == NULL || username[0] == '\0') + return FALSE; + + for (guint i = 1; i < gallery->len; i++) + { + FpPrint *print = g_ptr_array_index (gallery, i); + + if (g_strcmp0 (username, fp_print_get_username (print)) != 0) + return FALSE; + } + + return TRUE; +} + +typedef enum { + GOODIX533C_VERIFY_CAPTURE_REF = 0, + GOODIX533C_VERIFY_WAIT_FINGER, + GOODIX533C_VERIFY_CAPTURE, + GOODIX533C_VERIFY_MATCH, + GOODIX533C_VERIFY_FINISH, + GOODIX533C_VERIFY_NUM_STATES, +} Goodix533cVerifyState; + +void +goodix533c_clear_pending_result_report (FpiDeviceGoodix533c *self) +{ + self->pending_result_report = FALSE; + self->pending_result_action = 0; + self->pending_verify_result = 0; + g_clear_object (&self->pending_identify_match); + g_clear_error (&self->pending_result_error); + g_clear_error (&self->pending_action_error); +} + +static void +goodix533c_queue_action_error (FpiDeviceGoodix533c *self, + GError *error) +{ + goodix533c_clear_pending_result_report (self); + + self->pending_action_error = error; +} + +static void +goodix533c_queue_verify_report (FpiDeviceGoodix533c *self, + FpiMatchResult result, + GError *error) +{ + goodix533c_clear_pending_result_report (self); + + self->pending_result_report = TRUE; + self->pending_result_action = FPI_DEVICE_ACTION_VERIFY; + self->pending_verify_result = result; + self->pending_result_error = error; +} + +static void +goodix533c_queue_identify_report (FpiDeviceGoodix533c *self, + FpPrint *match, + GError *error) +{ + goodix533c_clear_pending_result_report (self); + + self->pending_result_report = TRUE; + self->pending_result_action = FPI_DEVICE_ACTION_IDENTIFY; + if (match != NULL) + self->pending_identify_match = g_object_ref (match); + self->pending_result_error = error; +} + +static void +goodix533c_flush_pending_result_report (FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + + if (!self->pending_result_report) + return; + + if (self->pending_result_action == FPI_DEVICE_ACTION_IDENTIFY) + { + g_autoptr(FpPrint) match = g_steal_pointer (&self->pending_identify_match); + + fpi_device_identify_report (dev, match, NULL, + g_steal_pointer (&self->pending_result_error)); + } + else + { + fpi_device_verify_report (dev, self->pending_verify_result, NULL, + g_steal_pointer (&self->pending_result_error)); + } + + self->pending_result_report = FALSE; + self->pending_result_action = 0; + self->pending_verify_result = 0; +} + +static void +goodix533c_verify_ssm_handler (FpiSsm *ssm, + FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case GOODIX533C_VERIFY_CAPTURE_REF: + goodix533c_start_ref_capture_subsm (ssm, dev); + break; + + case GOODIX533C_VERIFY_WAIT_FINGER: + goodix533c_start_finger_wait_subsm (ssm, dev, NULL, NULL); + break; + + case GOODIX533C_VERIFY_CAPTURE: + goodix533c_start_live_capture_subsm (ssm, dev); + break; + + case GOODIX533C_VERIFY_MATCH: + { + FpiDeviceAction action = fpi_device_get_current_action (dev); + GoodixMatchInfo *probe_info; + int keypoints; + + /* Extract SIFT features once for both identify and verify paths. */ + probe_info = goodix533c_match_extract (self->captured_image); + keypoints = goodix533c_match_keypoints_count (probe_info); + + if (keypoints < GOODIX533C_MIN_CAPTURE_KEYPOINTS) + { + if (action == FPI_DEVICE_ACTION_IDENTIFY) + { + goodix533c_queue_identify_report (self, NULL, + fpi_device_retry_new (FP_DEVICE_RETRY_REMOVE_FINGER)); + } + else + { + goodix533c_queue_verify_report (self, FPI_MATCH_ERROR, + fpi_device_retry_new (FP_DEVICE_RETRY_REMOVE_FINGER)); + } + + self->verify_wait_finger_up = TRUE; + goodix533c_match_free_info (probe_info); + g_clear_pointer (&self->captured_image, g_free); + fpi_ssm_next_state (ssm); + return; + } + + if (action == FPI_DEVICE_ACTION_IDENTIFY) + { + /* Identify: match against gallery of enrolled prints. */ + GPtrArray *gallery = NULL; + FpPrint *match = NULL; + int best_score = 0; + int best_match_score = 0; + int valid_templates = 0; + gboolean saw_unusable_template = FALSE; + gboolean stop_after_match; + + fpi_device_get_identify_data (dev, &gallery); + stop_after_match = + !goodix533c_match_scores_need_exhaustive_logging () && + goodix533c_gallery_has_single_username (gallery); + + for (guint i = 0; i < gallery->len; i++) + { + FpPrint *tmpl = g_ptr_array_index (gallery, i); + GVariant *tmpl_data = NULL; + GVariantIter iter; + GVariant *child; + int sample_idx = 0; + int tmpl_best_score = 0; + + g_object_get (G_OBJECT (tmpl), "fpi-data", &tmpl_data, NULL); + if (tmpl_data == NULL) + continue; + + g_variant_iter_init (&iter, tmpl_data); + while ((child = g_variant_iter_next_value (&iter))) + { + gsize len; + const guint8 *feature; + + feature = g_variant_get_fixed_array (child, &len, 1); + if (len > 0) + { + int score; + Goodix533cSigfmTemplateStatus template_status; + + template_status = goodix533c_match_serialized_feature (probe_info, + feature, + len, + &score); + if (template_status != GOODIX533C_SIGFM_TEMPLATE_OK) + { + saw_unusable_template = TRUE; + + fp_dbg ("identify: gallery[%u] sample %d invalid SIGFM template", + i, sample_idx); + sample_idx++; + g_variant_unref (child); + continue; + } + + valid_templates++; + fp_dbg ("identify: gallery[%u] sample %d sigfm_score %d", + i, sample_idx, score); + + if (score > tmpl_best_score) + tmpl_best_score = score; + + sample_idx++; + + if (stop_after_match && + tmpl_best_score >= GOODIX533C_SIGFM_BEST_MIN) + { + g_variant_unref (child); + break; + } + } + g_variant_unref (child); + } + g_variant_unref (tmpl_data); + + if (tmpl_best_score > best_score) + best_score = tmpl_best_score; + + if (tmpl_best_score >= GOODIX533C_SIGFM_BEST_MIN && + tmpl_best_score > best_match_score) + { + best_match_score = tmpl_best_score; + match = tmpl; + } + + if (stop_after_match && match != NULL) + break; + } + + fp_dbg ("Identify best SIGFM score: %d (best_min: %d)", + best_score, GOODIX533C_SIGFM_BEST_MIN); + + if (valid_templates == 0 && saw_unusable_template) + { + goodix533c_queue_action_error (self, + fpi_device_error_new (FP_DEVICE_ERROR_DATA_INVALID)); + self->verify_wait_finger_up = FALSE; + } + else if (match != NULL) + { + goodix533c_queue_identify_report (self, match, NULL); + self->verify_wait_finger_up = FALSE; + } + else + { + goodix533c_queue_identify_report (self, NULL, NULL); + self->verify_wait_finger_up = TRUE; + } + } + else + { + /* Verify: match against single enrolled print. */ + FpPrint *print = NULL; + GVariant *data = NULL; + int best_score = 0; + int sample_idx = 0; + int valid_templates = 0; + gboolean saw_unusable_template = FALSE; + gboolean score_all_templates = + goodix533c_match_scores_need_exhaustive_logging (); + + fpi_device_get_verify_data (dev, &print); + g_object_get (G_OBJECT (print), "fpi-data", &data, NULL); + + if (data != NULL) + { + GVariantIter iter; + GVariant *child; + + g_variant_iter_init (&iter, data); + while ((child = g_variant_iter_next_value (&iter))) + { + gsize len; + const guint8 *feature; + + feature = g_variant_get_fixed_array (child, &len, 1); + if (len > 0) + { + int score; + Goodix533cSigfmTemplateStatus template_status; + + template_status = goodix533c_match_serialized_feature (probe_info, + feature, + len, + &score); + if (template_status != GOODIX533C_SIGFM_TEMPLATE_OK) + { + saw_unusable_template = TRUE; + + fp_dbg ("verify: sample %d invalid SIGFM template", + sample_idx); + sample_idx++; + g_variant_unref (child); + continue; + } + + valid_templates++; + fp_dbg ("verify: sample %d sigfm_score %d", + sample_idx, score); + + if (score > best_score) + best_score = score; + + sample_idx++; + + if (!score_all_templates && + best_score >= GOODIX533C_SIGFM_BEST_MIN) + { + g_variant_unref (child); + break; + } + } + g_variant_unref (child); + } + g_variant_unref (data); + } + + fp_dbg ("Verify best SIGFM score: %d (best_min: %d)", + best_score, GOODIX533C_SIGFM_BEST_MIN); + + if (valid_templates == 0 && saw_unusable_template) + { + goodix533c_queue_action_error (self, + fpi_device_error_new (FP_DEVICE_ERROR_DATA_INVALID)); + self->verify_wait_finger_up = FALSE; + } + else if (best_score >= GOODIX533C_SIGFM_BEST_MIN) + { + goodix533c_queue_verify_report (self, FPI_MATCH_SUCCESS, NULL); + self->verify_wait_finger_up = FALSE; + } + else + { + goodix533c_queue_verify_report (self, FPI_MATCH_FAIL, NULL); + self->verify_wait_finger_up = TRUE; + } + } + + goodix533c_match_free_info (probe_info); + g_clear_pointer (&self->captured_image, g_free); + + if (self->verify_wait_finger_up) + goodix533c_flush_pending_result_report (dev); + + fpi_ssm_next_state (ssm); + } + break; + + case GOODIX533C_VERIFY_FINISH: + /* Always wait for lift-off here -- see the file comment for why this + * driver has no cheap "deactivate without waiting" alternative for + * the success path. */ + goodix533c_start_finger_up_subsm (ssm, dev); + break; + } +} + +static void +goodix533c_verify_ssm_done (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + FpiDeviceAction action = fpi_device_get_current_action (dev); + + self->task_ssm = NULL; + g_clear_pointer (&self->reference_pixels, g_free); + self->have_reference = FALSE; + g_clear_pointer (&self->captured_image, g_free); + + if (error) + { + /* If cleanup fails after matching, the auth result still matters more + * than post-result hardware cleanup. */ + gint failed_state = fpi_ssm_get_cur_state (ssm); + + if (failed_state >= GOODIX533C_VERIFY_FINISH && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + fp_warn ("Post-match cleanup error (non-fatal): %s", + error->message); + g_clear_error (&error); + } + } + + if (error == NULL) + { + if (self->pending_action_error != NULL) + error = g_steal_pointer (&self->pending_action_error); + else + goodix533c_flush_pending_result_report (dev); + } + else + goodix533c_clear_pending_result_report (self); + + self->verify_wait_finger_up = FALSE; + + if (action == FPI_DEVICE_ACTION_IDENTIFY) + fpi_device_identify_complete (dev, error); + else + fpi_device_verify_complete (dev, error); +} + +void +goodix533c_auth_start (FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + FpiSsm *ssm; + + goodix533c_clear_pending_result_report (self); + self->verify_wait_finger_up = FALSE; + g_clear_pointer (&self->reference_pixels, g_free); + self->have_reference = FALSE; + g_clear_pointer (&self->captured_image, g_free); + + ssm = fpi_ssm_new (dev, goodix533c_verify_ssm_handler, + GOODIX533C_VERIFY_NUM_STATES); + self->task_ssm = ssm; + fpi_ssm_start (ssm, goodix533c_verify_ssm_done); +} diff --git a/libfprint/drivers/goodix533c/goodix533c-auth.h b/libfprint/drivers/goodix533c/goodix533c-auth.h new file mode 100644 index 000000000..1071fc2fe --- /dev/null +++ b/libfprint/drivers/goodix533c/goodix533c-auth.h @@ -0,0 +1,33 @@ +/* + * Goodix 27c6:533c native driver for libfprint — Verify/identify flow + * + * Copyright (C) 2026 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "goodix533c-private.h" + +/* Reset verify/identify action state and start the top-level auth flow. The + * shared SSM dispatches on fpi_device_get_current_action() internally. + * Implements both FpDeviceClass::verify and FpDeviceClass::identify. */ +void goodix533c_auth_start (FpDevice *dev); + +/* Drop any match result queued while waiting for finger-up. Used by + * goodix533c_auth_start() and by close()/cancel() to discard stale + * results. */ +void goodix533c_clear_pending_result_report (FpiDeviceGoodix533c *self); diff --git a/libfprint/drivers/goodix533c/goodix533c-enroll.c b/libfprint/drivers/goodix533c/goodix533c-enroll.c new file mode 100644 index 000000000..d9b767355 --- /dev/null +++ b/libfprint/drivers/goodix533c/goodix533c-enroll.c @@ -0,0 +1,234 @@ +/* + * Goodix 27c6:533c native driver for libfprint — Enrollment flow + * + * Copyright (C) 2026 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* + * Enroll SSM shape ported near-verbatim from goodix53x5-enroll.c (sibling + * driver, same SIGFM approach). goodix53x5 has REINIT/REINIT_DONE states to + * recover from a suspend-induced stale USB claim; this driver has no + * suspend/resume story yet (open()'s TLS/config/FDT-baseline bring-up is + * assumed valid for the whole session), so those states are dropped here. + */ + +#define FP_COMPONENT "goodix533c" + +#include "drivers_api.h" +#include "goodix533c-private.h" +#include "goodix533c-match.h" +#include "goodix533c-enroll.h" + +#include + +/* Settle time between an enrollment stage's finger-up wait and the next + * stage's fresh reference capture, so sensor state from the just-released + * touch doesn't bleed into the next capture. Same value goodix53x5 uses. */ +#define GOODIX533C_ENROLL_RELEASE_SETTLE_MS 350 + +typedef enum { + GOODIX533C_ENROLL_CAPTURE_REF = 0, + GOODIX533C_ENROLL_WAIT_FINGER, + GOODIX533C_ENROLL_CAPTURE, + GOODIX533C_ENROLL_PROCESS, + GOODIX533C_ENROLL_WAIT_FINGER_UP, + GOODIX533C_ENROLL_NEXT, + GOODIX533C_ENROLL_NUM_STATES, +} Goodix533cEnrollState; + +static void +goodix533c_enroll_ssm_handler (FpiSsm *ssm, + FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case GOODIX533C_ENROLL_CAPTURE_REF: + if (fpi_device_action_is_cancelled (dev)) + { + fpi_ssm_mark_failed (ssm, + g_error_new_literal (G_IO_ERROR, + G_IO_ERROR_CANCELLED, + "Enrollment cancelled")); + return; + } + + goodix533c_start_ref_capture_subsm (ssm, dev); + break; + + case GOODIX533C_ENROLL_WAIT_FINGER: + goodix533c_start_finger_wait_subsm (ssm, dev, NULL, NULL); + break; + + case GOODIX533C_ENROLL_CAPTURE: + goodix533c_start_live_capture_subsm (ssm, dev); + break; + + case GOODIX533C_ENROLL_PROCESS: + { + GoodixMatchInfo *info; + GBytes *feature; + int keypoints; + + /* Partial-contact captures make weak templates -- ask the user to + * re-place the finger instead of storing such a stage. This gate is + * cheap and correct, though likely inert at this driver's current + * headroom-safe gain -- see GOODIX533C_RAW12_CLIP's doc comment. */ + if (self->captured_clipped_fraction > GOODIX533C_ENROLL_MAX_CLIPPED_FRACTION) + { + fp_dbg ("Enrollment stage rejected: %.1f%% of frame has no " + "finger contact (limit %.1f%%)", + self->captured_clipped_fraction * 100.0, + GOODIX533C_ENROLL_MAX_CLIPPED_FRACTION * 100.0); + g_clear_pointer (&self->captured_image, g_free); + fpi_device_enroll_progress (dev, self->enroll_stage, NULL, + fpi_device_retry_new (FP_DEVICE_RETRY_CENTER_FINGER)); + fpi_ssm_next_state (ssm); + return; + } + + info = goodix533c_match_extract (self->captured_image); + keypoints = goodix533c_match_keypoints_count (info); + + if (keypoints < GOODIX533C_MIN_CAPTURE_KEYPOINTS) + { + goodix533c_match_free_info (info); + g_clear_pointer (&self->captured_image, g_free); + fpi_device_enroll_progress (dev, self->enroll_stage, NULL, + fpi_device_retry_new (FP_DEVICE_RETRY_REMOVE_FINGER)); + fpi_ssm_next_state (ssm); + return; + } + + feature = goodix533c_match_serialize_template (info); + goodix533c_match_free_info (info); + if (feature == NULL) + { + g_clear_pointer (&self->captured_image, g_free); + fpi_ssm_mark_failed (ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL, + "Failed to serialize SIGFM features")); + return; + } + + g_ptr_array_add (self->enroll_features, feature); + g_clear_pointer (&self->captured_image, g_free); + self->enroll_stage++; + + fp_dbg ("Enrollment stage %d/%d complete", + self->enroll_stage, GOODIX533C_ENROLL_SAMPLES); + + fpi_device_enroll_progress (dev, self->enroll_stage, NULL, NULL); + fpi_ssm_next_state (ssm); + } + break; + + case GOODIX533C_ENROLL_WAIT_FINGER_UP: + goodix533c_start_finger_up_subsm (ssm, dev); + break; + + case GOODIX533C_ENROLL_NEXT: + if (self->enroll_stage < GOODIX533C_ENROLL_SAMPLES) + { + if (fpi_device_action_is_cancelled (dev)) + { + fpi_ssm_mark_failed (ssm, + g_error_new_literal (G_IO_ERROR, + G_IO_ERROR_CANCELLED, + "Enrollment cancelled")); + return; + } + + fp_dbg ("Waiting %dms for enrollment release to settle", + GOODIX533C_ENROLL_RELEASE_SETTLE_MS); + fpi_ssm_jump_to_state_delayed (ssm, GOODIX533C_ENROLL_CAPTURE_REF, + GOODIX533C_ENROLL_RELEASE_SETTLE_MS); + } + else + fpi_ssm_mark_completed (ssm); + break; + } +} + +static void +goodix533c_enroll_ssm_done (FpiSsm *ssm, FpDevice *dev, GError *error) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + FpPrint *print = NULL; + GVariantBuilder builder; + GVariant *data; + + self->task_ssm = NULL; + + if (error) + { + g_clear_pointer (&self->enroll_features, g_ptr_array_unref); + g_clear_pointer (&self->reference_pixels, g_free); + g_clear_pointer (&self->captured_image, g_free); + fpi_device_enroll_complete (dev, NULL, error); + return; + } + + /* Build print from serialized enrollment features. */ + fpi_device_get_enroll_data (dev, &print); + fpi_print_set_type (print, FPI_PRINT_RAW); + + /* GVariant "aay" -- array of byte arrays, one per enrollment sample. */ + g_variant_builder_init (&builder, G_VARIANT_TYPE ("aay")); + + for (guint i = 0; i < self->enroll_features->len; i++) + { + GBytes *feature = g_ptr_array_index (self->enroll_features, i); + gsize feature_len; + const guint8 *feature_data = g_bytes_get_data (feature, &feature_len); + + g_variant_builder_add (&builder, "@ay", + g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE, + feature_data, + feature_len, + 1)); + } + + data = g_variant_builder_end (&builder); + g_object_set (G_OBJECT (print), "fpi-data", data, NULL); + + g_clear_pointer (&self->enroll_features, g_ptr_array_unref); + + fp_info ("Enrollment complete with %d samples", GOODIX533C_ENROLL_SAMPLES); + + fpi_device_enroll_complete (dev, g_object_ref (print), NULL); +} + +void +goodix533c_enroll_start (FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + FpiSsm *ssm; + + self->enroll_stage = 0; + g_clear_pointer (&self->reference_pixels, g_free); + self->have_reference = FALSE; + g_clear_pointer (&self->captured_image, g_free); + g_clear_pointer (&self->enroll_features, g_ptr_array_unref); + self->enroll_features = g_ptr_array_new_with_free_func ((GDestroyNotify) g_bytes_unref); + + ssm = fpi_ssm_new (dev, goodix533c_enroll_ssm_handler, + GOODIX533C_ENROLL_NUM_STATES); + self->task_ssm = ssm; + fpi_ssm_start (ssm, goodix533c_enroll_ssm_done); +} diff --git a/libfprint/drivers/goodix533c/goodix533c-enroll.h b/libfprint/drivers/goodix533c/goodix533c-enroll.h new file mode 100644 index 000000000..d8af6c783 --- /dev/null +++ b/libfprint/drivers/goodix533c/goodix533c-enroll.h @@ -0,0 +1,30 @@ +/* + * Goodix 27c6:533c native driver for libfprint — Enrollment flow + * + * Copyright (C) 2026 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "goodix533c-private.h" + +/* Reset enrollment action state and run the full enroll flow (capture + * reference -> wait finger -> capture -> extract/quality-gate/store -> + * wait finger up, repeated GOODIX533C_ENROLL_SAMPLES times); reports + * completion through fpi_device_enroll_*. Implements + * FpDeviceClass::enroll. */ +void goodix533c_enroll_start (FpDevice *dev); diff --git a/libfprint/drivers/goodix533c/goodix533c-match.c b/libfprint/drivers/goodix533c/goodix533c-match.c new file mode 100644 index 000000000..b59943fc5 --- /dev/null +++ b/libfprint/drivers/goodix533c/goodix533c-match.c @@ -0,0 +1,161 @@ +/* + * Goodix 27c6:533c native driver for libfprint — SIGFM template format and matching + * + * Copyright (C) 2026 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* + * Driver-side wrapper around sigfm/sigfm.hpp. This module -- and only this + * module -- talks to the SIGFM/OpenCV implementation directly; everything + * else in this driver only ever sees the GoodixMatchInfo opaque handle and + * serialized GBytes* templates declared in goodix533c-match.h. + * + * Ported near-verbatim from goodix53x5-match.c (sibling driver, same SIGFM + * approach, same 108x88 sensor resolution) with only the driver prefix and + * template magic bytes changed. + */ + +#define FP_COMPONENT "goodix533c" + +#include "drivers_api.h" +#include "goodix533c-private.h" +#include "goodix533c-match.h" +#include "sigfm/sigfm.hpp" + +#include + +/* Driver-owned wrapper for serialized SIGFM features. Bump the version when + * preprocessing, extraction, or matching semantics make old templates unsafe + * to compare against newly enrolled templates. Own magic distinct from + * goodix53x5's "G53S" -- these templates are never interchangeable (533c's + * captured_image comes from a different preprocessing pipeline, flat-field + * regression rather than percentile normalization). */ +#define GOODIX533C_SIGFM_TEMPLATE_MAGIC "G533" +#define GOODIX533C_SIGFM_TEMPLATE_MAGIC_LEN 4 +#define GOODIX533C_SIGFM_TEMPLATE_VERSION 1 +#define GOODIX533C_SIGFM_TEMPLATE_HEADER_LEN \ + (GOODIX533C_SIGFM_TEMPLATE_MAGIC_LEN + sizeof (guint16)) +#define GOODIX533C_SIGFM_TEMPLATE_MAX_LEN (1024 * 1024) + +GoodixMatchInfo * +goodix533c_match_extract (const guint8 *image) +{ + return sigfm_extract (image, GOODIX533C_SENSOR_WIDTH, GOODIX533C_SENSOR_HEIGHT); +} + +int +goodix533c_match_keypoints_count (GoodixMatchInfo *info) +{ + return sigfm_keypoints_count (info); +} + +void +goodix533c_match_free_info (GoodixMatchInfo *info) +{ + sigfm_free_info (info); +} + +GBytes * +goodix533c_match_serialize_template (GoodixMatchInfo *info) +{ + guint8 *feature; + guint8 *tmpl; + guint16 version; + int feature_len; + + feature = sigfm_serialize_binary (info, &feature_len); + if (feature == NULL || feature_len <= 0 || + feature_len > GOODIX533C_SIGFM_TEMPLATE_MAX_LEN - GOODIX533C_SIGFM_TEMPLATE_HEADER_LEN) + { + g_free (feature); + return NULL; + } + + tmpl = g_malloc (GOODIX533C_SIGFM_TEMPLATE_HEADER_LEN + feature_len); + memcpy (tmpl, GOODIX533C_SIGFM_TEMPLATE_MAGIC, + GOODIX533C_SIGFM_TEMPLATE_MAGIC_LEN); + version = GUINT16_TO_LE (GOODIX533C_SIGFM_TEMPLATE_VERSION); + memcpy (tmpl + GOODIX533C_SIGFM_TEMPLATE_MAGIC_LEN, &version, + sizeof (version)); + memcpy (tmpl + GOODIX533C_SIGFM_TEMPLATE_HEADER_LEN, feature, feature_len); + g_free (feature); + + return g_bytes_new_take (tmpl, + GOODIX533C_SIGFM_TEMPLATE_HEADER_LEN + feature_len); +} + +static SigfmImgInfo * +goodix533c_match_deserialize_template (const guint8 *tmpl, + gsize tmpl_len, + Goodix533cSigfmTemplateStatus *status) +{ + SigfmImgInfo *info; + guint16 version; + gsize feature_len; + + *status = GOODIX533C_SIGFM_TEMPLATE_INVALID; + + if (tmpl_len <= GOODIX533C_SIGFM_TEMPLATE_HEADER_LEN || + tmpl_len > GOODIX533C_SIGFM_TEMPLATE_MAX_LEN || + memcmp (tmpl, GOODIX533C_SIGFM_TEMPLATE_MAGIC, + GOODIX533C_SIGFM_TEMPLATE_MAGIC_LEN) != 0) + { + *status = GOODIX533C_SIGFM_TEMPLATE_INCOMPATIBLE; + return NULL; + } + + memcpy (&version, tmpl + GOODIX533C_SIGFM_TEMPLATE_MAGIC_LEN, + sizeof (version)); + if (GUINT16_FROM_LE (version) != GOODIX533C_SIGFM_TEMPLATE_VERSION) + { + *status = GOODIX533C_SIGFM_TEMPLATE_INCOMPATIBLE; + return NULL; + } + + feature_len = tmpl_len - GOODIX533C_SIGFM_TEMPLATE_HEADER_LEN; + if (feature_len > G_MAXINT) + return NULL; + + info = sigfm_deserialize_binary (tmpl + GOODIX533C_SIGFM_TEMPLATE_HEADER_LEN, + (int) feature_len); + if (info != NULL) + *status = GOODIX533C_SIGFM_TEMPLATE_OK; + + return info; +} + +Goodix533cSigfmTemplateStatus +goodix533c_match_serialized_feature (GoodixMatchInfo *probe_info, + const guint8 *feature, + gsize feature_len, + int *score) +{ + SigfmImgInfo *tmpl_info; + Goodix533cSigfmTemplateStatus status; + + tmpl_info = goodix533c_match_deserialize_template (feature, feature_len, + &status); + if (tmpl_info == NULL) + return status; + + *score = sigfm_match_score (probe_info, tmpl_info); + sigfm_free_info (tmpl_info); + if (*score < 0) + return GOODIX533C_SIGFM_TEMPLATE_INVALID; + + return GOODIX533C_SIGFM_TEMPLATE_OK; +} diff --git a/libfprint/drivers/goodix533c/goodix533c-match.h b/libfprint/drivers/goodix533c/goodix533c-match.h new file mode 100644 index 000000000..40c1db055 --- /dev/null +++ b/libfprint/drivers/goodix533c/goodix533c-match.h @@ -0,0 +1,54 @@ +/* + * Goodix 27c6:533c native driver for libfprint — SIGFM template format and matching + * + * Copyright (C) 2026 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "goodix533c-private.h" + +/* Opaque handle for extracted SIGFM features (struct SigfmImgInfo). Only + * this module talks to the SIGFM/OpenCV implementation directly. */ +typedef struct SigfmImgInfo GoodixMatchInfo; + +typedef enum { + GOODIX533C_SIGFM_TEMPLATE_OK, + GOODIX533C_SIGFM_TEMPLATE_INCOMPATIBLE, + GOODIX533C_SIGFM_TEMPLATE_INVALID, +} Goodix533cSigfmTemplateStatus; + +/* Extract SIGFM features (CLAHE + SIFT) from a processed 8-bit sensor frame + * of GOODIX533C_SENSOR_WIDTH x GOODIX533C_SENSOR_HEIGHT pixels. Free the + * result with goodix533c_match_free_info(). Returns NULL on failure (never + * throws across the C ABI -- see sigfm.cpp). */ +GoodixMatchInfo *goodix533c_match_extract (const guint8 *image); + +int goodix533c_match_keypoints_count (GoodixMatchInfo *info); + +void goodix533c_match_free_info (GoodixMatchInfo *info); + +/* Serialize extracted features into a driver-owned template (magic + version + * header + serialized features). Returns NULL on serialization failure. */ +GBytes *goodix533c_match_serialize_template (GoodixMatchInfo *info); + +/* Score @probe_info against one serialized enrolled sample. On + * GOODIX533C_SIGFM_TEMPLATE_OK, *score holds the SIGFM match score. */ +Goodix533cSigfmTemplateStatus goodix533c_match_serialized_feature (GoodixMatchInfo *probe_info, + const guint8 *feature, + gsize feature_len, + int *score); diff --git a/libfprint/drivers/goodix533c/goodix533c-private.h b/libfprint/drivers/goodix533c/goodix533c-private.h new file mode 100644 index 000000000..f314c630f --- /dev/null +++ b/libfprint/drivers/goodix533c/goodix533c-private.h @@ -0,0 +1,175 @@ +/* + * Goodix 27c6:533c native driver for libfprint — Private device state + * + * Copyright (C) 2026 libfprint contributors + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* + * Shared private state and sub-SSM entry points for the goodix533c driver. + * goodix533c.c owns the transport/protocol/capture internals and defines + * everything declared here; goodix533c-match.c, goodix533c-enroll.c, and + * goodix533c-auth.c only see this header (plus goodix533c-match.h) so they + * stay decoupled from the wire protocol. + */ + +#pragma once + +#include "fpi-device.h" +#include "fpi-ssm.h" + +/* goodixtls.h embeds SSL_CTX / SSL fields but does not include openssl + * itself (it relies on its one existing includer, goodix533c.c, having + * already done so) -- since this header now also embeds a GoodixTlsServer + * by value in the struct below, include openssl first here too. */ +#include + +#include "../goodixtls/goodixtls.h" + +#include "goodix533c.h" + +/* Enrollment sample count. Starting point taken from the sibling goodix53x5 + * driver (same SIGFM approach, same 108x88 sensor resolution) — not yet + * independently tuned against 533c's own capture characteristics. */ +#define GOODIX533C_ENROLL_SAMPLES 8 + +/* SIGFM (SIFT-based) matching parameters — same starting points as + * goodix53x5-private.h; see the report for why these were kept as-is. */ +#define GOODIX533C_SIGFM_BEST_MIN 150 +#define GOODIX533C_MIN_CAPTURE_KEYPOINTS 20 + +/* decode_frame() in goodix533c.c uses the same 12-bit packing as + * goodix53x5-image.c's goodix_device_decode_image() (bit-identical chunk + * layout), and NOTES.md's gain sweep confirms this device's raw samples + * span the same 0-4095 range ("clipped_px=.../9504", "0-4095 range"). Reused + * as-is; see the report for why this gate is likely inert at the gain this + * driver already uses. */ +#define GOODIX533C_RAW12_CLIP 4095 + +/* Enrollment stages with more than this fraction of non-contact (clipped) + * pixels are rejected with a retry. See GOODIX533C_RAW12_CLIP's comment — + * this gate is expected to rarely (if ever) fire on this device at its + * current headroom-safe gain, but it is cheap and correct to keep. */ +#define GOODIX533C_ENROLL_MAX_CLIPPED_FRACTION 0.10 + +/* Generic single-in-flight command callback shape. Declared here (not just + * in goodix533c.c) because it is the type of the callback/user_data fields + * below. */ +typedef void (*Goodix533cCmdCallback)(FpDevice *dev, + guint8 *data, + guint16 length, + gpointer user_data, + GError *error); + +/* --- Device struct --- */ +struct _FpiDeviceGoodix533c +{ + FpDevice parent_instance; + + GCancellable *transfer_cancel_tkn; + gboolean interface_claimed; + gboolean read_loop_started; + + /* reassembly buffer for the current incoming pack */ + guint8 *rx_buf; + guint32 rx_len; + + /* in-flight command state -- single command at a time */ + guint8 cmd; + gboolean ack_pending; + gboolean reply_pending; + GSource *timeout_src; + Goodix533cCmdCallback callback; + gpointer user_data; + + /* embedded TLS-PSK server -- goodixtls.c, unmodified */ + GoodixTlsServer tls; + gboolean tls_active; + + /* per-session FDT baseline, read fresh every open */ + guint8 fdt_template[24]; + gboolean have_fdt_template; + + /* most recent no-finger reference frame (raw12), used to flat-field the + * next live capture against. Re-captured at the start of every + * enroll/verify/identify attempt, not just once per open() session. */ + guint16 *reference_pixels; + gboolean have_reference; + + /* most recent live (finger-present) capture */ + guint16 *live_raw_pixels; /* raw12, transient */ + guint8 *captured_image; /* flat-fielded + squashed 8-bit frame, + * GOODIX533C_SENSOR_WIDTH * + * GOODIX533C_SENSOR_HEIGHT bytes -- + * this is what SIGFM matches against */ + double captured_clipped_fraction; /* non-contact pixel fraction, quality gate */ + + /* Top-level enroll/verify/identify SSM currently running, if any. */ + FpiSsm *task_ssm; + + /* Enrollment tracking */ + GPtrArray *enroll_features; /* array of GBytes* serialized SIGFM templates */ + gint enroll_stage; + + /* Failed verify/identify attempts wait for lift-off before completing so + * one held invalid finger cannot be re-read as the next attempt. */ + gboolean verify_wait_finger_up; + + /* Verify/identify result queued until post-match cleanup (finger-up wait) + * has completed -- see goodix533c-auth.c. */ + gboolean pending_result_report; + FpiDeviceAction pending_result_action; + FpiMatchResult pending_verify_result; + FpPrint *pending_identify_match; + GError *pending_result_error; + GError *pending_action_error; +}; + +/* =========================================================================== + * Sub-SSM entry points, implemented in goodix533c.c, shared by the + * capture-test harness and the enroll/auth SSMs below. + * ======================================================================= */ + +/* Capture the TX-off no-finger reference frame into self->reference_pixels. + * Must run before goodix533c_start_live_capture_subsm(). */ +void goodix533c_start_ref_capture_subsm (FpiSsm *parent_ssm, + FpDevice *dev); + +/* Arm finger-down detection and block (within the SSM) until the device's + * asynchronous touch notification arrives. @wait_for_finger_cb is invoked + * once detection is armed and the wait begins; may be NULL (used by + * enroll/verify/identify, which use fpi_device_report_finger_status_changes() + * instead). */ +void goodix533c_start_finger_wait_subsm (FpiSsm *parent_ssm, + FpDevice *dev, + Goodix533cProgressFunc wait_for_finger_cb, + gpointer wait_for_finger_data); + +/* Capture a live finger frame, decrypt/decode it, flat-field it against + * self->reference_pixels, and store the processed 8-bit frame into + * self->captured_image plus the quality metric into + * self->captured_clipped_fraction. */ +void goodix533c_start_live_capture_subsm (FpiSsm *parent_ssm, + FpDevice *dev); + +/* Block (within the SSM) until finger lift-off is detected. */ +void goodix533c_start_finger_up_subsm (FpiSsm *parent_ssm, + FpDevice *dev); + +/* Force-fail whatever command is currently in flight (ack/reply wait) with + * G_IO_ERROR_CANCELLED. Used by FpDeviceClass::cancel to unblock a long + * finger-wait immediately instead of waiting out its timeout. */ +void goodix533c_cancel_pending_command (FpDevice *dev); diff --git a/libfprint/drivers/goodix533c/goodix533c.c b/libfprint/drivers/goodix533c/goodix533c.c index e8768db90..f3c6e7f8e 100644 --- a/libfprint/drivers/goodix533c/goodix533c.c +++ b/libfprint/drivers/goodix533c/goodix533c.c @@ -32,6 +32,9 @@ #include "../goodixtls/goodixtls.h" #include "goodix533c.h" +#include "goodix533c-private.h" +#include "goodix533c-enroll.h" +#include "goodix533c-auth.h" /* ---- device-level constants (all hardware-verified, see * findings/native-driver-architecture.md) ---- */ @@ -83,9 +86,10 @@ static const guint8 capture_off[2] = { 0x0a, 0x02 }; #define GOODIX533C_CMD_MCU_SWITCH_TO_SLEEP_MODE (0x60) /* FDT command prefixes -- fixed 2-byte prefix, each suffixed with the same - * 24-byte per-session template read during CAPTURE_STAGE_FDT_BASELINE. - * fdt_mode_idle (above) is the fourth member of this family, used with 24 - * zero bytes to *measure* the template; these three arm/query it. */ + * 24-byte per-session template read during OPEN_STAGE_FDT_BASELINE (see + * open_run() further down). fdt_mode_idle (above) is the fourth member of + * this family, used with 24 zero bytes to *measure* the template; these + * three arm/query it. */ static const guint8 fdt_mode_armed[2] = { 0x8d, 0x01 }; static const guint8 fdt_down_armed[2] = { 0x0c, 0x01 }; static const guint8 fdt_up_armed[2] = { 0x0e, 0x01 }; @@ -140,52 +144,6 @@ static const guint8 device_config[256] = { #define GOODIX533C_IMAGE_PIXELS \ (GOODIX533C_SENSOR_WIDTH * GOODIX533C_SENSOR_HEIGHT) -/* ---- generic single-in-flight command callback shape, ported from - * goodix.c's GoodixCmdCallback ---- */ -typedef void (*Goodix533cCmdCallback)(FpDevice *dev, - guint8 *data, - guint16 length, - gpointer user_data, - GError *error); - -struct _FpiDeviceGoodix533c -{ - FpDevice parent_instance; - - GCancellable *transfer_cancel_tkn; - gboolean interface_claimed; - gboolean read_loop_started; - - /* reassembly buffer for the current incoming pack */ - guint8 *rx_buf; - guint32 rx_len; - - /* in-flight command state -- single command at a time, exactly like - * goodix.c's FpiDeviceGoodixTlsPrivate */ - guint8 cmd; - gboolean ack_pending; - gboolean reply_pending; - GSource *timeout_src; - Goodix533cCmdCallback callback; - gpointer user_data; - - /* embedded TLS-PSK server -- goodixtls.c, unmodified */ - GoodixTlsServer tls; - gboolean tls_active; - - /* per-session FDT baseline, read fresh every open per the findings doc; - * appended (with a distinct fixed prefix) to every FDT arm/query - * command below. */ - guint8 fdt_template[24]; - gboolean have_fdt_template; - - /* no-finger reference frame, kept around (not just handed to a callback - * and discarded) so a later live-frame capture in the same session can - * flat-field against it without re-measuring. */ - guint16 *reference_pixels; - gboolean have_reference; -}; - G_DEFINE_TYPE (FpiDeviceGoodix533c, fpi_device_goodix533c, FP_TYPE_DEVICE) /* =========================================================================== @@ -1003,59 +961,30 @@ squash_frame_linear (const guint16 *frame, guint8 *squashed, guint32 count) } /* =========================================================================== - * Capture sequence -- new code, following capture_golden_session.py / - * driver_53xc.py's run_driver() exactly: reset through the no-finger - * reference frame, then sleep/query -> arm finger detection -> wait for a - * touch -> live (finger-present) frame -> flat-field the live frame - * against the reference. One continuous FpiSsm, not two -- see the FDT - * arm/wait/capture stages appended below CAPTURE_STAGE_CAPTURE_OFF. + * Capture building blocks -- ported logic from capture_golden_session.py / + * driver_53xc.py's run_driver(), refactored into reusable sub-SSMs. + * + * The original single-shot sequence (reset through the no-finger reference + * frame, sleep/query, arm finger detection, wait for a touch, live frame, + * flat-field against the reference) is split along a session-scoped vs. + * attempt-scoped line: + * + * - Session-scoped (reset through FDT baseline measurement) now lives in + * open_run() below -- it must only happen once per fp_device_open() + * session, not once per enroll/verify attempt, or enroll would mean 8 + * full USB re-handshakes instead of 8 fast touches. + * - Attempt-scoped (reference capture, finger wait, live capture, finger + * up) becomes four sub-SSM starter functions + * (goodix533c_start_{ref_capture,finger_wait,live_capture,finger_up}_subsm(), + * declared in goodix533c-private.h), each usable as a child of any + * parent SSM via fpi_ssm_start_subsm(). fpi_device_goodix533c_capture_test() + * below chains all four for the standalone test harness; + * goodix533c-enroll.c and goodix533c-auth.c each chain them their own way + * (enroll repeats all four up to GOODIX533C_ENROLL_SAMPLES times; auth + * runs the sequence once, replacing "finger up" cleanup with a match + * step in between). * ======================================================================= */ -enum capture_stage { - CAPTURE_STAGE_RESET, - CAPTURE_STAGE_READ_CHIP_ID, - CAPTURE_STAGE_READ_OTP, - CAPTURE_STAGE_TLS, - CAPTURE_STAGE_UPLOAD_CONFIG, - CAPTURE_STAGE_FDT_BASELINE, - CAPTURE_STAGE_CAPTURE_ON, /* reference frame, no finger */ - CAPTURE_STAGE_GET_IMAGE, - CAPTURE_STAGE_CAPTURE_OFF, - CAPTURE_STAGE_SLEEP, - CAPTURE_STAGE_QUERY_MCU_STATE, - CAPTURE_STAGE_FDT_ARM_DOWN, - CAPTURE_STAGE_WAIT_FOR_FINGER, - CAPTURE_STAGE_FDT_MODE_ARM, - CAPTURE_STAGE_CAPTURE_ON_LIVE, /* live (finger-present) frame */ - CAPTURE_STAGE_GET_IMAGE_LIVE, - CAPTURE_STAGE_CAPTURE_OFF_LIVE, - CAPTURE_STAGE_FDT_UP, - CAPTURE_STAGE_NUM, -}; - -typedef struct -{ - Goodix533cProgressFunc wait_for_finger_cb; - Goodix533cCaptureDoneFunc callback; - gpointer user_data; - - guint16 *raw_pixels; /* reference frame */ - guint8 *squashed; - - guint16 *live_raw_pixels; /* live frame */ - guint8 *corrected; /* flat-fielded + squashed */ -} CaptureData; - -static void -capture_data_free (CaptureData *data) -{ - g_free (data->raw_pixels); - g_free (data->squashed); - g_free (data->live_raw_pixels); - g_free (data->corrected); - g_free (data); -} - static void on_capture_step_reply (FpDevice *dev, guint8 *data, guint16 length, gpointer user_data, GError *error) @@ -1153,7 +1082,8 @@ on_fdt_baseline_reply (FpDevice *dev, guint8 *data, guint16 length, * template -- see fdt_template() in driver_53xc.py. Appended (with a * distinct fixed 2-byte prefix) to every later FDT arm/query command in * this same session -- see fdt_mode_armed/fdt_down_armed/fdt_up_armed - * above and their use in capture_run() below. */ + * above and their use in the sub-SSM handlers below (finger_wait_ssm_handler, + * finger_up_ssm_handler). */ /* Sample count: driver_53xc.py's fdt_template() computes this as * len(range(4, length - 1, 2)), which looks off-by-one against the * naive (length - 4) / 2 used below at first glance, but is not -- @@ -1249,16 +1179,20 @@ decode_get_image_reply (FpiDeviceGoodix533c *self, guint8 *data, return TRUE; } +/** + * on_ref_get_image_reply: GET_IMAGE reply handler for the no-finger + * reference capture. Stores the decoded raw12 frame into + * self->reference_pixels, replacing whatever the previous attempt (or + * open() session) left there -- each enroll stage / verify attempt + * re-measures its own fresh reference immediately before its live capture. + */ static void -on_get_image_reply (FpDevice *dev, guint8 *data, guint16 length, - gpointer user_data, GError *error) +on_ref_get_image_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) { FpiSsm *ssm = user_data; FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); - CaptureData *cap = fpi_ssm_get_data (ssm); - gboolean live = fpi_ssm_get_cur_state (ssm) == CAPTURE_STAGE_GET_IMAGE_LIVE; guint16 *raw_pixels = NULL; - guint8 *squashed = NULL; GError *decode_error = NULL; if (error) @@ -1267,32 +1201,52 @@ on_get_image_reply (FpDevice *dev, guint8 *data, guint16 length, return; } - if (!decode_get_image_reply (self, data, length, &raw_pixels, - live ? NULL : &squashed, &decode_error)) + if (!decode_get_image_reply (self, data, length, &raw_pixels, NULL, + &decode_error)) { fpi_ssm_mark_failed (ssm, decode_error); return; } - if (live) + g_clear_pointer (&self->reference_pixels, g_free); + self->reference_pixels = raw_pixels; + self->have_reference = TRUE; + + fpi_ssm_next_state (ssm); +} + +/** + * on_live_get_image_reply: GET_IMAGE reply handler for the live + * (finger-present) capture. Stores the decoded raw12 frame into + * self->live_raw_pixels -- flat-fielding against the reference and + * computing the clipped-fraction quality metric happens later, in the + * live-capture sub-SSM's PROCESS state, not here. + */ +static void +on_live_get_image_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + guint16 *raw_pixels = NULL; + GError *decode_error = NULL; + + if (error) { - cap->live_raw_pixels = raw_pixels; + fpi_ssm_mark_failed (ssm, error); + return; } - else - { - cap->raw_pixels = raw_pixels; - cap->squashed = squashed; - /* Keep a copy in the driver's private struct (not just handed to - * the callback) so the live-frame stages further down this same - * SSM can flat-field against it without re-measuring. */ - g_clear_pointer (&self->reference_pixels, g_free); - self->reference_pixels = g_new (guint16, GOODIX533C_IMAGE_PIXELS); - memcpy (self->reference_pixels, raw_pixels, - GOODIX533C_IMAGE_PIXELS * sizeof (guint16)); - self->have_reference = TRUE; + if (!decode_get_image_reply (self, data, length, &raw_pixels, NULL, + &decode_error)) + { + fpi_ssm_mark_failed (ssm, decode_error); + return; } + g_clear_pointer (&self->live_raw_pixels, g_free); + self->live_raw_pixels = raw_pixels; + fpi_ssm_next_state (ssm); } @@ -1320,6 +1274,8 @@ on_wait_finger_reply (FpDevice *dev, guint8 *data, guint16 length, } fp_dbg ("Finger detected (fdt_down push, %d bytes)", length); + fpi_device_report_finger_status_changes (dev, FP_FINGER_STATUS_PRESENT, + FP_FINGER_STATUS_NEEDED); fpi_ssm_next_state (ssm); } @@ -1381,64 +1337,110 @@ flat_field_squash (const guint16 *frame, const guint16 *reference, } } -static void -capture_run (FpiSsm *ssm, FpDevice *dev) +/** + * compute_clipped_fraction: fraction of raw12 pixels at/above ADC full + * scale, i.e. the non-contact area of a live frame. decode_frame() uses the + * same 12-bit packing as the sibling goodix53x5 driver's + * goodix_device_decode_image() (bit-identical chunk layout), and this + * project's own gain sweep (NOTES.md, "Ridge visibility resolved") confirms + * this device's raw samples span the same 0-4095 range, so + * GOODIX533C_RAW12_CLIP reuses goodix53x5's GOODIX_RAW12_CLIP value as-is. + */ +static double +compute_clipped_fraction (const guint16 *img12) { - switch (fpi_ssm_get_cur_state (ssm)) - { - case CAPTURE_STAGE_RESET: - cmd_reset (dev, TRUE, FALSE, 20, on_reset_reply, ssm); - break; - - case CAPTURE_STAGE_READ_CHIP_ID: - cmd_read_sensor_register (dev, 0x0000, 4, on_capture_step_reply, ssm); - break; - - case CAPTURE_STAGE_READ_OTP: - cmd_read_otp (dev, on_capture_step_reply, ssm); - break; + guint32 clipped = 0; + guint32 i; - case CAPTURE_STAGE_TLS: - tls_connect (dev, on_tls_connected, ssm); - break; + for (i = 0; i < GOODIX533C_IMAGE_PIXELS; i++) + if (img12[i] >= GOODIX533C_RAW12_CLIP) + clipped++; - case CAPTURE_STAGE_UPLOAD_CONFIG: - cmd_upload_config_mcu (dev, device_config, sizeof (device_config), - on_upload_config_reply, ssm); - break; + return (double) clipped / GOODIX533C_IMAGE_PIXELS; +} - case CAPTURE_STAGE_FDT_BASELINE: - { - guint8 mode[26]; +/* =========================================================================== + * Reference-frame capture sub-SSM (attempt-scoped): power the sensor and + * capture the TX-off no-finger reference frame into self->reference_pixels. + * Must run before goodix533c_start_live_capture_subsm(). + * ======================================================================= */ - memcpy (mode, fdt_mode_idle, sizeof (fdt_mode_idle)); - memset (mode + sizeof (fdt_mode_idle), 0, - sizeof (mode) - sizeof (fdt_mode_idle)); - cmd_mcu_switch_to_fdt_mode (dev, mode, sizeof (mode), TRUE, - on_fdt_baseline_reply, ssm); - } - break; +enum ref_capture_stage { + REF_CAPTURE_ON = 0, + REF_CAPTURE_GET_IMAGE, + REF_CAPTURE_OFF, + REF_CAPTURE_NUM_STATES, +}; - case CAPTURE_STAGE_CAPTURE_ON: +static void +ref_capture_ssm_handler (FpiSsm *ssm, FpDevice *dev) +{ + switch (fpi_ssm_get_cur_state (ssm)) + { + case REF_CAPTURE_ON: cmd_write_sensor_register (dev, GOODIX533C_CAPTURE_REGISTER, capture_on, on_capture_step_reply, ssm); break; - case CAPTURE_STAGE_GET_IMAGE: + case REF_CAPTURE_GET_IMAGE: cmd_mcu_get_image_gain (dev, GOODIX533C_IMAGE_FLAGS_CALIBRATE, - GOODIX533C_IMAGE_GAIN, on_get_image_reply, ssm); + GOODIX533C_IMAGE_GAIN, on_ref_get_image_reply, + ssm); break; - case CAPTURE_STAGE_CAPTURE_OFF: + case REF_CAPTURE_OFF: cmd_write_sensor_register (dev, GOODIX533C_CAPTURE_REGISTER, capture_off, on_capture_step_reply, ssm); break; - case CAPTURE_STAGE_SLEEP: + default: + g_assert_not_reached (); + } +} + +void +goodix533c_start_ref_capture_subsm (FpiSsm *parent_ssm, FpDevice *dev) +{ + FpiSsm *sub = fpi_ssm_new (dev, ref_capture_ssm_handler, + REF_CAPTURE_NUM_STATES); + + fpi_ssm_start_subsm (parent_ssm, sub); +} + +/* =========================================================================== + * Finger-wait sub-SSM (attempt-scoped): arm finger-down detection and block + * until the device's asynchronous touch notification arrives. + * ======================================================================= */ + +enum finger_wait_stage { + FINGER_WAIT_SLEEP = 0, + FINGER_WAIT_QUERY_MCU_STATE, + FINGER_WAIT_FDT_ARM_DOWN, + FINGER_WAIT_WAIT_FOR_FINGER, + FINGER_WAIT_FDT_MODE_ARM, + FINGER_WAIT_NUM_STATES, +}; + +typedef struct +{ + Goodix533cProgressFunc cb; /* nullable, see goodix533c-private.h */ + gpointer user_data; +} FingerWaitData; + +static void +finger_wait_ssm_handler (FpiSsm *ssm, FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case FINGER_WAIT_SLEEP: + fpi_device_report_finger_status_changes (dev, FP_FINGER_STATUS_NEEDED, + FP_FINGER_STATUS_PRESENT); cmd_mcu_switch_to_sleep_mode (dev, on_capture_step_reply, ssm); break; - case CAPTURE_STAGE_QUERY_MCU_STATE: + case FINGER_WAIT_QUERY_MCU_STATE: { /* Payload taken verbatim from run_driver()'s * query_mcu_state(b"\x01\x00\x01", False) call site. */ @@ -1449,9 +1451,8 @@ capture_run (FpiSsm *ssm, FpDevice *dev) } break; - case CAPTURE_STAGE_FDT_ARM_DOWN: + case FINGER_WAIT_FDT_ARM_DOWN: { - FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); guint8 mode[26]; memcpy (mode, fdt_down_armed, sizeof (fdt_down_armed)); @@ -1462,20 +1463,19 @@ capture_run (FpiSsm *ssm, FpDevice *dev) } break; - case CAPTURE_STAGE_WAIT_FOR_FINGER: + case FINGER_WAIT_WAIT_FOR_FINGER: { - CaptureData *cap = fpi_ssm_get_data (ssm); + FingerWaitData *data = fpi_ssm_get_data (ssm); - if (cap->wait_for_finger_cb) - cap->wait_for_finger_cb (dev, cap->user_data); + if (data->cb) + data->cb (dev, data->user_data); await_fdt_down_push (dev, GOODIX533C_FINGER_WAIT_TIMEOUT_MS, on_wait_finger_reply, ssm); } break; - case CAPTURE_STAGE_FDT_MODE_ARM: + case FINGER_WAIT_FDT_MODE_ARM: { - FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); guint8 mode[26]; memcpy (mode, fdt_mode_armed, sizeof (fdt_mode_armed)); @@ -1491,35 +1491,178 @@ capture_run (FpiSsm *ssm, FpDevice *dev) } break; - case CAPTURE_STAGE_CAPTURE_ON_LIVE: + default: + g_assert_not_reached (); + } +} + +void +goodix533c_start_finger_wait_subsm (FpiSsm *parent_ssm, + FpDevice *dev, + Goodix533cProgressFunc wait_for_finger_cb, + gpointer wait_for_finger_data) +{ + FpiSsm *sub = fpi_ssm_new (dev, finger_wait_ssm_handler, + FINGER_WAIT_NUM_STATES); + FingerWaitData *data = g_new0 (FingerWaitData, 1); + + data->cb = wait_for_finger_cb; + data->user_data = wait_for_finger_data; + fpi_ssm_set_data (sub, data, g_free); + + fpi_ssm_start_subsm (parent_ssm, sub); +} + +/* =========================================================================== + * Live-capture sub-SSM (attempt-scoped): capture the live (finger-present) + * frame, decrypt/decode it, then flat-field it against + * self->reference_pixels and compute the clipped-fraction quality metric -- + * both new relative to the original single-shot flow, needed so + * enroll/verify/identify can quality-gate and match immediately, before + * waiting for finger-up. + * ======================================================================= */ + +enum live_capture_stage { + LIVE_CAPTURE_ON = 0, + LIVE_CAPTURE_GET_IMAGE, + LIVE_CAPTURE_OFF, + LIVE_CAPTURE_PROCESS, + LIVE_CAPTURE_NUM_STATES, +}; + +static void +live_capture_ssm_handler (FpiSsm *ssm, FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case LIVE_CAPTURE_ON: cmd_write_sensor_register (dev, GOODIX533C_CAPTURE_REGISTER, capture_on, on_capture_step_reply, ssm); break; - case CAPTURE_STAGE_GET_IMAGE_LIVE: + case LIVE_CAPTURE_GET_IMAGE: /* Gain 0xc2, not driver_53xc.py's default 0x86 for the live frame -- * see GOODIX533C_LIVE_IMAGE_GAIN's doc comment above for why this is * a deliberate, hardware-verified deviation on this unit. */ cmd_mcu_get_image_gain (dev, GOODIX533C_IMAGE_FLAGS_LIVE, - GOODIX533C_LIVE_IMAGE_GAIN, on_get_image_reply, - ssm); + GOODIX533C_LIVE_IMAGE_GAIN, + on_live_get_image_reply, ssm); break; - case CAPTURE_STAGE_CAPTURE_OFF_LIVE: + case LIVE_CAPTURE_OFF: cmd_write_sensor_register (dev, GOODIX533C_CAPTURE_REGISTER, capture_off, on_capture_step_reply, ssm); break; - case CAPTURE_STAGE_FDT_UP: + case LIVE_CAPTURE_PROCESS: + if (self->live_raw_pixels == NULL || !self->have_reference) + { + fpi_ssm_mark_failed (ssm, + fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO, + "Missing reference or live frame")); + return; + } + + self->captured_clipped_fraction = + compute_clipped_fraction (self->live_raw_pixels); + + g_clear_pointer (&self->captured_image, g_free); + self->captured_image = g_new0 (guint8, GOODIX533C_IMAGE_PIXELS); + flat_field_squash (self->live_raw_pixels, self->reference_pixels, + GOODIX533C_IMAGE_PIXELS, self->captured_image); + + fpi_ssm_next_state (ssm); + break; + + default: + g_assert_not_reached (); + } +} + +void +goodix533c_start_live_capture_subsm (FpiSsm *parent_ssm, FpDevice *dev) +{ + FpiSsm *sub = fpi_ssm_new (dev, live_capture_ssm_handler, + LIVE_CAPTURE_NUM_STATES); + + fpi_ssm_start_subsm (parent_ssm, sub); +} + +/* =========================================================================== + * Finger-up sub-SSM (attempt-scoped): block until finger lift-off is + * detected, so a lingering touch is never misread as the next attempt's + * touch. mcu_switch_to_fdt_up's reply itself blocks until the device sees + * the down->up transition (hardware-verified this session for a single + * capture), so this is a thin wrapper around the existing command rather + * than new detection logic. + * ======================================================================= */ + +enum finger_up_stage { + FINGER_UP_WAIT = 0, + FINGER_UP_NUM_STATES, +}; + +static void +on_finger_up_reply (FpDevice *dev, guint8 *data, guint16 length, + gpointer user_data, GError *error) +{ + FpiSsm *ssm = user_data; + + if (error) + { + /* A bare timeout here just means the user has not lifted their + * finger within GOODIX533C_FDT_UP_TIMEOUT_MS (5s) yet -- unlike the + * finger-wait timeout above, this is not necessarily user error, and + * failing the whole enroll/verify/identify action over it would be + * harsh, especially for enroll, which runs this after every one of + * GOODIX533C_ENROLL_SAMPLES stages. Treat a timeout as "assume + * lifted" and proceed instead of aborting the action. + * + * This reintroduces some of the staleness risk the wait exists to + * prevent (a finger still down could be misread as part of the next + * attempt) and has not been exercised against real hardware with a + * deliberately slow lift-off -- see the report for what a human + * needs to validate here. Any other error remains fatal. */ + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) + { + fp_warn ("Finger-up wait timed out after %dms; assuming lifted " + "and continuing", GOODIX533C_FDT_UP_TIMEOUT_MS); + g_clear_error (&error); + fpi_device_report_finger_status_changes (dev, FP_FINGER_STATUS_NONE, + FP_FINGER_STATUS_PRESENT | + FP_FINGER_STATUS_NEEDED); + fpi_ssm_next_state (ssm); + return; + } + + fpi_ssm_mark_failed (ssm, error); + return; + } + + fpi_device_report_finger_status_changes (dev, FP_FINGER_STATUS_NONE, + FP_FINGER_STATUS_PRESENT | + FP_FINGER_STATUS_NEEDED); + fpi_ssm_next_state (ssm); +} + +static void +finger_up_ssm_handler (FpiSsm *ssm, FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case FINGER_UP_WAIT: { - FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); guint8 mode[26]; memcpy (mode, fdt_up_armed, sizeof (fdt_up_armed)); memcpy (mode + sizeof (fdt_up_armed), self->fdt_template, sizeof (self->fdt_template)); cmd_mcu_switch_to_fdt_up (dev, mode, sizeof (mode), - on_capture_step_reply, ssm); + on_finger_up_reply, ssm); } break; @@ -1528,28 +1671,110 @@ capture_run (FpiSsm *ssm, FpDevice *dev) } } +void +goodix533c_start_finger_up_subsm (FpiSsm *parent_ssm, FpDevice *dev) +{ + FpiSsm *sub = fpi_ssm_new (dev, finger_up_ssm_handler, + FINGER_UP_NUM_STATES); + + fpi_ssm_start_subsm (parent_ssm, sub); +} + +/* =========================================================================== + * cancel() support: force-fail whatever command is currently in flight. + * ======================================================================= */ + +void +goodix533c_cancel_pending_command (FpDevice *dev) +{ + FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); + GError *error = NULL; + + if (!self->ack_pending && !self->reply_pending) + return; + + g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_CANCELLED, + "Action cancelled"); + deliver_reply (dev, NULL, 0, error); +} + +/* =========================================================================== + * Test-only capture harness -- chains the four sub-SSMs above in the same + * order the original monolithic capture_run() used, then synthesizes the + * legacy Goodix533cCaptureDoneFunc callback shape from whatever + * self->reference_pixels / self->live_raw_pixels / self->captured_image + * hold at completion time. Those fields persist past whichever sub-SSM + * produced them (unlike the old per-call CaptureData struct), so a frame + * that did succeed is never lost just because a later stage (e.g. + * finger-wait timing out with no physical touch) failed. + * ======================================================================= */ + +enum capture_test_stage { + CAPTURE_TEST_REF = 0, + CAPTURE_TEST_FINGER_WAIT, + CAPTURE_TEST_LIVE, + CAPTURE_TEST_FINGER_UP, + CAPTURE_TEST_NUM_STATES, +}; + +typedef struct +{ + Goodix533cProgressFunc wait_for_finger_cb; + Goodix533cCaptureDoneFunc callback; + gpointer user_data; +} CaptureTestData; + static void -capture_done (FpiSsm *ssm, FpDevice *dev, GError *error) +capture_test_ssm_handler (FpiSsm *ssm, FpDevice *dev) +{ + CaptureTestData *data = fpi_ssm_get_data (ssm); + + switch (fpi_ssm_get_cur_state (ssm)) + { + case CAPTURE_TEST_REF: + goodix533c_start_ref_capture_subsm (ssm, dev); + break; + + case CAPTURE_TEST_FINGER_WAIT: + goodix533c_start_finger_wait_subsm (ssm, dev, data->wait_for_finger_cb, + data->user_data); + break; + + case CAPTURE_TEST_LIVE: + goodix533c_start_live_capture_subsm (ssm, dev); + break; + + case CAPTURE_TEST_FINGER_UP: + goodix533c_start_finger_up_subsm (ssm, dev); + break; + + default: + g_assert_not_reached (); + } +} + +static void +capture_test_ssm_done (FpiSsm *ssm, FpDevice *dev, GError *error) { FpiDeviceGoodix533c *self = FPI_DEVICE_GOODIX533C (dev); - CaptureData *cap = fpi_ssm_get_data (ssm); + CaptureTestData *data = fpi_ssm_get_data (ssm); + guint16 *raw_pixels = NULL; + g_autofree guint8 *squashed = NULL; + guint8 *corrected = NULL; - /* Flat-field only if the live frame actually got captured -- e.g. the - * finger-wait stage timing out (the only path exercised against real - * hardware this session, since it requires no physical touch) leaves - * live_raw_pixels NULL and error non-NULL, and cap->raw_pixels/squashed - * (the reference frame, captured earlier in this same sequence) are - * still handed back below regardless of @error, so a harness never - * loses a frame that did succeed just because a later stage failed. */ - if (cap->live_raw_pixels && self->have_reference) + if (self->have_reference && self->reference_pixels) { - cap->corrected = g_new0 (guint8, GOODIX533C_IMAGE_PIXELS); - flat_field_squash (cap->live_raw_pixels, self->reference_pixels, - GOODIX533C_IMAGE_PIXELS, cap->corrected); + raw_pixels = self->reference_pixels; + squashed = g_new0 (guint8, GOODIX533C_IMAGE_PIXELS); + squash_frame_linear (self->reference_pixels, squashed, + GOODIX533C_IMAGE_PIXELS); } - cap->callback (dev, cap->raw_pixels, cap->squashed, cap->live_raw_pixels, - cap->corrected, cap->user_data, error); + if (self->live_raw_pixels && self->captured_image) + corrected = self->captured_image; + + data->callback (dev, raw_pixels, squashed, self->live_raw_pixels, + corrected, data->user_data, error); } void @@ -1558,16 +1783,16 @@ fpi_device_goodix533c_capture_test (FpDevice *dev, Goodix533cCaptureDoneFunc callback, gpointer user_data) { - CaptureData *cap = g_new0 (CaptureData, 1); + CaptureTestData *data = g_new0 (CaptureTestData, 1); FpiSsm *ssm; - cap->wait_for_finger_cb = wait_for_finger_cb; - cap->callback = callback; - cap->user_data = user_data; + data->wait_for_finger_cb = wait_for_finger_cb; + data->callback = callback; + data->user_data = user_data; - ssm = fpi_ssm_new (dev, capture_run, CAPTURE_STAGE_NUM); - fpi_ssm_set_data (ssm, cap, (GDestroyNotify) capture_data_free); - fpi_ssm_start (ssm, capture_done); + ssm = fpi_ssm_new (dev, capture_test_ssm_handler, CAPTURE_TEST_NUM_STATES); + fpi_ssm_set_data (ssm, data, g_free); + fpi_ssm_start (ssm, capture_test_ssm_done); } /* =========================================================================== @@ -1576,12 +1801,29 @@ fpi_device_goodix533c_capture_test (FpDevice *dev, * init_device(). Ported logic, new SSM (goodix.c has no equivalent * standalone open sequence -- that's spread across goodix5xx.c's shared * ACTIVATE state machine, which this driver deliberately does not use). + * + * RESET through FDT_BASELINE used to be the first six states of the + * single-shot capture_run() SSM (see capture_test.c's original flow). + * They are session-scoped -- TLS handshake, config upload, and the FDT + * threshold template are all valid for the whole open() session, not just + * one capture -- so they belong here, run once, rather than being repeated + * by every enroll stage or verify/identify attempt. Everything + * attempt-scoped (reference capture, finger wait, live capture, finger up) + * lives in the sub-SSM starter functions below instead; enroll/verify/ + * identify assume open() has already brought the device through + * FDT_BASELINE and call only those. * ======================================================================= */ enum open_stage { OPEN_STAGE_NOP, OPEN_STAGE_FIRMWARE_VERSION, OPEN_STAGE_PSK_READ, + OPEN_STAGE_RESET, + OPEN_STAGE_READ_CHIP_ID, + OPEN_STAGE_READ_OTP, + OPEN_STAGE_TLS, + OPEN_STAGE_UPLOAD_CONFIG, + OPEN_STAGE_FDT_BASELINE, OPEN_STAGE_NUM, }; @@ -1710,6 +1952,39 @@ open_run (FpiSsm *ssm, FpDevice *dev) 0, on_open_psk_read_reply, ssm); break; + case OPEN_STAGE_RESET: + cmd_reset (dev, TRUE, FALSE, 20, on_reset_reply, ssm); + break; + + case OPEN_STAGE_READ_CHIP_ID: + cmd_read_sensor_register (dev, 0x0000, 4, on_capture_step_reply, ssm); + break; + + case OPEN_STAGE_READ_OTP: + cmd_read_otp (dev, on_capture_step_reply, ssm); + break; + + case OPEN_STAGE_TLS: + tls_connect (dev, on_tls_connected, ssm); + break; + + case OPEN_STAGE_UPLOAD_CONFIG: + cmd_upload_config_mcu (dev, device_config, sizeof (device_config), + on_upload_config_reply, ssm); + break; + + case OPEN_STAGE_FDT_BASELINE: + { + guint8 mode[26]; + + memcpy (mode, fdt_mode_idle, sizeof (fdt_mode_idle)); + memset (mode + sizeof (fdt_mode_idle), 0, + sizeof (mode) - sizeof (fdt_mode_idle)); + cmd_mcu_switch_to_fdt_mode (dev, mode, sizeof (mode), TRUE, + on_fdt_baseline_reply, ssm); + } + break; + default: g_assert_not_reached (); } @@ -1792,6 +2067,17 @@ goodix533c_close (FpDevice *dev) self->have_reference = FALSE; self->have_fdt_template = FALSE; + /* Attempt-scoped enroll/verify/identify state -- also cleared here (not + * just at the end of each action) in case close() runs mid-action, e.g. + * the client disconnecting during an enroll. */ + g_clear_pointer (&self->live_raw_pixels, g_free); + g_clear_pointer (&self->captured_image, g_free); + g_clear_pointer (&self->enroll_features, g_ptr_array_unref); + self->enroll_stage = 0; + self->task_ssm = NULL; + self->verify_wait_finger_up = FALSE; + goodix533c_clear_pending_result_report (self); + if (self->interface_claimed) { g_usb_device_release_interface (fpi_device_get_usb_device (dev), @@ -1802,6 +2088,30 @@ goodix533c_close (FpDevice *dev) fpi_device_close_complete (dev, error); } +static void +goodix533c_enroll (FpDevice *dev) +{ + goodix533c_enroll_start (dev); +} + +static void +goodix533c_verify (FpDevice *dev) +{ + goodix533c_auth_start (dev); +} + +static void +goodix533c_identify (FpDevice *dev) +{ + goodix533c_auth_start (dev); +} + +static void +goodix533c_cancel (FpDevice *dev) +{ + goodix533c_cancel_pending_command (dev); +} + /* =========================================================================== * GObject boilerplate * ======================================================================= */ @@ -1818,6 +2128,10 @@ fpi_device_goodix533c_finalize (GObject *object) g_clear_pointer (&self->rx_buf, g_free); g_clear_pointer (&self->reference_pixels, g_free); + g_clear_pointer (&self->live_raw_pixels, g_free); + g_clear_pointer (&self->captured_image, g_free); + g_clear_pointer (&self->enroll_features, g_ptr_array_unref); + goodix533c_clear_pending_result_report (self); g_clear_object (&self->transfer_cancel_tkn); G_OBJECT_CLASS (fpi_device_goodix533c_parent_class)->finalize (object); @@ -1841,15 +2155,24 @@ fpi_device_goodix533c_class_init (FpiDeviceGoodix533cClass *klass) dev_class->type = FP_DEVICE_TYPE_USB; dev_class->scan_type = FP_SCAN_TYPE_PRESS; dev_class->id_table = goodix533c_id_table; - dev_class->nr_enroll_stages = 1; + dev_class->nr_enroll_stages = GOODIX533C_ENROLL_SAMPLES; dev_class->temp_hot_seconds = -1; - /* FpDevice requires a non-NONE feature set (see fp_device_constructed()'s - * g_assert). FP_DEVICE_FEATURE_CAPTURE is the accurate declaration here - * and, unlike VERIFY/IDENTIFY, does not require those vfuncs to be set -- - * enroll/verify/identify are out of scope for this driver so far; open() - * + capture is exercised only via fpi_device_goodix533c_capture_test(). */ - dev_class->features = FP_DEVICE_FEATURE_CAPTURE; dev_class->open = goodix533c_open; dev_class->close = goodix533c_close; + dev_class->enroll = goodix533c_enroll; + dev_class->verify = goodix533c_verify; + dev_class->identify = goodix533c_identify; + dev_class->cancel = goodix533c_cancel; + + /* No dev_class->capture vfunc is wired -- open() + capture is exercised + * only via the test-only fpi_device_goodix533c_capture_test() entry + * point, so auto_initialize_features() correctly does not claim + * FP_DEVICE_FEATURE_CAPTURE (it only sets that bit when + * dev_class->capture is non-NULL). It does pick up VERIFY/IDENTIFY from + * the vfuncs just set, and FP_DEVICE_FEATURE_ALWAYS_ON from + * temp_hot_seconds < 0 above -- matching the sibling goodixtls511 + * driver's convention of calling this once at the end of class_init() + * rather than assigning dev_class->features by hand. */ + fpi_device_class_auto_initialize_features (dev_class); } From 1a5ab3be3cea3d4cc7dad6e0ecfadc4dd622521b Mon Sep 17 00:00:00 2001 From: daemonhorn Date: Sun, 23 Aug 2026 07:15:45 -0400 Subject: [PATCH 07/12] sigfm: add standalone SIGFM round-trip test Host-side test with no hardware/sensor dependency: extract SIFT features from a synthetic 108x88 structured frame (a grid of Gaussian blobs -- not flat grey, which yields zero SIFT keypoints and would make a self-match pass vacuously), serialize, deserialize, and score the round trip against the original, plus a few auxiliary checks (sigfm_copy_info(), a second independent extraction, a cross-match against a different synthetic frame). Confirms the OpenCV/SIGFM integration itself links and behaves correctly, independent of the driver build. Not wired into the meson build, matching the upstream goodix53x5-libfprint repo's own sigfm/tests convention (manually invoked via g++, not a `meson test` target). Build/run command is in the file's header comment. --- sigfm/tests/test_sigfm_roundtrip.cpp | 175 +++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 sigfm/tests/test_sigfm_roundtrip.cpp diff --git a/sigfm/tests/test_sigfm_roundtrip.cpp b/sigfm/tests/test_sigfm_roundtrip.cpp new file mode 100644 index 000000000..8fd95e35b --- /dev/null +++ b/sigfm/tests/test_sigfm_roundtrip.cpp @@ -0,0 +1,175 @@ +// Standalone host-side round-trip test for the vendored SIGFM library. +// No hardware/sensor needed. Verifies the OpenCV/SIGFM integration itself +// (extraction, serialization, deserialization, scoring) is sound before it +// is ever touched by real capture data. +// +// Uses the driver's real sensor dimensions (108x88, GOODIX533C_SENSOR_WIDTH +// x GOODIX533C_SENSOR_HEIGHT) and structured synthetic input (a grid of +// Gaussian-like blobs), not flat grey -- SIFT finds zero keypoints on a +// flat image, which would make a self-match round trip pass vacuously. +// +// Build (run from the repo root, so the -I. below reaches sigfm/sigfm.hpp): +// g++ -std=c++17 -I. sigfm/tests/test_sigfm_roundtrip.cpp sigfm/sigfm.cpp \ +// $(pkg-config --cflags opencv5 2>/dev/null || pkg-config --cflags opencv4) \ +// -lopencv_core -lopencv_imgproc -lopencv_flann \ +// $(pkg-config --exists opencv5 && echo -lopencv_features || echo -lopencv_features2d) \ +// -o /tmp/sigfm_roundtrip_test && /tmp/sigfm_roundtrip_test +// +// Not wired into the meson build (matching the upstream goodix53x5-libfprint +// repo's own sigfm/tests, which are likewise standalone/manually invoked +// rather than a `meson test` target) -- this exercises the SIGFM library in +// isolation, independent of whether any particular driver is selected. + +#include "sigfm/sigfm.hpp" + +#include +#include +#include +#include +#include + +static int failures = 0; + +#define CHECK(cond, msg) \ + do { \ + if (!(cond)) { \ + std::printf(" FAIL: %s\n", msg); \ + failures++; \ + } else { \ + std::printf(" ok: %s\n", msg); \ + } \ + } while (0) + +static const int kWidth = 108; // GOODIX533C_SENSOR_WIDTH +static const int kHeight = 88; // GOODIX533C_SENSOR_HEIGHT + +// A grid of soft Gaussian blobs at pseudo-random offsets/amplitudes -- gives +// SIFT plenty of local structure to key on, unlike a flat or purely linear +// gradient image (which either has zero keypoints or keypoints only at the +// border). +static std::vector make_structured_frame(unsigned seed) +{ + std::vector img(kWidth * kHeight); + std::vector> blobs; // x, y, sigma, amplitude + + unsigned state = seed; + auto next = [&state]() { + state = state * 1103515245u + 12345u; + return (double) ((state >> 8) & 0xFFFF) / 65535.0; + }; + + for (int i = 0; i < 24; i++) { + double x = 6.0 + next() * (kWidth - 12.0); + double y = 6.0 + next() * (kHeight - 12.0); + double sigma = 2.5 + next() * 4.0; + double amp = 60.0 + next() * 120.0; + blobs.push_back({x, y, sigma, amp}); + } + + for (int y = 0; y < kHeight; y++) { + for (int x = 0; x < kWidth; x++) { + double v = 90.0; // mid-grey baseline + for (const auto &b : blobs) { + double dx = x - b[0]; + double dy = y - b[1]; + double d2 = dx * dx + dy * dy; + v += b[3] * std::exp(-d2 / (2.0 * b[2] * b[2])); + } + int iv = (int) std::lround(v); + if (iv < 0) iv = 0; + if (iv > 255) iv = 255; + img[y * kWidth + x] = (unsigned char) iv; + } + } + return img; +} + +int main() +{ + std::printf("SIGFM round-trip test (%dx%d synthetic structured frame)\n\n", + kWidth, kHeight); + + std::vector frame = make_structured_frame(0xC0FFEE); + + // 1. Extract. + SigfmImgInfo *info = sigfm_extract(frame.data(), kWidth, kHeight); + CHECK(info != nullptr, "sigfm_extract() succeeds on structured input"); + if (info == nullptr) { + std::printf("\n%d TEST(S) FAILED (cannot continue)\n", ++failures); + return 1; + } + + int keypoints = sigfm_keypoints_count(info); + std::printf(" keypoints extracted: %d\n", keypoints); + CHECK(keypoints > 0, "structured frame yields at least one SIFT keypoint"); + + // 2. Serialize. + int serialized_len = 0; + unsigned char *serialized = sigfm_serialize_binary(info, &serialized_len); + CHECK(serialized != nullptr && serialized_len > 0, + "sigfm_serialize_binary() produces a non-empty buffer"); + std::printf(" serialized size: %d bytes\n", serialized_len); + + // 3. Deserialize. + SigfmImgInfo *roundtrip = sigfm_deserialize_binary(serialized, serialized_len); + CHECK(roundtrip != nullptr, "sigfm_deserialize_binary() succeeds"); + + if (roundtrip != nullptr) { + CHECK(sigfm_keypoints_count(roundtrip) == keypoints, + "deserialized keypoint count matches original"); + + // 4. Score the deserialized copy against the original -- a perfect + // self-match (identical keypoints/descriptors) should score very high, + // comfortably above GOODIX533C_SIGFM_BEST_MIN (150). + int score = sigfm_match_score(info, roundtrip); + std::printf(" self-match score (original vs. round-tripped): %d\n", score); + CHECK(score >= 150, "round-tripped template scores >= GOODIX533C_SIGFM_BEST_MIN (150) against itself"); + } + + // 5. Also sanity-check sigfm_copy_info() and a genuinely independent + // extraction of the *same* pixel buffer -- two independent SIFT passes + // over identical input should also match each other highly. + SigfmImgInfo *copy = sigfm_copy_info(info); + CHECK(copy != nullptr, "sigfm_copy_info() succeeds"); + if (copy != nullptr) { + int score = sigfm_match_score(info, copy); + std::printf(" self-match score (original vs. sigfm_copy_info()): %d\n", score); + CHECK(score >= 150, "copied info scores >= GOODIX533C_SIGFM_BEST_MIN against original"); + sigfm_free_info(copy); + } + + SigfmImgInfo *independent = sigfm_extract(frame.data(), kWidth, kHeight); + CHECK(independent != nullptr, "second independent sigfm_extract() call succeeds"); + if (independent != nullptr) { + int score = sigfm_match_score(info, independent); + std::printf(" self-match score (original vs. independent re-extract): %d\n", score); + CHECK(score >= 150, "independently re-extracted frame scores >= GOODIX533C_SIGFM_BEST_MIN"); + sigfm_free_info(independent); + } + + // Cross-check against a *different* structured frame (different seed): + // real impostor rejection depends on preprocessing/descriptors differing, + // which this synthetic generator does provide across seeds, so this + // should score noticeably lower than the self-match cases above (though + // not necessarily below the accept gate -- that is not this test's + // contract, see test_sigfm_match.cpp upstream for the geometry contract). + std::vector other_frame = make_structured_frame(0xDEADBEEF); + SigfmImgInfo *other = sigfm_extract(other_frame.data(), kWidth, kHeight); + if (other != nullptr && roundtrip != nullptr) { + int score = sigfm_match_score(roundtrip, other); + std::printf(" cross-match score (round-tripped vs. different frame): %d\n", score); + sigfm_free_info(other); + } + + if (roundtrip != nullptr) + sigfm_free_info(roundtrip); + free(serialized); + sigfm_free_info(info); + + if (failures == 0) { + std::printf("\nALL TESTS PASSED\n"); + return 0; + } + std::printf("\n%d TEST(S) FAILED\n", failures); + return 1; +} From 02cc1c26f2ee8ebc4f9380b5017dda9e424d9671 Mon Sep 17 00:00:00 2001 From: daemonhorn Date: Sun, 23 Aug 2026 07:20:36 -0400 Subject: [PATCH 08/12] tests: add goodix533c umockdev fixture (device discovery + feature flags) Copies the finger-absent device/capture.pcapng fixture from the parent project's tests/goodix533c/ (unmodified there) into this submodule as tests/goodix533c/{device,custom.pcapng}, matching the custom.pcapng/ custom.py naming convention used by sibling drivers (goodixmoc, fpcmoc, elanmoc), and wires 'goodix533c' into tests/meson.build's drivers_tests list following the existing goodixmoc pattern. custom.py currently only exercises device discovery and feature-flag assertions (VERIFY/IDENTIFY present, CAPTURE/STORAGE* absent, matching the driver's current auto-derived features). It deliberately does not call open_sync() or beyond: replaying custom.pcapng against the real goodix533c-capture-test binary and via meson test showed the fixture's bulk-IN response endpoint (0x83) carries zero captured reply payload throughout the entire file (confirmed with tshark, cross-checked against tests/goodixmoc/custom.pcapng as a control), so it cannot actually replay the driver's open() sequence past its second command. See tests/goodix533c/README.md's "Verified replay result" section for the full transcript and diagnosis, and its "Follow-up needed" section for what a corrected capture and, later, enroll/verify/identify coverage would require -- both need a new capture that only a human can safely make, per the finger-present PSK-decryptability constraint documented there. No new USB capture was made as part of this change. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y9XXWG7y2kfBEXJj3MS5yv --- tests/goodix533c/README.md | 188 +++++++++++++++++++++++ tests/goodix533c/custom.pcapng | Bin 0 -> 37244 bytes tests/goodix533c/custom.py | 84 +++++++++++ tests/goodix533c/device | 268 +++++++++++++++++++++++++++++++++ tests/meson.build | 1 + 5 files changed, 541 insertions(+) create mode 100644 tests/goodix533c/README.md create mode 100644 tests/goodix533c/custom.pcapng create mode 100644 tests/goodix533c/custom.py create mode 100644 tests/goodix533c/device diff --git a/tests/goodix533c/README.md b/tests/goodix533c/README.md new file mode 100644 index 000000000..f100c79e0 --- /dev/null +++ b/tests/goodix533c/README.md @@ -0,0 +1,188 @@ +# goodix533c umockdev test fixture + +Real USB traffic captured from a physical `27c6:533c` sensor via +`usbmon`/`tshark`, for `umockdev-run -p` replay -- same mechanism used by +`tests/goodixmoc/`, `tests/fpcmoc/`, `tests/elanmoc/`, etc. in this tree. + +This is a copy of the fixture originally captured and vetted in the parent +project at `tests/goodix533c/` (outside this submodule); see that +directory's own README.md for the full capture provenance notes. The files +here are byte-identical copies, renamed to match this tree's convention +(`custom.pcapng`/`custom.py`, per `tests/umockdev-test.py`) rather than +moved -- the original is left in place. + +- `device` -- `umockdev-record`'s sysfs/udev description of the real + device (vendor/product IDs, descriptors, interfaces, endpoints). +- `custom.pcapng` -- one full session: `nop -> reset -> read chip ID -> + read OTP -> TLS-PSK handshake -> upload_config_mcu -> FDT baseline -> + one mcu_get_image capture (reference frame, gain 0xc2)`. +- `custom.py` -- driven by `tests/umockdev-test.py` (invoked via `meson + test`), exercises device discovery and feature-flag assertions against + the replayed session. It deliberately stops there and does not call + `open_sync()` -- see "Verified replay result" below for why. + +## Deliberately finger-absent + +This fixture stops after the no-finger reference-frame capture and never +calls `wait_for_finger()`/captures a live frame. The PSK for this whole +device family is public (all-zero), so anyone with the pcapng can decrypt +every `mcu_get_image` payload in it. A live capture would be a real, +recoverable fingerprint image committed to a public repo -- so it was +deliberately not what got recorded. + +**No new capture may ever be added here that contains a finger-present +`mcu_get_image` reply, for any reason** -- not to test finger-detect-wait, +not to test live-capture/flat-field, and not to test SIGFM +enroll/verify/identify (see below -- those vfuncs are wired up in the +driver now, but nothing in this fixture can safely exercise them). Any +such fixture must be captured and vetted by a human outside of an +automated agent, exactly as this one was. + +## Verified replay result (important -- read before trusting this fixture) + +Replaying `custom.pcapng` against the real, currently-built `goodix533c` +driver (both directly via the `goodix533c-capture-test` binary, and via +`custom.py`/`meson test`) was checked while adding this fixture to this +submodule. It does **not** get as far as the protocol summary above +implies. Concretely: + +```sh +$ umockdev-run -d device \ + -p /sys/devices/pci0000:00/0000:00:14.0/usb3/3-3=custom.pcapng \ + -- .../builddir/libfprint/goodix533c-capture-test +Found: 0 (Goodix 27c6:533c Fingerprint Sensor) - driver goodix533c +Opening 0 ... + +(process:NNNNN): libfprint-goodix533c-WARNING **: Unknown pack flags: 0x00 + +(process:NNNNN): libfprint-goodix533c-WARNING **: Unknown pack flags: 0x00 +open() FAILED: Command timed out: 0xa8 +``` + +`0xa8` is `GOODIX_CMD_FIRMWARE_VERSION`, the *second* command the driver's +open() sequence sends (after `nop`, whose reply -- or lack of one -- the +driver already tolerates). Under the standard `meson test` driver-test +harness, which sets `G_DEBUG=fatal-warnings`, the same underlying +condition instead aborts the process with `SIGTRAP` on the "Unknown pack +flags: 0x00" warning rather than reaching the timeout message, because +that warning becomes fatal. + +Root cause, confirmed with `tshark`'s decoded USB URB fields (not just a +manual hex read) across the *entire* capture file: every completion event +on the fingerprint device's (bus 3, address 6) bulk-IN endpoint (address +`0x83`) has `usb.data_len == 0` -- i.e. **no bulk-IN reply payload was +ever captured for this device, anywhere in this file**, even though every +outgoing bulk-OUT request was captured in full (including the later-stage +TLS ClientHello/PSK and config-upload writes -- confirmed via +`usb.endpoint_address.direction` to genuinely be host-to-device, not +misattributed replies) and the control-endpoint (EP0) enumeration traffic +has real payload. This holds for every command, not just +firmware_version -- firmware_version simply happens to be the first +command in open() that actually requires a substantive reply (`nop`'s +reply is optional by design). + +This finding was cross-checked with a control, since it's a strong claim +about an existing, already-vetted fixture: the same query +(`usb.endpoint_address==0x83 && usb.data_len>0`) against +`tests/goodixmoc/custom.pcapng` (a single-device capture with no bus +noise, known-good in upstream CI) returns 124 hits on its own endpoint +`0x83`, confirming both that the methodology correctly detects real +captured payload when present, and that a genuinely-replayable fixture +does carry it throughout. `goodix533c/custom.pcapng` returns 0 hits on +the same query, restricted to its own device's address (6) to exclude +unrelated bus traffic from another USB device (a Bluetooth adapter, +address 4) and the root hub (address 1) that happen to share the same +capture window. + +That the driver's outgoing requests visibly *progress* through the whole +open() sequence in this capture (firmware_version, PSK read, reset, chip +ID/OTP reads, then a multi-packet TLS ClientHello/PSK and config-upload +write sequence) shows the real hardware genuinely replied at each stage +during the original live session -- otherwise the driver could never +have gotten far enough to send those later commands. What's missing is +specifically the *captured* reply payload, i.e. a property of how this +file was recorded, not of what happened on the wire when it was recorded. + +**Practical effect on `custom.py`**: it does not call `open_sync()` (or +anything past it) at all, precisely because of this gap -- see the file +for the reasoning inline. It only asserts device discovery and feature +flags, which are fully verifiable against this fixture. `meson test`'s +`goodix533c` entry is expected to PASS with that reduced scope. A +previous draft of this fixture called `open_sync()`/`close_sync()` +unconditionally and documented the resulting failure instead of avoiding +it; that was reverted in favor of keeping the suite green and putting the +gap here, in the README, and in the task report instead of in a +permanently-red test. + +## Current scope and limitations + +`goodix533c.c` currently wires up `dev_class->open`/`->close`/`->enroll`/ +`->verify`/`->identify`/`->cancel` (via the concurrent SIGFM work), with +`features` derived by `fpi_device_class_auto_initialize_features()`: +`VERIFY`, `IDENTIFY`, and `ALWAYS_ON` are set; `CAPTURE` is deliberately +NOT set (`dev_class->capture` itself is left NULL -- the open()+one-frame +capture path is only exercised via the test-only +`goodix533c-capture-test` binary, not the public FPrint API); no +`STORAGE*` bits are set (no on-chip storage -- this driver's design is +match-on-host via SIGFM, see `sigfm/` and `libfprint/drivers/goodix533c/`). +Accordingly `custom.py`: + +- Does exercise: device enumeration and driver-name/feature-flag + assertions matching the current wiring. +- Does NOT exercise: `open_sync()`/`close_sync()` (see "Verified replay + result" above -- this specific capture file cannot support it), + `enroll_sync()`, `verify_sync()`, `identify_sync()`, or any on-chip + storage calls. The latter would require driving the device past what + this fixture could ever safely record (a live finger-present frame), + which is exactly what must not be committed, on top of the open() + replay gap making it moot anyway. + +**Follow-up needed, in two independent stages:** + +1. **Fix the replay gap first.** A corrected `custom.pcapng` (or a + replacement fixture) is needed that retains bulk-IN reply payload + data -- still finger-absent, still stopping before any live + `mcu_get_image` reply, just captured with a method that doesn't drop + the device's response bytes. Once that exists, add + `open_sync()`/`close_sync()` back into `custom.py` (they were removed + from this version specifically because the current fixture can't + support them -- see "Verified replay result" above) and confirm they + pass. +2. **Then, once SIGFM enroll/verify/identify work is complete**, extend + `custom.py` (or add a second fixture-specific test file) to drive + `enroll_sync()`/`verify_sync()`/`identify_sync()`, modeled on + `tests/fpcmoc/custom.py` or `tests/elanmoc/custom.py` (both + match-on-device though, not match-on-host -- so adapt rather than copy + the `STORAGE*` assertions; this driver has none of those). This needs + a *further* new capture that includes real finger-present + `mcu_get_image` replies -- which, per the constraint above, must be + captured and safety-reviewed by a human, never generated by an agent, + and only committed if the human is certain they're comfortable with + those frames being third-party-decryptable (the PSK is public). + +Neither of the two new captures described above were made as part of +adding this fixture. + +## Replay + +```sh +umockdev-run -d device \ + -p /sys/devices/pci0000:00/0000:00:14.0/usb3/3-3=custom.pcapng \ + -- +``` + +The syspath is specific to the machine this was captured on but is only +used as a mock sysfs label by umockdev -- any syspath works as long as +the `-p` flag's key matches the `P:` line in `device` with `/sys` +prepended. `tests/umockdev-test.py` derives this automatically from the +`device` file, so `meson test goodix533c` does not need the path spelled +out manually. + +**Known limitation** (inherited from the original capture): replaying +against `vendor/goodix-fp-dump-nikicat`'s Python reference driver directly +fails at device-open (PyUSB's `protocol.py` makes `is_kernel_driver_active`/ +`set_configuration` calls that a libfprint C driver using `GUsbDevice`/ +`g_usb_device_claim_interface` would not). This fixture targets the native +libfprint `goodix533c` driver, not the Python reference implementation -- +though see "Verified replay result" above, since even against the native +driver this fixture currently can't complete `open()`. diff --git a/tests/goodix533c/custom.pcapng b/tests/goodix533c/custom.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..9d7266059788762fc093135a6de1fccde710f8aa GIT binary patch literal 37244 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 VdwVbPC1SgsJxNQ#wo|B#{{yG{X5s(< literal 0 HcmV?d00001 diff --git a/tests/goodix533c/custom.py b/tests/goodix533c/custom.py new file mode 100644 index 000000000..cf46353e8 --- /dev/null +++ b/tests/goodix533c/custom.py @@ -0,0 +1,84 @@ +#!/usr/bin/python3 + +# umockdev-replayed smoke test for the goodix533c driver. +# +# Scope: this driver has no on-chip storage -- matching is done on the host +# (see sigfm/ and libfprint/drivers/goodix533c/) rather than via +# FP_DEVICE_FEATURE_STORAGE, so this does not exercise +# list_prints_sync/delete_print_sync/clear_storage_sync the way +# tests/goodixmoc/custom.py or tests/synaptics/custom.py do. +# +# It is also deliberately scoped to what custom.pcapng can actually +# replay. custom.pcapng is a real, finger-absent capture from a physical +# 27c6:533c device (see README.md for full provenance and the finger- +# absent safety constraint -- no new capture may ever be added here that +# contains a finger-present mcu_get_image reply, for any reason). But as +# documented in detail in README.md's "Verified replay result" section, +# this specific capture file was empirically found (via tshark, with a +# goodixmoc/fpcmoc control confirming the methodology) to carry zero +# captured bulk-IN reply payload bytes on the device's response endpoint, +# anywhere in the file -- so it cannot actually replay the driver's +# open() sequence past its second command (firmware_version) via +# umockdev-run. Calling open_sync() here would therefore always fail +# (timeout, or a fatal "Unknown pack flags" warning under meson test's +# G_DEBUG=fatal-warnings), not because of anything this test or the +# driver gets wrong, but because of a gap in this specific capture file. +# Rather than land a permanently-red suite entry, this test is scoped to +# only what is genuinely, currently verifiable against this fixture: +# device discovery and feature-flag assertions. It deliberately does NOT +# call open_sync()/close_sync()/enroll_sync()/verify_sync()/ +# identify_sync() -- see README.md for exactly what a follow-up capture +# needs to provide before those can be added back. + +import traceback +import sys +import gi + +gi.require_version('FPrint', '2.0') +from gi.repository import FPrint, GLib + +# Exit with error on any exception, included those happening in async callbacks +sys.excepthook = lambda *args: (traceback.print_exception(*args), sys.exit(1)) + +ctx = GLib.main_context_default() + +c = FPrint.Context() +c.enumerate() +devices = c.get_devices() + +assert len(devices) == 1 +d = devices[0] +del devices + +assert d.get_driver() == "goodix533c" + +# Feature flags as currently wired in goodix533c.c via +# fpi_device_class_auto_initialize_features(): VERIFY/IDENTIFY are derived +# from ->verify/->identify being set; CAPTURE is NOT derived because +# dev_class->capture itself is deliberately left NULL (the open()+one- +# frame-capture path is only exercised via the test-only +# goodix533c-capture-test binary, not the public FPrint API); there is no +# on-chip storage (->list/->delete/->clear_storage all NULL) so none of +# the STORAGE* bits are set either. +# +# NOTE: this is a snapshot verified against a live-moving driver file +# shared with a concurrent SIGFM-matching work stream. If dev_class- +# >capture or any ->list/->delete/->clear_storage vfunc gets wired up +# after this was written, these assertions will start failing and need +# to be re-run/updated. +assert not d.has_feature(FPrint.DeviceFeature.CAPTURE) +assert d.has_feature(FPrint.DeviceFeature.IDENTIFY) +assert d.has_feature(FPrint.DeviceFeature.VERIFY) +assert not d.has_feature(FPrint.DeviceFeature.STORAGE) +assert not d.has_feature(FPrint.DeviceFeature.STORAGE_LIST) +assert not d.has_feature(FPrint.DeviceFeature.STORAGE_DELETE) +assert not d.has_feature(FPrint.DeviceFeature.STORAGE_CLEAR) +assert not d.has_feature(FPrint.DeviceFeature.DUPLICATES_CHECK) + +# open_sync()/close_sync() and beyond are intentionally NOT exercised here +# -- see the module docstring and README.md's "Verified replay result" +# for why this fixture cannot currently support that, with the exact +# umockdev-run transcripts that were captured while establishing this. + +del d +del c diff --git a/tests/goodix533c/device b/tests/goodix533c/device new file mode 100644 index 000000000..43b00381f --- /dev/null +++ b/tests/goodix533c/device @@ -0,0 +1,268 @@ +P: /devices/pci0000:00/0000:00:14.0/usb3/3-3 +N: bus/usb/003/006=12010002FF000040C6273C5300010102000109022000010100A0320904000002FF0000000705010240000007058302400000 +E: BUSNUM=003 +E: DEVNAME=/dev/bus/usb/003/006 +E: DEVNUM=006 +E: DEVTYPE=usb_device +E: DRIVER=usb +E: ID_AUTOSUSPEND=1 +E: ID_BUS=usb +E: ID_MODEL=FingerPrint +E: ID_MODEL_ENC=FingerPrint +E: ID_MODEL_ID=533c +E: ID_PATH=pci-0000:00:14.0-usb-0:3 +E: ID_PATH_TAG=pci-0000_00_14_0-usb-0_3 +E: ID_PATH_WITH_USB_REVISION=pci-0000:00:14.0-usbv2-0:3 +E: ID_PERSIST=0 +E: ID_REVISION=0100 +E: ID_SERIAL=Goodix_FingerPrint +E: ID_USB_INTERFACES=:ff0000: +E: ID_USB_MODEL=FingerPrint +E: ID_USB_MODEL_ENC=FingerPrint +E: ID_USB_MODEL_ID=533c +E: ID_USB_REVISION=0100 +E: ID_USB_SERIAL=Goodix_FingerPrint +E: ID_USB_VENDOR=Goodix +E: ID_USB_VENDOR_ENC=Goodix +E: ID_USB_VENDOR_ID=27c6 +E: ID_VENDOR=Goodix +E: ID_VENDOR_ENC=Goodix +E: ID_VENDOR_FROM_DATABASE=Shenzhen Goodix Technology Co.,Ltd. +E: ID_VENDOR_ID=27c6 +E: LIBFPRINT_DRIVER=Goodix Fingerprint Sensor +E: MAJOR=189 +E: MINOR=261 +E: PRODUCT=27c6/533c/100 +E: SUBSYSTEM=usb +E: TYPE=255/0/0 +A: authorized=1\n +A: avoid_reset_quirk=0\n +A: bConfigurationValue=1\n +A: bDeviceClass=ff\n +A: bDeviceProtocol=00\n +A: bDeviceSubClass=00\n +A: bMaxPacketSize0=64\n +A: bMaxPower=100mA\n +A: bNumConfigurations=1\n +A: bNumInterfaces= 1\n +A: bcdDevice=0100\n +A: bmAttributes=a0\n +A: busnum=3\n +A: configuration= +H: descriptors=12010002FF000040C6273C5300010102000109022000010100A0320904000002FF0000000705010240000007058302400000 +A: dev=189:261\n +A: devnum=6\n +A: devpath=3\n +L: driver=../../../../../bus/usb/drivers/usb +L: firmware_node=../../../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:13/device:14/device:17 +A: idProduct=533c\n +A: idVendor=27c6\n +A: ltm_capable=no\n +A: manufacturer=Goodix\n +A: maxchild=0\n +A: physical_location/dock=no\n +A: physical_location/horizontal_position=left\n +A: physical_location/lid=no\n +A: physical_location/panel=unknown\n +A: physical_location/vertical_position=upper\n +L: port=../3-0:1.0/usb3-port3 +A: power/active_duration=1376388\n +A: power/async=enabled\n +A: power/autosuspend=2\n +A: power/autosuspend_delay_ms=2000\n +A: power/connected_duration=9450652\n +A: power/control=auto\n +A: power/level=auto\n +A: power/persist=1\n +A: power/runtime_active_kids=0\n +A: power/runtime_active_time=1381176\n +A: power/runtime_enabled=enabled\n +A: power/runtime_status=active\n +A: power/runtime_suspended_time=8069203\n +A: power/runtime_usage=0\n +A: power/wakeup=disabled\n +A: power/wakeup_abort_count=\n +A: power/wakeup_active=\n +A: power/wakeup_active_count=\n +A: power/wakeup_count=\n +A: power/wakeup_expire_count=\n +A: power/wakeup_last_time_ms=\n +A: power/wakeup_max_time_ms=\n +A: power/wakeup_total_time_ms=\n +A: product=FingerPrint\n +A: quirks=0x0\n +A: removable=fixed\n +A: rx_lanes=1\n +A: speed=12\n +A: tx_lanes=1\n +A: urbnum=15688\n +A: version= 2.00\n + +P: /devices/pci0000:00/0000:00:14.0/usb3 +N: bus/usb/003/001=12010002090001406B1D020012060302010109021900010100E0000904000001090000000705810304000C +E: BUSNUM=003 +E: CURRENT_TAGS=:seat: +E: DEVNAME=/dev/bus/usb/003/001 +E: DEVNUM=001 +E: DEVTYPE=usb_device +E: DRIVER=usb +E: ID_AUTOSUSPEND=1 +E: ID_BUS=usb +E: ID_FOR_SEAT=usb-pci-0000_00_14_0 +E: ID_MODEL=xHCI_Host_Controller +E: ID_MODEL_ENC=xHCI\x20Host\x20Controller +E: ID_MODEL_FROM_DATABASE=2.0 root hub +E: ID_MODEL_ID=0002 +E: ID_PATH=pci-0000:00:14.0 +E: ID_PATH_TAG=pci-0000_00_14_0 +E: ID_REVISION=0612 +E: ID_SERIAL=Linux_6.12.101+deb13-amd64_xhci-hcd_xHCI_Host_Controller_0000:00:14.0 +E: ID_SERIAL_SHORT=0000:00:14.0 +E: ID_USB_INTERFACES=:090000: +E: ID_USB_MODEL=xHCI_Host_Controller +E: ID_USB_MODEL_ENC=xHCI\x20Host\x20Controller +E: ID_USB_MODEL_ID=0002 +E: ID_USB_REVISION=0612 +E: ID_USB_SERIAL=Linux_6.12.101+deb13-amd64_xhci-hcd_xHCI_Host_Controller_0000:00:14.0 +E: ID_USB_SERIAL_SHORT=0000:00:14.0 +E: ID_USB_VENDOR=Linux_6.12.101+deb13-amd64_xhci-hcd +E: ID_USB_VENDOR_ENC=Linux\x206.12.101+deb13-amd64\x20xhci-hcd +E: ID_USB_VENDOR_ID=1d6b +E: ID_VENDOR=Linux_6.12.101+deb13-amd64_xhci-hcd +E: ID_VENDOR_ENC=Linux\x206.12.101+deb13-amd64\x20xhci-hcd +E: ID_VENDOR_FROM_DATABASE=Linux Foundation +E: ID_VENDOR_ID=1d6b +E: MAJOR=189 +E: MINOR=256 +E: PRODUCT=1d6b/2/612 +E: SUBSYSTEM=usb +E: TAGS=:seat: +E: TYPE=9/0/1 +A: authorized=1\n +A: authorized_default=1\n +A: avoid_reset_quirk=0\n +A: bConfigurationValue=1\n +A: bDeviceClass=09\n +A: bDeviceProtocol=01\n +A: bDeviceSubClass=00\n +A: bMaxPacketSize0=64\n +A: bMaxPower=0mA\n +A: bNumConfigurations=1\n +A: bNumInterfaces= 1\n +A: bcdDevice=0612\n +A: bmAttributes=e0\n +A: busnum=3\n +A: configuration= +H: descriptors=12010002090001406B1D020012060302010109021900010100E0000904000001090000000705810304000C +A: dev=189:256\n +A: devnum=1\n +A: devpath=0\n +L: driver=../../../../bus/usb/drivers/usb +L: firmware_node=../../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:13/device:14 +A: idProduct=0002\n +A: idVendor=1d6b\n +A: interface_authorized_default=1\n +A: ltm_capable=no\n +A: manufacturer=Linux 6.12.101+deb13-amd64 xhci-hcd\n +A: maxchild=12\n +A: power/active_duration=15404136\n +A: power/async=enabled\n +A: power/autosuspend=0\n +A: power/autosuspend_delay_ms=0\n +A: power/connected_duration=15537064\n +A: power/control=auto\n +A: power/level=auto\n +A: power/runtime_active_kids=3\n +A: power/runtime_active_time=15404112\n +A: power/runtime_enabled=enabled\n +A: power/runtime_status=active\n +A: power/runtime_suspended_time=132949\n +A: power/runtime_usage=0\n +A: power/wakeup=disabled\n +A: power/wakeup_abort_count=\n +A: power/wakeup_active=\n +A: power/wakeup_active_count=\n +A: power/wakeup_count=\n +A: power/wakeup_expire_count=\n +A: power/wakeup_last_time_ms=\n +A: power/wakeup_max_time_ms=\n +A: power/wakeup_total_time_ms=\n +A: product=xHCI Host Controller\n +A: quirks=0x0\n +A: removable=unknown\n +A: rx_lanes=1\n +A: serial=0000:00:14.0\n +A: speed=480\n +A: tx_lanes=1\n +A: urbnum=844\n +A: version= 2.00\n + +P: /devices/pci0000:00/0000:00:14.0 +E: DRIVER=xhci_hcd +E: ID_AUTOSUSPEND=1 +E: ID_MODEL_FROM_DATABASE=Tiger Lake-LP USB 3.2 Gen 2x1 xHCI Host Controller +E: ID_PATH=pci-0000:00:14.0 +E: ID_PATH_TAG=pci-0000_00_14_0 +E: ID_PCI_CLASS_FROM_DATABASE=Serial bus controller +E: ID_PCI_INTERFACE_FROM_DATABASE=XHCI +E: ID_PCI_SUBCLASS_FROM_DATABASE=USB controller +E: ID_VENDOR_FROM_DATABASE=Intel Corporation +E: MODALIAS=pci:v00008086d0000A0EDsv00001028sd00000AFCbc0Csc03i30 +E: PCI_CLASS=C0330 +E: PCI_ID=8086:A0ED +E: PCI_SLOT_NAME=0000:00:14.0 +E: PCI_SUBSYS_ID=1028:0AFC +E: SUBSYSTEM=pci +A: ari_enabled=0\n +A: broken_parity_status=0\n +A: class=0x0c0330\n +H: config=8680EDA0060490023030030C0000800004001A536000000000000000000000000000000000000000000000002810FC0A000000007000000000000000FF010000 +A: consistent_dma_mask_bits=64\n +A: d3cold_allowed=1\n +A: device=0xa0ed\n +A: dma_mask_bits=64\n +L: driver=../../../bus/pci/drivers/xhci_hcd +A: driver_override=(null)\n +A: enable=1\n +L: firmware_node=../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:13 +L: iommu=../../virtual/iommu/dmar3 +L: iommu_group=../../../kernel/iommu_groups/10 +A: irq=179\n +A: local_cpulist=0-7\n +A: local_cpus=ff\n +A: modalias=pci:v00008086d0000A0EDsv00001028sd00000AFCbc0Csc03i30\n +A: msi_bus=1\n +A: msi_irqs/179=msi\n +A: msi_irqs/180=msi\n +A: msi_irqs/181=msi\n +A: msi_irqs/182=msi\n +A: msi_irqs/183=msi\n +A: msi_irqs/184=msi\n +A: msi_irqs/185=msi\n +A: msi_irqs/186=msi\n +A: numa_node=-1\n +A: pools=poolinfo - 0.1\nbuffer-2048 0 0 2048 0\nbuffer-512 0 0 512 0\nbuffer-128 0 0 128 0\nbuffer-32 0 0 32 0\nxHCI 1KB stream ctx arrays 0 0 1024 0\nxHCI 256 byte stream ctx arrays 0 0 256 0\nxHCI input/output contexts 8 9 2112 9\nxHCI ring segments 33 33 4096 33\nbuffer-2048 0 0 2048 0\nbuffer-512 0 0 512 0\nbuffer-128 3 32 128 1\nbuffer-32 0 0 32 0\n +A: power/async=enabled\n +A: power/control=auto\n +A: power/runtime_active_kids=1\n +A: power/runtime_active_time=15405376\n +A: power/runtime_enabled=enabled\n +A: power/runtime_status=active\n +A: power/runtime_suspended_time=132546\n +A: power/runtime_usage=0\n +A: power/wakeup=enabled\n +A: power/wakeup_abort_count=0\n +A: power/wakeup_active=0\n +A: power/wakeup_active_count=16\n +A: power/wakeup_count=0\n +A: power/wakeup_expire_count=16\n +A: power/wakeup_last_time_ms=354456\n +A: power/wakeup_max_time_ms=107\n +A: power/wakeup_total_time_ms=1646\n +A: power_state=D0\n +A: resource=0x00000060531a0000 0x00000060531affff 0x0000000000140204\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n0x0000000000000000 0x0000000000000000 0x0000000000000000\n +A: revision=0x30\n +A: subsystem_device=0x0afc\n +A: subsystem_vendor=0x1028\n +A: vendor=0x8086\n + diff --git a/tests/meson.build b/tests/meson.build index 97a5bfa16..5b0191427 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -37,6 +37,7 @@ drivers_tests = [ 'vfs5011', 'vfs7552', 'goodixmoc', + 'goodix533c', 'nb1010', 'egis0570', 'fpcmoc', From a7ab23ef98a64d39f5dc62b13791589dab82ecb7 Mon Sep 17 00:00:00 2001 From: daemonhorn Date: Sun, 23 Aug 2026 07:28:45 -0400 Subject: [PATCH 09/12] tests: document that snaplen is ruled out as the replay-gap cause Tried two fresh finger-absent recapture attempts (tshark -s 0, then -s 65535) specifically to test the snaplen hypothesis for why custom.pcapng carries zero bulk-IN reply payload bytes. Both reproduced the identical result -- every completion event capped at exactly 64 bytes, entirely consumed by usbmon's own capture header, no truncated- but-present payload. Rules out the most obvious fix and narrows what a follow-up pass needs to check next. Both recapture attempts (finger- absent, so not sensitive, but no reason to keep) were discarded. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y9XXWG7y2kfBEXJj3MS5yv --- tests/goodix533c/README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/goodix533c/README.md b/tests/goodix533c/README.md index f100c79e0..b24eecaab 100644 --- a/tests/goodix533c/README.md +++ b/tests/goodix533c/README.md @@ -163,6 +163,31 @@ Accordingly `custom.py`: Neither of the two new captures described above were made as part of adding this fixture. +**Snaplen ruled out as the cause.** After this fixture was added, two +fresh finger-absent recapture attempts were made (via +`capture_fixture_session.py`, same safe no-finger-only script, using +`tshark -i usbmon3 -s 0 ...` and then `-s 65535 ...` -- explicit +unlimited and explicit-large snap lengths respectively) specifically to +test whether a truncated capture snaplen was the cause. Both attempts +reproduced the exact same result: every bulk-IN (`0x83`) completion +event capped at exactly 64 bytes total frame length with 0 bytes of +captured payload, identical to the original fixture. Raw hex inspection +of one such frame (`tshark -x`) confirms the 64 bytes are consumed +entirely by usbmon's own binary capture header, with no payload bytes +attached at all -- not a truncated-but-present payload, a genuinely +absent one. This means the gap is not a tshark/dumpcap snaplen flag +issue; the actual cause is some other property of how `usbmon`'s +binary interface is capturing (or not capturing) this device's +bulk-IN completions on this system/kernel, not yet identified. Both +recapture attempts were deleted (they added no value and, being +finger-absent, carried no sensitivity, but there was no reason to keep +them). Whoever picks up "fix the replay gap" next should start by +ruling out something other than snaplen -- e.g. usbmon's ring buffer +size (`/sys/kernel/debug/usb/usbmon/` / `MON_IOCT_RING_SIZE`), a +`usbmon0u` text-mode capture as a simpler diagnostic cross-check, or +capturing via `dumpcap` directly instead of through `tshark`'s +wrapper. + ## Replay ```sh From e92d49de8da6a5f191e3fdaab8f649d8abca2eec Mon Sep 17 00:00:00 2001 From: daemonhorn Date: Sun, 23 Aug 2026 07:42:13 -0400 Subject: [PATCH 10/12] goodix533c: fix pre-PR blockers (build gating, GLib guard, aliasing) - Pull goodix533c out of default_drivers: it's the only driver needing OpenCV/SIGFM, so a plain `meson setup` was forcing that dependency on everyone, including unrelated in-flight driver work. Opt in via -Ddrivers=goodix533c or -Ddrivers=all. - Fix the GLib version guard around g_log_writer_default_would_drop() (2.68+) to check GLIB_VERSION_MAX_ALLOWED, the project's declared 2.56 floor, instead of GLIB_CHECK_VERSION, which just reflects the build machine's installed headers and left the deprecation warning firing on any machine with newer glib. - Replace unaligned guint16*/guint32* pointer casts in the PSK-read, sensor-register read/write command builders and the PSK-length reply parse with memcpy -- the original casts were UB and a SIGBUS risk on strict-alignment architectures. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y9XXWG7y2kfBEXJj3MS5yv --- .../drivers/goodix533c/goodix533c-auth.c | 13 ++++++++ libfprint/drivers/goodix533c/goodix533c.c | 33 ++++++++++++++----- meson.build | 10 ++++-- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/libfprint/drivers/goodix533c/goodix533c-auth.c b/libfprint/drivers/goodix533c/goodix533c-auth.c index ab49ac8c2..f3103ae61 100644 --- a/libfprint/drivers/goodix533c/goodix533c-auth.c +++ b/libfprint/drivers/goodix533c/goodix533c-auth.c @@ -52,7 +52,20 @@ static gboolean goodix533c_match_scores_need_exhaustive_logging (void) { +#if GLIB_VERSION_MAX_ALLOWED >= GLIB_VERSION_2_68 return !g_log_writer_default_would_drop (G_LOG_LEVEL_DEBUG, G_LOG_DOMAIN); +#else + /* g_log_writer_default_would_drop() is 2.68+; this project pins + * GLIB_VERSION_MAX_ALLOWED to its declared floor of 2.56 (see + * glib_min_version in meson.build), regardless of the glib actually + * installed on the build machine -- so gate on that macro, not + * GLIB_CHECK_VERSION (which reflects the build machine's headers and + * would silently produce a binary that needs a newer runtime glib than + * the project claims to support). Below 2.68 there is no cheap way to + * ask in advance whether debug logging would be dropped, so just + * always do the exhaustive per-candidate logging. */ + return TRUE; +#endif } static gboolean diff --git a/libfprint/drivers/goodix533c/goodix533c.c b/libfprint/drivers/goodix533c/goodix533c.c index f3c6e7f8e..c447f3503 100644 --- a/libfprint/drivers/goodix533c/goodix533c.c +++ b/libfprint/drivers/goodix533c/goodix533c.c @@ -484,11 +484,18 @@ cmd_preset_psk_read (FpDevice *dev, guint32 flags, guint32 length, gpointer user_data) { guint8 payload[16]; - - *(guint32 *) (payload + 0) = GUINT32_TO_LE (length); - *(guint32 *) (payload + 4) = GUINT32_TO_LE (offset); - *(guint32 *) (payload + 8) = GUINT32_TO_LE (flags); - *(guint32 *) (payload + 12) = GUINT32_TO_LE (0); + guint32 length_le = GUINT32_TO_LE (length); + guint32 offset_le = GUINT32_TO_LE (offset); + guint32 flags_le = GUINT32_TO_LE (flags); + guint32 zero_le = GUINT32_TO_LE (0); + + /* payload isn't guaranteed 4-byte aligned, so store via memcpy rather + * than an unaligned guint32* cast (UB, and a real SIGBUS risk on + * strict-alignment architectures). */ + memcpy (payload + 0, &length_le, sizeof (length_le)); + memcpy (payload + 4, &offset_le, sizeof (offset_le)); + memcpy (payload + 8, &flags_le, sizeof (flags_le)); + memcpy (payload + 12, &zero_le, sizeof (zero_le)); send_protocol (dev, GOODIX_CMD_PRESET_PSK_READ, payload, sizeof (payload), TRUE, GOODIX533C_TIMEOUT_MS, TRUE, TRUE, callback, @@ -515,9 +522,10 @@ cmd_read_sensor_register (FpDevice *dev, guint16 address, guint8 length, Goodix533cCmdCallback callback, gpointer user_data) { guint8 payload[4]; + guint16 address_le = GUINT16_TO_LE (address); payload[0] = 0x00; - *(guint16 *) (payload + 1) = GUINT16_TO_LE (address); + memcpy (payload + 1, &address_le, sizeof (address_le)); payload[3] = length; send_protocol (dev, GOODIX_CMD_READ_SENSOR_REGISTER, payload, @@ -531,9 +539,10 @@ cmd_write_sensor_register (FpDevice *dev, guint16 address, Goodix533cCmdCallback callback, gpointer user_data) { guint8 payload[5]; + guint16 address_le = GUINT16_TO_LE (address); payload[0] = 0x00; - *(guint16 *) (payload + 1) = GUINT16_TO_LE (address); + memcpy (payload + 1, &address_le, sizeof (address_le)); payload[3] = value[0]; payload[4] = value[1]; @@ -1901,7 +1910,15 @@ on_open_psk_read_reply (FpDevice *dev, guint8 *data, guint16 length, return; } - psk_length = GUINT32_FROM_LE (*(guint32 *) (data + 5)); + { + guint32 psk_length_le; + + /* data+5 isn't guaranteed 4-byte aligned; memcpy avoids the unaligned + * guint32* cast (UB, and a real SIGBUS risk on strict-alignment + * architectures). */ + memcpy (&psk_length_le, data + 5, sizeof (psk_length_le)); + psk_length = GUINT32_FROM_LE (psk_length_le); + } if (length < 9 + psk_length) { fpi_ssm_mark_failed (ssm, g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, diff --git a/meson.build b/meson.build index 44fe6068f..db79558eb 100644 --- a/meson.build +++ b/meson.build @@ -125,7 +125,6 @@ default_drivers = [ 'upekts', 'goodixmoc', 'goodixtls511', - 'goodix533c', 'nb1010', 'fpcmoc', @@ -133,6 +132,13 @@ default_drivers = [ 'elanspi', ] +# Not in default_drivers: pulls in OpenCV (see driver_helper_mapping's +# 'sigfm' helper below), which the rest of default_drivers does not +# require. Opt in explicitly with -Ddrivers=goodix533c or -Ddrivers=all. +all_drivers_only = [ + 'goodix533c', +] + # FIXME: All the drivers should be fixed by adjusting the byte order. # See https://gitlab.freedesktop.org/libfprint/libfprint/-/issues/236 endian_independent_drivers = virtual_drivers + [ @@ -140,7 +146,7 @@ endian_independent_drivers = virtual_drivers + [ 'synaptics', ] -all_drivers = default_drivers + virtual_drivers +all_drivers = default_drivers + virtual_drivers + all_drivers_only if drivers == [ 'all' ] drivers = all_drivers From 2c54b286586f651a84c2d568aa26ec95a8598a70 Mon Sep 17 00:00:00 2001 From: daemonhorn Date: Sun, 23 Aug 2026 09:45:29 -0400 Subject: [PATCH 11/12] tests: fix goodix533c fixture's zero-payload capture bug Root cause: host kernel lockdown mode (confidentiality) redacts usbmon's captured USB payload data system-wide, including for root -- confirmed via the text interface returning EPERM and the binary interface (used by both tshark and dumpcap, ruling out tool choice) reporting correct urb_len but always data_len=0. Not a driver, harness, or capture-tool bug. Fix: recapture from inside a VM whose guest kernel has no lockdown enabled, passing the physical sensor through via QEMU usb-host. The new custom.pcapng is byte-verified complete: every bulk-IN completion's data_len matches its urb_len, 14835/14835 bytes total including the full 14338-byte image-capture frame. custom.py stays scoped to discovery/feature-flag assertions rather than re-enabling open_sync()/close_sync(): replaying the corrected capture surfaces a second, separate umockdev replay desync ("Reaping discard URB... without corresponding submit") not present with the old zero-payload capture. Documented in the README's "Replay status" section for whoever picks this up next -- not resolved here. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y9XXWG7y2kfBEXJj3MS5yv --- tests/goodix533c/README.md | 219 ++++++++++++++++----------------- tests/goodix533c/custom.pcapng | Bin 37244 -> 26920 bytes 2 files changed, 109 insertions(+), 110 deletions(-) diff --git a/tests/goodix533c/README.md b/tests/goodix533c/README.md index b24eecaab..d828c4b47 100644 --- a/tests/goodix533c/README.md +++ b/tests/goodix533c/README.md @@ -19,7 +19,7 @@ moved -- the original is left in place. - `custom.py` -- driven by `tests/umockdev-test.py` (invoked via `meson test`), exercises device discovery and feature-flag assertions against the replayed session. It deliberately stops there and does not call - `open_sync()` -- see "Verified replay result" below for why. + `open_sync()` -- see "Replay status" below for why. ## Deliberately finger-absent @@ -38,81 +38,106 @@ driver now, but nothing in this fixture can safely exercise them). Any such fixture must be captured and vetted by a human outside of an automated agent, exactly as this one was. -## Verified replay result (important -- read before trusting this fixture) +## Replay status (important -- read before trusting this fixture) + +### The original zero-payload bug: root cause found, and fixed + +The first cut of this fixture (still the version described in stale form +below until this section was rewritten) could not replay past the +driver's second open() command (`0xa8`, `GOODIX_CMD_FIRMWARE_VERSION`): +every bulk-IN (`0x83`) completion in the capture had `usb.data_len == 0` +despite `usb.urb_len` correctly reporting the real transfer size -- +metadata preserved, payload always redacted. Tool choice was +conclusively ruled out first: `tools/recapture_fixture_dumpcap.sh` in the +parent project captures via `dumpcap` directly (bypassing tshark's +wrapper) and reproduces the *identical* symptom, including on the +14,338-byte real image-transfer frame. + +**Root cause: Linux kernel lockdown mode (`confidentiality`), which +redacts USB payload capture system-wide, including for root.** Confirmed +directly on the host that produced every earlier attempt: + +- `cat /sys/kernel/security/lockdown` reports `none [integrity] + confidentiality` -- confidentiality mode active. +- The usbmon **text** interface (`/sys/kernel/debug/usb/usbmon/u`) + returns `Operation not permitted` (EPERM) even as root -- the kernel's + `LOCKDOWN_USB` restriction blocking a debugfs interface outright, not a + DAC permission issue (root bypasses DAC; it cannot bypass a lockdown + LSM check). +- The usbmon **binary** interface (what both `tshark` and `dumpcap` use) + stays readable, but has its captured-data length forced to 0 on every + bulk-IN completion for this device, while `urb_len` (the real transfer + size) stays correct -- exactly the "metadata preserved, payload + redacted" shape `LOCKDOWN_USB` produces, and exactly what both tool + choices independently reproduced. + +This is intentional kernel behavior (typically auto-enabled by Secure +Boot), not a bug in the driver, the test harness, or any capture tool -- +and not something to work around by changing lockdown/Secure Boot +settings on a real machine. + +**Fix: capture from inside a VM whose guest kernel has no lockdown +enabled.** The physical sensor was passed through via QEMU +(`-device usb-host,vendorid=0x27c6,productid=0x533c`) to the existing +`vm/` Ubuntu 20.04 cloud image (already used earlier in this project for +a different capture, see `findings/vm-capture-analysis.md`), running the +same finger-absent `capture_fixture_session.py` inside the guest while +`tshark` captured on the guest's own `usbmonN`. Verified byte-exact: +every one of the 26 bulk-IN completions in the resulting capture has +`usb.data_len == usb.urb_len`, summing to 14,835/14,835 bytes across the +whole session, including the full 14,338-byte encrypted image-capture +frame. The reusable capture script is +`vm/usbmon-capture-in-vm.sh` in the parent project (plus a small +`vm/patch_future_annotations.py` helper, needed because the VM's stock +Python 3.8 predates the PEP 604/585 type-hint syntax the vendored +`goodix-fp-dump-nikicat` driver uses) -- read its header comment before +re-running it, since a future finger-present capture (see "Deliberately +finger-absent" above) will need the same mechanism. + +### Current open item: umockdev replay desync on the full-payload capture + +`custom.pcapng` now carries genuinely complete payload data (confirmed +above), but replaying it against the driver surfaces a **different, +second problem**, not present with the old zero-payload capture: -Replaying `custom.pcapng` against the real, currently-built `goodix533c` -driver (both directly via the `goodix533c-capture-test` binary, and via -`custom.py`/`meson test`) was checked while adding this fixture to this -submodule. It does **not** get as far as the protocol summary above -implies. Concretely: - -```sh -$ umockdev-run -d device \ - -p /sys/devices/pci0000:00/0000:00:14.0/usb3/3-3=custom.pcapng \ - -- .../builddir/libfprint/goodix533c-capture-test -Found: 0 (Goodix 27c6:533c Fingerprint Sensor) - driver goodix533c -Opening 0 ... - -(process:NNNNN): libfprint-goodix533c-WARNING **: Unknown pack flags: 0x00 - -(process:NNNNN): libfprint-goodix533c-WARNING **: Unknown pack flags: 0x00 -open() FAILED: Command timed out: 0xa8 +``` +umockdev-pcap.vala:158: Replay may be stuck: Reaping discard URB of type +BULK, for endpoint 0x01 with length 64 without corresponding submit ``` -`0xa8` is `GOODIX_CMD_FIRMWARE_VERSION`, the *second* command the driver's -open() sequence sends (after `nop`, whose reply -- or lack of one -- the -driver already tolerates). Under the standard `meson test` driver-test -harness, which sets `G_DEBUG=fatal-warnings`, the same underlying -condition instead aborts the process with `SIGTRAP` on the "Unknown pack -flags: 0x00" warning rather than reaching the timeout message, because -that warning becomes fatal. - -Root cause, confirmed with `tshark`'s decoded USB URB fields (not just a -manual hex read) across the *entire* capture file: every completion event -on the fingerprint device's (bus 3, address 6) bulk-IN endpoint (address -`0x83`) has `usb.data_len == 0` -- i.e. **no bulk-IN reply payload was -ever captured for this device, anywhere in this file**, even though every -outgoing bulk-OUT request was captured in full (including the later-stage -TLS ClientHello/PSK and config-upload writes -- confirmed via -`usb.endpoint_address.direction` to genuinely be host-to-device, not -misattributed replies) and the control-endpoint (EP0) enumeration traffic -has real payload. This holds for every command, not just -firmware_version -- firmware_version simply happens to be the first -command in open() that actually requires a substantive reply (`nop`'s -reply is optional by design). - -This finding was cross-checked with a control, since it's a strong claim -about an existing, already-vetted fixture: the same query -(`usb.endpoint_address==0x83 && usb.data_len>0`) against -`tests/goodixmoc/custom.pcapng` (a single-device capture with no bus -noise, known-good in upstream CI) returns 124 hits on its own endpoint -`0x83`, confirming both that the methodology correctly detects real -captured payload when present, and that a genuinely-replayable fixture -does carry it throughout. `goodix533c/custom.pcapng` returns 0 hits on -the same query, restricted to its own device's address (6) to exclude -unrelated bus traffic from another USB device (a Bluetooth adapter, -address 4) and the root hub (address 1) that happen to share the same -capture window. - -That the driver's outgoing requests visibly *progress* through the whole -open() sequence in this capture (firmware_version, PSK read, reset, chip -ID/OTP reads, then a multi-packet TLS ClientHello/PSK and config-upload -write sequence) shows the real hardware genuinely replied at each stage -during the original live session -- otherwise the driver could never -have gotten far enough to send those later commands. What's missing is -specifically the *captured* reply payload, i.e. a property of how this -file was recorded, not of what happened on the wire when it was recorded. - -**Practical effect on `custom.py`**: it does not call `open_sync()` (or -anything past it) at all, precisely because of this gap -- see the file -for the reasoning inline. It only asserts device discovery and feature -flags, which are fully verifiable against this fixture. `meson test`'s -`goodix533c` entry is expected to PASS with that reduced scope. A -previous draft of this fixture called `open_sync()`/`close_sync()` -unconditionally and documented the resulting failure instead of avoiding -it; that was reverted in favor of keeping the suite green and putting the -gap here, in the README, and in the task report instead of in a -permanently-red test. +...followed by the same `Command timed out: 0xa8` outcome. Diagnostic +work so far (with `G_MESSAGES_DEBUG=all umockdev-run ...`): + +- The two captures are structurally near-identical for the whole + `nop`/`firmware_version` exchange -- same submit/complete ordering, + same cancelled-read pattern for `nop`'s tolerant no-reply timeout, same + write byte content. Replaying the *old* zero-payload capture against + the current driver build reproduces its originally-documented behavior + exactly (two `Unknown pack flags: 0x00` warnings, then the timeout) -- + no "stuck"/discard message at all. +- The divergence is therefore specifically triggered by the presence of + real, non-empty reply payload (the `firmware_version` reply now + arrives as a real `COMMAND_ACK` then real `COMMAND_FIRMWARE_VERSION` + data, versus two empty reads before) -- something in umockdev's own + URB submit/complete bookkeeping desyncs once there's real data to + track, rather than an ordering or content mismatch in the capture + itself. +- Root-hub traffic interleaving (device address 1 vs the sensor's device + address 2) was ruled out as the cause -- filtering the capture to only + the sensor's own traffic (`usb.device_address == 2`) makes no + difference. + +**Not yet resolved.** `custom.py` is therefore left as-is (device +discovery and feature-flag assertions only, `open_sync()` still not +called) until this is understood -- re-enabling it on a capture that is +known not to replay would trade a documented, honest gap for a silently +broken test. `custom.pcapng` itself is worth keeping as-is regardless: +it is the *complete, correct* protocol capture (confirmed byte-exact), +which is what any future recapture would need to start from, and is +already strictly more useful than the zero-payload version for anyone +debugging this further (e.g. by decoding it with +`tools/decode_capture.py`/`tools/parse_capture.py` in the parent +project). ## Current scope and limitations @@ -139,14 +164,13 @@ Accordingly `custom.py`: **Follow-up needed, in two independent stages:** -1. **Fix the replay gap first.** A corrected `custom.pcapng` (or a - replacement fixture) is needed that retains bulk-IN reply payload - data -- still finger-absent, still stopping before any live - `mcu_get_image` reply, just captured with a method that doesn't drop - the device's response bytes. Once that exists, add - `open_sync()`/`close_sync()` back into `custom.py` (they were removed - from this version specifically because the current fixture can't - support them -- see "Verified replay result" above) and confirm they +1. **Resolve the umockdev replay desync described in "Replay status" + above.** The capture itself is no longer the blocker (it has complete + payload data, byte-verified); what's blocking is umockdev's own + submit/complete bookkeeping getting stuck partway through replaying + it. Once `custom.pcapng` replays cleanly end to end, add + `open_sync()`/`close_sync()` back into `custom.py` (removed from this + version because the fixture couldn't support them) and confirm they pass. 2. **Then, once SIGFM enroll/verify/identify work is complete**, extend `custom.py` (or add a second fixture-specific test file) to drive @@ -158,35 +182,10 @@ Accordingly `custom.py`: `mcu_get_image` replies -- which, per the constraint above, must be captured and safety-reviewed by a human, never generated by an agent, and only committed if the human is certain they're comfortable with - those frames being third-party-decryptable (the PSK is public). - -Neither of the two new captures described above were made as part of -adding this fixture. - -**Snaplen ruled out as the cause.** After this fixture was added, two -fresh finger-absent recapture attempts were made (via -`capture_fixture_session.py`, same safe no-finger-only script, using -`tshark -i usbmon3 -s 0 ...` and then `-s 65535 ...` -- explicit -unlimited and explicit-large snap lengths respectively) specifically to -test whether a truncated capture snaplen was the cause. Both attempts -reproduced the exact same result: every bulk-IN (`0x83`) completion -event capped at exactly 64 bytes total frame length with 0 bytes of -captured payload, identical to the original fixture. Raw hex inspection -of one such frame (`tshark -x`) confirms the 64 bytes are consumed -entirely by usbmon's own binary capture header, with no payload bytes -attached at all -- not a truncated-but-present payload, a genuinely -absent one. This means the gap is not a tshark/dumpcap snaplen flag -issue; the actual cause is some other property of how `usbmon`'s -binary interface is capturing (or not capturing) this device's -bulk-IN completions on this system/kernel, not yet identified. Both -recapture attempts were deleted (they added no value and, being -finger-absent, carried no sensitivity, but there was no reason to keep -them). Whoever picks up "fix the replay gap" next should start by -ruling out something other than snaplen -- e.g. usbmon's ring buffer -size (`/sys/kernel/debug/usb/usbmon/` / `MON_IOCT_RING_SIZE`), a -`usbmon0u` text-mode capture as a simpler diagnostic cross-check, or -capturing via `dumpcap` directly instead of through `tshark`'s -wrapper. + those frames being third-party-decryptable (the PSK is public). Use + `vm/usbmon-capture-in-vm.sh` (parent project) for the underlying + capture mechanism -- it's the only one confirmed to retain full + payload data on a lockdown-enabled host. ## Replay diff --git a/tests/goodix533c/custom.pcapng b/tests/goodix533c/custom.pcapng index 9d7266059788762fc093135a6de1fccde710f8aa..a552f4b1844ffc4d737869f1ddf1cf3a69aef3b7 100644 GIT binary patch literal 26920 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 literal 37244 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 VdwVbPC1SgsJxNQ#wo|B#{{yG{X5s(< From 9484a83d8280552feaa80c471f218a0e98b9f82a Mon Sep 17 00:00:00 2001 From: daemonhorn Date: Sun, 23 Aug 2026 10:12:53 -0400 Subject: [PATCH 12/12] tests: fix goodix533c fixture's bus/device-address mismatch The fixture's `device` file still declared busnum=3/devnum=6 from the original host capture; the VM-recaptured custom.pcapng (previous commit) recorded the sensor at bus=1/device=2, its own USB topology. umockdev's pcap replay needs these to match or its submit/complete bookkeeping desyncs silently -- surfacing as the same "Reaping discard URB... without corresponding submit" message regardless of cause, which is what made this take so long to isolate (ruled out first: write ordering, urb_id reuse, root-hub interleaving, and reply payload content itself, none of which were it). Relabeling every packet's busnum/devnum fields to 3/6 (a mechanical, structure-preserving rewrite -- payload data reconfirmed byte-identical afterward) fixes replay completely through the non-TLS portion of open(): nop, firmware_version, preset_psk_read, reset, read_sensor_register, read_otp, and request_tls_connection all now replay and decode correctly. Replay still can't get past the TLS handshake itself, but this is now a confirmed structural limitation rather than an open question: traced through goodix533c.c, on_request_tls_connection_reply() feeds the replayed ClientHello into the driver's own embedded TLS server, whose SSL_accept() generates a genuinely fresh ServerHello (new randomness, new ECDHE keys) every run -- output that can never byte-match a previously recorded session. No pcap fix can address this. Documented in the README along with a concrete follow-up: a scoped test stopping before TLS, which this fixture can now actually support. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Y9XXWG7y2kfBEXJj3MS5yv --- tests/goodix533c/README.md | 140 ++++++++++++++++++++------------- tests/goodix533c/custom.pcapng | Bin 26920 -> 26920 bytes 2 files changed, 85 insertions(+), 55 deletions(-) diff --git a/tests/goodix533c/README.md b/tests/goodix533c/README.md index d828c4b47..d9a2e5b10 100644 --- a/tests/goodix533c/README.md +++ b/tests/goodix533c/README.md @@ -94,50 +94,77 @@ Python 3.8 predates the PEP 604/585 type-hint syntax the vendored re-running it, since a future finger-present capture (see "Deliberately finger-absent" above) will need the same mechanism. -### Current open item: umockdev replay desync on the full-payload capture - -`custom.pcapng` now carries genuinely complete payload data (confirmed -above), but replaying it against the driver surfaces a **different, -second problem**, not present with the old zero-payload capture: - -``` -umockdev-pcap.vala:158: Replay may be stuck: Reaping discard URB of type -BULK, for endpoint 0x01 with length 64 without corresponding submit -``` - -...followed by the same `Command timed out: 0xa8` outcome. Diagnostic -work so far (with `G_MESSAGES_DEBUG=all umockdev-run ...`): - -- The two captures are structurally near-identical for the whole - `nop`/`firmware_version` exchange -- same submit/complete ordering, - same cancelled-read pattern for `nop`'s tolerant no-reply timeout, same - write byte content. Replaying the *old* zero-payload capture against - the current driver build reproduces its originally-documented behavior - exactly (two `Unknown pack flags: 0x00` warnings, then the timeout) -- - no "stuck"/discard message at all. -- The divergence is therefore specifically triggered by the presence of - real, non-empty reply payload (the `firmware_version` reply now - arrives as a real `COMMAND_ACK` then real `COMMAND_FIRMWARE_VERSION` - data, versus two empty reads before) -- something in umockdev's own - URB submit/complete bookkeeping desyncs once there's real data to - track, rather than an ordering or content mismatch in the capture - itself. -- Root-hub traffic interleaving (device address 1 vs the sensor's device - address 2) was ruled out as the cause -- filtering the capture to only - the sensor's own traffic (`usb.device_address == 2`) makes no - difference. - -**Not yet resolved.** `custom.py` is therefore left as-is (device -discovery and feature-flag assertions only, `open_sync()` still not -called) until this is understood -- re-enabling it on a capture that is -known not to replay would trade a documented, honest gap for a silently -broken test. `custom.pcapng` itself is worth keeping as-is regardless: -it is the *complete, correct* protocol capture (confirmed byte-exact), -which is what any future recapture would need to start from, and is -already strictly more useful than the zero-payload version for anyone -debugging this further (e.g. by decoding it with -`tools/decode_capture.py`/`tools/parse_capture.py` in the parent -project). +### Second bug found and fixed: bus/device-address mismatch + +Replaying the payload-complete capture still hit the same-looking +`umockdev-pcap.vala:158: Replay may be stuck: Reaping discard URB of type +BULK, for endpoint 0x01 with length 64 without corresponding submit` +message. Ruled out first (via `G_MESSAGES_DEBUG=all umockdev-run` plus +`tools/decode_capture.py`/`tools/parse_capture.py`-based frame-by-frame +comparison against the old capture): write ordering, `nop`'s +cancelled-read pattern, root-hub traffic interleaving, `urb_id` reuse or +collision (usbmon IDs are raw kernel pointers and get reused constantly +in both captures -- confirmed harmless in both), and reply payload +content itself (redacting every captured byte back to the old capture's +all-zero shape, while keeping the same frame count/structure, still hung +identically). + +**Actual cause**: the VM capture recorded the sensor at `bus=1, +device=2` (the VM's own USB topology), but `device` in this fixture still +declares `busnum=3, devnum=6` (the *original* host capture's numbers, +untouched since this fixture's very first version). umockdev's pcap +replay apparently needs the trace's own recorded bus/device address to +match what the mocked `device` file declares, or its submit/complete +matching desyncs -- silently, with no error naming the actual mismatch. +Relabeling every packet's `busnum`/`devnum` fields in the capture (a +mechanical, structure-preserving rewrite -- see the note below) to 3/6 +fixed this completely: replay now proceeds correctly through the +*entire* non-TLS open() sequence -- `nop`, `firmware_version`, +`preset_psk_read`, `reset`, `read_sensor_register`, `read_otp`, and +`request_tls_connection` all replay and decode exactly as captured. + +### Third, structural limitation: TLS handshake replay is not fixable this way + +With the bus/device fix in place, replay gets all the way to the TLS +handshake before failing (`TLS handshake failed: transfer timed out`, +plus one more "stuck" message). Traced directly through +`libfprint/drivers/goodix533c/goodix533c.c`: `on_request_tls_connection_reply` +takes the device's (replayed, real) ClientHello and feeds it into the +driver's own embedded TLS server (`goodix_tls_client_write`, backed by a +genuine `SSL_accept()` in `goodixtls.c`). `tls_handshake_run`'s first +state, `TLS_STAGE_HELLO_S`, then reads that embedded server's own +**freshly generated** `ServerHello` (`goodix_tls_client_read` -- new +random values and a new ECDHE key pair every single run, exactly as real +TLS requires) and sends *that* out over USB. + +This is not a umockdev bug, and not something a better capture or a +smarter pcap edit can fix: the driver's outgoing TLS bytes are +genuinely non-deterministic by design, so they can never byte-match (or +even length-match) whatever a *previously recorded* session happened to +produce. Static pcap replay is fundamentally the wrong tool for testing +past this point without either mocking the TLS layer itself for tests +(e.g. a deterministic PRNG hook, out of scope for a driver that must use +real crypto in production) or having umockdev tolerate arbitrary +OUT-direction content past a certain stage (not something this fixture +controls). + +**Practical effect**: `custom.py` stays as-is (device discovery and +feature-flag assertions only). `open_sync()` cannot be added back via +this mechanism -- not because the fixture is incomplete, but because the +open() sequence's TLS stage is inherently unreplayable this way. Anyone +revisiting this should treat "get `open_sync()` passing under `custom.py`" +as requiring a different testing strategy for the TLS portion specifically +(e.g. stopping the umockdev-driven test at `request_tls_connection`, +verified up through there now, rather than attempting a full `open()`), +not as a capture-quality problem to keep chasing. + +**Note on the bus/device relabeling**: rewriting `busnum`/`devnum` is a +simple in-place edit of each packet's usbmon capture header (`busnum` and +`devnum` are literal fields in that header -- see the format doc at the +top of `tools/parse_capture.py` in the parent project) and touches +nothing else; it was verified afterward that every byte of payload data +was still intact (`usb.data_len == usb.urb_len` for all 26 bulk-IN +completions, 14835/14835 bytes total, same as before relabeling). ## Current scope and limitations @@ -162,17 +189,20 @@ Accordingly `custom.py`: which is exactly what must not be committed, on top of the open() replay gap making it moot anyway. -**Follow-up needed, in two independent stages:** - -1. **Resolve the umockdev replay desync described in "Replay status" - above.** The capture itself is no longer the blocker (it has complete - payload data, byte-verified); what's blocking is umockdev's own - submit/complete bookkeeping getting stuck partway through replaying - it. Once `custom.pcapng` replays cleanly end to end, add - `open_sync()`/`close_sync()` back into `custom.py` (removed from this - version because the fixture couldn't support them) and confirm they - pass. -2. **Then, once SIGFM enroll/verify/identify work is complete**, extend +**Follow-up needed:** + +1. **Add a scoped replay test that stops before TLS.** `custom.pcapng` + now replays correctly through the entire non-TLS open() sequence (see + "Replay status" above) -- `nop` through `request_tls_connection` all + decode exactly as captured. A test that exercises up through there + (rather than a full `open_sync()`, which requires the TLS stage to + also replay -- structurally not possible per "Third, structural + limitation" above) would be genuine, valuable coverage this fixture + can actually support today. This likely needs a small test-only entry + point in the driver (there's already a precedent: + `goodix533c-capture-test`), since `FpDevice`'s public API doesn't + expose a way to stop mid-open(). +2. **Once SIGFM enroll/verify/identify work is complete**, extend `custom.py` (or add a second fixture-specific test file) to drive `enroll_sync()`/`verify_sync()`/`identify_sync()`, modeled on `tests/fpcmoc/custom.py` or `tests/elanmoc/custom.py` (both diff --git a/tests/goodix533c/custom.pcapng b/tests/goodix533c/custom.pcapng index a552f4b1844ffc4d737869f1ddf1cf3a69aef3b7..550f6e73fba63557939e55557cfad7fbdf50b0cd 100644 GIT binary patch delta 784 zcmXAmJ4ixd7=}GwQ`5Y3yqDLL9&b%TK@Jg)1`Q4^K`xm@P+;Iv6c`w|B(^0q2rfbq zE^Q4BjR_774Gs=M4M7fV^}XL&UY_@T{_`I=-GR^@2=@s=sZ}*o!VX14C{lbgZ7Gk5 zUX<7*6}GwxG-1lCK|S>enUV%n(;N~_nF?BzeGX9vs_5FJh%-zaLyn9d+fa`!V?eYF zWjY`jLA%BaqOlQXZ5HL2BWHs7U_xm%Uz6D}!_3X0>~Lr-Fpn%K-*V@SsUWk z22*bb?b|^^JIu;F${|Na#C9j56dadyd))z3cA~6vOq?(aE|h)O4Vig2%r7^}yobrd zZdWoF=igQYfb!g*3M3G?kCR<>Ct6Tib&wm7vx*#-)uv-+H^c delta 784 zcmXAmJxBs!9L9T3%`VeU?eepaORqY=nuLNHA{-4G99n{0Dv6-Lz@;cKxS%Det)M|@ z5sGkWYiMXpxS^q;!9l1Y$f2$N&;OQ(-}C(5dk^lmC2m{dE-55Q)Y4*&;!G4NzL{2} zJ)&m`n{LBq+CUwqydBiA?~|!JKvl;MQJ1OEi?Yd~IzeUUH7RNz6Nkl-m9beewroG* zqQ683gbTFpIwoqnU{(iEwmEWcnD=g!Ue7t1bq~zkAj&$2LxH)gpnPRo@WOoeqFnGH zu6!_MKWNJjviva1LntkdtcvYMMJa|)=yr1$rV&7Ca&!VP3qh34;3b*)5X`R-%6yn9 z%oIU1A_ruqqoA`W-tx<|I`T-HI(kjf9EGBeF^w^eGmSH8h_Z&ad}!E?CQyD%FvXZ+ zOmU_-lg^|wO)_yTPL&i%^rYvPKTk_N|Bq)|t{p0KwV$Z~eVuz6cUGoxHe}#zh2zb@ zmP??lC-Aq8B+Qp2N+X3ZQ!u+JY!_*iU5-Kq+f#