Initial GXFP5130 userspace prototype

This commit is contained in:
2026-08-03 11:43:42 +03:00
commit 088d8ef75c
30 changed files with 5318 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
# libfprint GXFP5130 integration skeleton
This driver connects libfprint's `FpImageDevice` state machine to the
hardware-tested `gxfp_session` API. Blocking sensor operations run in a GLib
worker thread; all libfprint callbacks run in the main context.
Current discovery is intentionally explicit because the sensor is an ACPI
platform/character device, while upstream libfprint has no generic ACPI char
device discovery type. Build the driver as an optional virtual-type driver and
start libfprint/fprintd with:
```sh
FP_GXFP5130=/dev/gxfp
```
The integration must add `gxfp5130` to `drivers_info` and
`libfprint_drivers_sources` in upstream Meson, include this file, add the GXFP
headers, and link the PIC-built `libgxfp.a` plus mbedTLS libraries.
The first image path uses the verified 64x80 sample order and the deterministic
`gxfp_image12_to_u8()` conversion. No transpose, flip, inversion, percentile
clipping, or unverified `0x21` command is applied.
Before upstreaming, replace the environment-backed discovery with a dedicated
udev/ACPI character-device discovery mechanism and install permissions for
`/dev/gxfp`.

404
libfprint-driver/gxfp5130.c Normal file
View File

@@ -0,0 +1,404 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#define FP_COMPONENT "gxfp5130"
#include "drivers_api.h"
#include "gxfp/session.h"
#include <errno.h>
#include <limits.h>
#include <string.h>
#include <glib/gstdio.h>
#include <unistd.h>
#define GXFP_DEVICE_PATH_DEFAULT "/dev/gxfp"
#define GXFP_ACTIVATE_TIMEOUT_MS 10000
#define GXFP_FINGER_TIMEOUT_MS INT_MAX
#define GXFP_CAPTURE_TIMEOUT_MS 20000
#define GXFP_IMAGE_WIDTH 64
#define GXFP_IMAGE_HEIGHT 80
#define GXFP_IMAGE_PPMM (500.0 / 25.4)
typedef enum {
GXFP_JOB_OPEN,
GXFP_JOB_ACTIVATE,
GXFP_JOB_WAIT_FINGER_DOWN,
GXFP_JOB_CAPTURE,
GXFP_JOB_WAIT_FINGER_UP,
GXFP_JOB_DEACTIVATE,
GXFP_JOB_CLOSE,
} GxfpJob;
typedef struct {
GxfpJob job;
gint result;
guint8 *image_data;
gsize image_size;
} GxfpJobResult;
struct _FpiDeviceGxfp5130
{
FpImageDevice parent;
struct gxfp_session *session;
gchar *device_path;
gchar *debug_image_dir;
guint64 capture_sequence;
gboolean worker_running;
gboolean deactivating;
};
G_DECLARE_FINAL_TYPE (FpiDeviceGxfp5130, fpi_device_gxfp5130,
FPI, DEVICE_GXFP5130, FpImageDevice)
G_DEFINE_TYPE (FpiDeviceGxfp5130, fpi_device_gxfp5130, FP_TYPE_IMAGE_DEVICE)
static void
gxfp_save_debug_image (FpiDeviceGxfp5130 *self,
const guint8 *data,
gsize size)
{
g_autofree gchar *filename = NULL;
g_autofree gchar *contents = NULL;
g_autoptr(GError) error = NULL;
gsize header_size;
if (!self->debug_image_dir || !self->debug_image_dir[0])
return;
if (size != GXFP_IMAGE_WIDTH * GXFP_IMAGE_HEIGHT)
{
fp_warn ("Refusing to save debug image with invalid size %" G_GSIZE_FORMAT,
size);
return;
}
if (g_mkdir_with_parents (self->debug_image_dir, 0700) < 0)
{
fp_warn ("Failed to create debug image directory %s: %s",
self->debug_image_dir, g_strerror (errno));
return;
}
self->capture_sequence++;
filename = g_strdup_printf ("%s/capture-%u-%06" G_GUINT64_FORMAT ".pgm",
self->debug_image_dir,
(guint) getpid (),
self->capture_sequence);
contents = g_malloc (32 + size);
header_size = g_snprintf (contents, 32, "P5\n%d %d\n255\n",
GXFP_IMAGE_WIDTH, GXFP_IMAGE_HEIGHT);
memcpy (contents + header_size, data, size);
if (!g_file_set_contents (filename, contents, header_size + size, &error))
fp_warn ("Failed to save debug image %s: %s", filename, error->message);
else
fp_dbg ("Saved raw debug image to %s", filename);
}
static void gxfp_start_job (FpiDeviceGxfp5130 *self, GxfpJob job);
static GError *
gxfp_error_new (gint result)
{
gint error_number = result < 0 ? -result : EIO;
return g_error_new (G_IO_ERROR, g_io_error_from_errno (error_number),
"GXFP session failed: %s (%d)",
g_strerror (error_number), result);
}
static void
gxfp_job_result_free (GxfpJobResult *result)
{
if (!result)
return;
g_free (result->image_data);
g_free (result);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC (GxfpJobResult, gxfp_job_result_free)
static void
gxfp_job_thread (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (source_object);
GxfpJob job = GPOINTER_TO_INT (task_data);
GxfpJobResult *result = g_new0 (GxfpJobResult, 1);
(void) cancellable;
result->job = job;
switch (job)
{
case GXFP_JOB_OPEN:
result->result = gxfp_session_open (&self->session, self->device_path);
break;
case GXFP_JOB_ACTIVATE:
result->result = gxfp_session_activate (self->session,
GXFP_ACTIVATE_TIMEOUT_MS);
break;
case GXFP_JOB_WAIT_FINGER_DOWN:
result->result = gxfp_session_arm_finger_down (self->session);
if (!result->result)
result->result = gxfp_session_wait_finger_down (
self->session, GXFP_FINGER_TIMEOUT_MS);
break;
case GXFP_JOB_CAPTURE:
{
struct gxfp_image12 image = { 0 };
result->result = gxfp_session_capture (self->session, &image,
GXFP_CAPTURE_TIMEOUT_MS);
if (!result->result)
{
result->image_size = image.width * image.height;
result->image_data = g_malloc (result->image_size);
result->result = gxfp_image12_to_u8 (&image,
result->image_data,
result->image_size);
}
gxfp_image12_clear (&image);
}
break;
case GXFP_JOB_WAIT_FINGER_UP:
result->result = gxfp_session_arm_finger_up (self->session);
if (!result->result)
result->result = gxfp_session_wait_finger_up (
self->session, GXFP_FINGER_TIMEOUT_MS);
break;
case GXFP_JOB_DEACTIVATE:
result->result = gxfp_session_deactivate (self->session);
break;
case GXFP_JOB_CLOSE:
gxfp_session_close (self->session);
self->session = NULL;
result->result = 0;
break;
default:
result->result = -EINVAL;
break;
}
g_task_return_pointer (task, result, (GDestroyNotify) gxfp_job_result_free);
}
static void
gxfp_finish_deactivate (FpiDeviceGxfp5130 *self, gint result)
{
self->deactivating = FALSE;
fpi_image_device_deactivate_complete (
FP_IMAGE_DEVICE (self), result ? gxfp_error_new (result) : NULL);
}
static void
gxfp_job_done (GObject *source_object, GAsyncResult *res, gpointer user_data)
{
FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (source_object);
g_autoptr(GxfpJobResult) result = g_task_propagate_pointer (G_TASK (res), NULL);
FpImageDevice *image_device = FP_IMAGE_DEVICE (self);
(void) user_data;
self->worker_running = FALSE;
if (self->deactivating && result->job != GXFP_JOB_DEACTIVATE)
{
gxfp_start_job (self, GXFP_JOB_DEACTIVATE);
return;
}
if (result->result)
{
GError *error = gxfp_error_new (result->result);
switch (result->job)
{
case GXFP_JOB_OPEN:
fpi_image_device_open_complete (image_device, error);
break;
case GXFP_JOB_ACTIVATE:
fpi_image_device_activate_complete (image_device, error);
break;
case GXFP_JOB_DEACTIVATE:
gxfp_finish_deactivate (self, result->result);
g_error_free (error);
break;
case GXFP_JOB_CLOSE:
fpi_image_device_close_complete (image_device, error);
break;
case GXFP_JOB_WAIT_FINGER_DOWN:
case GXFP_JOB_CAPTURE:
case GXFP_JOB_WAIT_FINGER_UP:
default:
fpi_image_device_session_error (image_device, error);
break;
}
return;
}
switch (result->job)
{
case GXFP_JOB_OPEN:
fpi_image_device_open_complete (image_device, NULL);
break;
case GXFP_JOB_ACTIVATE:
fpi_image_device_activate_complete (image_device, NULL);
break;
case GXFP_JOB_WAIT_FINGER_DOWN:
fpi_image_device_report_finger_status (image_device, TRUE);
break;
case GXFP_JOB_CAPTURE:
{
FpImage *image;
g_assert (result->image_size == GXFP_IMAGE_WIDTH * GXFP_IMAGE_HEIGHT);
image = fp_image_new (GXFP_IMAGE_WIDTH, GXFP_IMAGE_HEIGHT);
memcpy (image->data, result->image_data, result->image_size);
image->ppmm = GXFP_IMAGE_PPMM;
image->flags = FPI_IMAGE_NONE;
gxfp_save_debug_image (self, result->image_data, result->image_size);
fpi_image_device_image_captured (image_device, image);
}
break;
case GXFP_JOB_WAIT_FINGER_UP:
fpi_image_device_report_finger_status (image_device, FALSE);
break;
case GXFP_JOB_DEACTIVATE:
gxfp_finish_deactivate (self, 0);
break;
case GXFP_JOB_CLOSE:
g_clear_pointer (&self->device_path, g_free);
g_clear_pointer (&self->debug_image_dir, g_free);
fpi_image_device_close_complete (image_device, NULL);
break;
}
}
static void
gxfp_start_job (FpiDeviceGxfp5130 *self, GxfpJob job)
{
g_autoptr(GTask) task = NULL;
g_assert (!self->worker_running);
self->worker_running = TRUE;
task = g_task_new (self, NULL, gxfp_job_done, NULL);
g_task_set_task_data (task, GINT_TO_POINTER (job), NULL);
g_task_run_in_thread (task, gxfp_job_thread);
}
static void
gxfp_dev_open (FpImageDevice *device)
{
FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (device);
const gchar *path = fpi_device_get_virtual_env (FP_DEVICE (device));
self->device_path = g_strdup (path && path[0] ? path
: GXFP_DEVICE_PATH_DEFAULT);
self->debug_image_dir = g_strdup (g_getenv ("GXFP_DEBUG_IMAGE_DIR"));
self->capture_sequence = 0;
gxfp_start_job (self, GXFP_JOB_OPEN);
}
static void
gxfp_dev_close (FpImageDevice *device)
{
FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (device);
g_assert (!self->worker_running);
gxfp_start_job (self, GXFP_JOB_CLOSE);
}
static void
gxfp_dev_activate (FpImageDevice *device)
{
FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (device);
self->deactivating = FALSE;
gxfp_start_job (self, GXFP_JOB_ACTIVATE);
}
static void
gxfp_dev_deactivate (FpImageDevice *device)
{
FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (device);
self->deactivating = TRUE;
if (self->worker_running)
{
gxfp_session_cancel (self->session);
return;
}
gxfp_start_job (self, GXFP_JOB_DEACTIVATE);
}
static void
gxfp_dev_change_state (FpImageDevice *device, FpiImageDeviceState state)
{
FpiDeviceGxfp5130 *self = FPI_DEVICE_GXFP5130 (device);
if (self->deactivating || self->worker_running)
return;
switch (state)
{
case FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_ON:
gxfp_start_job (self, GXFP_JOB_WAIT_FINGER_DOWN);
break;
case FPI_IMAGE_DEVICE_STATE_CAPTURE:
gxfp_start_job (self, GXFP_JOB_CAPTURE);
break;
case FPI_IMAGE_DEVICE_STATE_AWAIT_FINGER_OFF:
gxfp_start_job (self, GXFP_JOB_WAIT_FINGER_UP);
break;
case FPI_IMAGE_DEVICE_STATE_INACTIVE:
case FPI_IMAGE_DEVICE_STATE_ACTIVATING:
case FPI_IMAGE_DEVICE_STATE_DEACTIVATING:
case FPI_IMAGE_DEVICE_STATE_IDLE:
default:
break;
}
}
static const FpIdEntry id_table[] = {
{ .virtual_envvar = "FP_GXFP5130" },
{ .virtual_envvar = NULL },
};
static void
fpi_device_gxfp5130_init (FpiDeviceGxfp5130 *self)
{
self->session = NULL;
}
static void
fpi_device_gxfp5130_class_init (FpiDeviceGxfp5130Class *klass)
{
FpDeviceClass *device_class = FP_DEVICE_CLASS (klass);
FpImageDeviceClass *image_class = FP_IMAGE_DEVICE_CLASS (klass);
device_class->id = FP_COMPONENT;
device_class->full_name = "Goodix GXFP5130 (ChicagoHU 0x2504)";
device_class->type = FP_DEVICE_TYPE_VIRTUAL;
device_class->id_table = id_table;
device_class->scan_type = FP_SCAN_TYPE_PRESS;
image_class->img_open = gxfp_dev_open;
image_class->img_close = gxfp_dev_close;
image_class->activate = gxfp_dev_activate;
image_class->deactivate = gxfp_dev_deactivate;
image_class->change_state = gxfp_dev_change_state;
image_class->img_width = GXFP_IMAGE_WIDTH;
image_class->img_height = GXFP_IMAGE_HEIGHT;
}