diff --git a/libfprint/drivers/goodix533c/capture_test.c b/libfprint/drivers/goodix533c/capture_test.c new file mode 100644 index 000000000..cdc4466d5 --- /dev/null +++ b/libfprint/drivers/goodix533c/capture_test.c @@ -0,0 +1,233 @@ +/* + * 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 [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 + +#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); +} + +/* 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) +{ + 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_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, 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; + + /* 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) + { + 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; + } + } + else + { + g_print ("No reference frame captured.\n"); + } + + 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 (error) + { + 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); +} + +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_wait_for_finger, + 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-auth.c b/libfprint/drivers/goodix533c/goodix533c-auth.c new file mode 100644 index 000000000..f3103ae61 --- /dev/null +++ b/libfprint/drivers/goodix533c/goodix533c-auth.c @@ -0,0 +1,504 @@ +/* + * 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) +{ +#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 +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 new file mode 100644 index 000000000..c447f3503 --- /dev/null +++ b/libfprint/drivers/goodix533c/goodix533c.c @@ -0,0 +1,2195 @@ +/* + * 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" +#include "goodix533c-private.h" +#include "goodix533c-enroll.h" +#include "goodix533c-auth.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) + +/* 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 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 }; + +/* 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 + * 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) + +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 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, + 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]; + guint16 address_le = GUINT16_TO_LE (address); + + payload[0] = 0x00; + memcpy (payload + 1, &address_le, sizeof (address_le)); + 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]; + guint16 address_le = GUINT16_TO_LE (address); + + payload[0] = 0x00; + memcpy (payload + 1, &address_le, sizeof (address_le)); + 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_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) +{ + 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 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). + * ======================================================================= */ + +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. 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 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 -- + * 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++) + { + 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); +} + +/** + * 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) +{ + guint8 decrypt_buf[65535]; + int decrypted; + + if (length <= GOODIX533C_IMAGE_REPLY_HEADER_LEN) + { + 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 + * 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), error); + if (decrypted <= 0) + { + 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) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "short decrypt: %d < %d", decrypted, + GOODIX533C_IMAGE_BYTES); + return FALSE; + } + + *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; +} + +/** + * 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_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); + guint16 *raw_pixels = NULL; + GError *decode_error = NULL; + + if (error) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + + 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->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) + { + fpi_ssm_mark_failed (ssm, error); + return; + } + + 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); +} + +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_device_report_finger_status_changes (dev, FP_FINGER_STATUS_PRESENT, + FP_FINGER_STATUS_NEEDED); + 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)); + } +} + +/** + * 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) +{ + guint32 clipped = 0; + guint32 i; + + for (i = 0; i < GOODIX533C_IMAGE_PIXELS; i++) + if (img12[i] >= GOODIX533C_RAW12_CLIP) + clipped++; + + return (double) clipped / GOODIX533C_IMAGE_PIXELS; +} + +/* =========================================================================== + * 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(). + * ======================================================================= */ + +enum ref_capture_stage { + REF_CAPTURE_ON = 0, + REF_CAPTURE_GET_IMAGE, + REF_CAPTURE_OFF, + REF_CAPTURE_NUM_STATES, +}; + +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 REF_CAPTURE_GET_IMAGE: + cmd_mcu_get_image_gain (dev, GOODIX533C_IMAGE_FLAGS_CALIBRATE, + GOODIX533C_IMAGE_GAIN, on_ref_get_image_reply, + ssm); + break; + + case REF_CAPTURE_OFF: + cmd_write_sensor_register (dev, GOODIX533C_CAPTURE_REGISTER, + capture_off, on_capture_step_reply, ssm); + break; + + 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 FINGER_WAIT_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 FINGER_WAIT_FDT_ARM_DOWN: + { + 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 FINGER_WAIT_WAIT_FOR_FINGER: + { + FingerWaitData *data = fpi_ssm_get_data (ssm); + + 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 FINGER_WAIT_FDT_MODE_ARM: + { + 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; + + 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 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_live_get_image_reply, ssm); + break; + + case LIVE_CAPTURE_OFF: + cmd_write_sensor_register (dev, GOODIX533C_CAPTURE_REGISTER, + capture_off, on_capture_step_reply, ssm); + break; + + 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: + { + 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_finger_up_reply, ssm); + } + break; + + default: + g_assert_not_reached (); + } +} + +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_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); + CaptureTestData *data = fpi_ssm_get_data (ssm); + guint16 *raw_pixels = NULL; + g_autofree guint8 *squashed = NULL; + guint8 *corrected = NULL; + + if (self->have_reference && self->reference_pixels) + { + raw_pixels = self->reference_pixels; + squashed = g_new0 (guint8, GOODIX533C_IMAGE_PIXELS); + squash_frame_linear (self->reference_pixels, squashed, + GOODIX533C_IMAGE_PIXELS); + } + + 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 +fpi_device_goodix533c_capture_test (FpDevice *dev, + Goodix533cProgressFunc wait_for_finger_cb, + Goodix533cCaptureDoneFunc callback, + gpointer user_data) +{ + CaptureTestData *data = g_new0 (CaptureTestData, 1); + FpiSsm *ssm; + + data->wait_for_finger_cb = wait_for_finger_cb; + data->callback = callback; + data->user_data = user_data; + + 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); +} + +/* =========================================================================== + * 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). + * + * 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, +}; + +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; + } + + { + 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, + "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; + + 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 (); + } +} + +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; + + /* 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; + + /* 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), + GOODIX533C_USB_INTERFACE, 0, &error); + self->interface_claimed = FALSE; + } + + 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 + * ======================================================================= */ + +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_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); +} + +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 = GOODIX533C_ENROLL_SAMPLES; + dev_class->temp_hot_seconds = -1; + + 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); +} diff --git a/libfprint/drivers/goodix533c/goodix533c.h b/libfprint/drivers/goodix533c/goodix533c.h new file mode 100644 index 000000000..2e6ef892a --- /dev/null +++ b/libfprint/drivers/goodix533c/goodix533c.h @@ -0,0 +1,116 @@ +/* + * 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) + +/* 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. 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): 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, + const guint16 *raw_pixels, + const guint8 *squashed, + const guint16 *live_raw_pixels, + const guint8 *corrected, + 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 -> 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); 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; }; /** diff --git a/libfprint/meson.build b/libfprint/meson.build index 6df412a34..32b2ca38d 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -141,6 +141,11 @@ driver_sources = { [ 'drivers/goodixmoc/goodix.c', 'drivers/goodixmoc/goodix_proto.c' ], 'goodixtls511' : [ 'drivers/goodixtls/goodix511.c' ], + 'goodix533c' : + [ 'drivers/goodix533c/goodix533c.c', + 'drivers/goodix533c/goodix533c-match.c', + 'drivers/goodix533c/goodix533c-enroll.c', + 'drivers/goodix533c/goodix533c-auth.c' ], 'fpcmoc' : [ 'drivers/fpcmoc/fpc.c' ], } @@ -158,6 +163,8 @@ helper_sources = { [ ], 'udev' : [ ], + 'sigfm' : + [ ], 'virtual' : [ 'drivers/virtual-device-listener.c' ], } @@ -259,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, @@ -278,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) @@ -296,15 +319,28 @@ 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, ] ) +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..db79558eb 100644 --- a/meson.build +++ b/meson.build @@ -132,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 + [ @@ -139,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 @@ -160,6 +167,7 @@ driver_helper_mapping = { 'uru4000' : [ 'nss' ], 'elanspi' : [ 'udev' ], 'goodixtls511' : [ 'goodixtls' ], + 'goodix533c' : [ 'goodixtls', 'sigfm' ], 'virtual_image' : [ 'virtual' ], 'virtual_device' : [ 'virtual' ], 'virtual_device_storage' : [ 'virtual' ], @@ -198,6 +206,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 @@ -245,6 +259,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 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 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; +} diff --git a/tests/goodix533c/README.md b/tests/goodix533c/README.md new file mode 100644 index 000000000..d9a2e5b10 --- /dev/null +++ b/tests/goodix533c/README.md @@ -0,0 +1,242 @@ +# 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 "Replay status" 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. + +## 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. + +### 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 + +`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:** + +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 + 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). 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 + +```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 000000000..550f6e73f Binary files /dev/null and b/tests/goodix533c/custom.pcapng differ 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',