File elanmoc2.patch of Package libfprint
diff --git a/data/autosuspend.hwdb b/data/autosuspend.hwdb
index 9115fb0..04cd2b4 100644
--- a/data/autosuspend.hwdb
+++ b/data/autosuspend.hwdb
@@ -166,6 +166,13 @@ usb:v04F3p0CA3*
ID_AUTOSUSPEND=1
ID_PERSIST=0
+# Supported by libfprint driver elanmoc2
+usb:v04F3p0C00*
+usb:v04F3p0C4C*
+usb:v04F3p0C5E*
+ ID_AUTOSUSPEND=1
+ ID_PERSIST=0
+
# Supported by libfprint driver etes603
usb:v1C7Ap0603*
ID_AUTOSUSPEND=1
@@ -339,10 +346,7 @@ usb:v047Dp8054*
usb:v047Dp8055*
usb:v04E8p730B*
usb:v04F3p036B*
-usb:v04F3p0C00*
-usb:v04F3p0C4C*
usb:v04F3p0C57*
-usb:v04F3p0C5E*
usb:v04F3p0C5A*
usb:v04F3p0C60*
usb:v04F3p0C6C*
diff --git a/libfprint/drivers/elanmoc2/elanmoc2.c b/libfprint/drivers/elanmoc2/elanmoc2.c
new file mode 100644
index 0000000..62a2c1a
--- /dev/null
+++ b/libfprint/drivers/elanmoc2/elanmoc2.c
@@ -0,0 +1,1489 @@
+/*
+ * Driver for ELAN Match-On-Chip sensors
+ * Copyright (C) 2021-2023 Davide Depau <davide@depau.eu>
+ *
+ * Based on original reverse-engineering work by Davide Depau. The protocol has
+ * been reverse-engineered from captures of the official Windows driver, and by
+ * testing commands on the sensor with a multiplatform Python prototype driver:
+ * https://github.com/depau/Elan-Fingerprint-0c4c-PoC/
+ *
+ * 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 "elanmoc2"
+
+// Library includes
+#include <glib.h>
+#include <sys/param.h>
+
+// Local includes
+#include "drivers_api.h"
+
+#include "elanmoc2.h"
+
+struct _FpiDeviceElanMoC2
+{
+ FpDevice parent;
+
+ /* Device properties */
+ unsigned int dev_type;
+
+ /* USB response data */
+ GBytes *buffer_in;
+ const Elanmoc2Cmd *in_flight_cmd;
+
+ /* Command status data */
+ FpiSsm *ssm;
+ unsigned int enrolled_num;
+ unsigned int enrolled_num_retries;
+ unsigned int print_index;
+ GPtrArray *list_result;
+
+ // Enroll
+ int enroll_stage;
+ FpPrint *enroll_print;
+};
+
+G_DEFINE_TYPE (FpiDeviceElanMoC2, fpi_device_elanmoc2, FP_TYPE_DEVICE);
+
+
+static void
+elanmoc2_cmd_usb_callback (FpiUsbTransfer *transfer,
+ FpDevice *device,
+ gpointer user_data,
+ GError *error)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+ gboolean short_is_error = (gboolean) (uintptr_t) user_data;
+
+ if (self->ssm == NULL)
+ {
+ if (self->in_flight_cmd == NULL || !self->in_flight_cmd->ssm_not_required)
+ fp_warn ("Received USB callback with no ongoing action");
+
+ self->in_flight_cmd = NULL;
+
+ if (error)
+ {
+ fp_info ("USB callback error: %s", error->message);
+ g_error_free (error);
+ }
+ return;
+ }
+
+ if (error)
+ {
+ fpi_ssm_mark_failed (g_steal_pointer (&self->ssm),
+ g_steal_pointer (&error));
+ return;
+ }
+
+ if (self->in_flight_cmd != NULL)
+ {
+ /* Send callback */
+ const Elanmoc2Cmd *cmd = g_steal_pointer (&self->in_flight_cmd);
+
+ if (cmd->in_len == 0)
+ {
+ /* Nothing to receive */
+ fpi_ssm_next_state (self->ssm);
+ return;
+ }
+
+ FpiUsbTransfer *transfer_in = fpi_usb_transfer_new (device);
+
+ transfer_in->short_is_error = short_is_error;
+
+ fpi_usb_transfer_fill_bulk (transfer_in, cmd->ep_in,
+ cmd->in_len);
+
+ g_autoptr(GCancellable) cancellable =
+ cmd->cancellable ? fpi_device_get_cancellable (device) : NULL;
+
+ fpi_usb_transfer_submit (transfer_in,
+ ELANMOC2_USB_RECV_TIMEOUT,
+ g_steal_pointer (&cancellable),
+ elanmoc2_cmd_usb_callback,
+ NULL);
+ }
+ else
+ {
+ /* Receive callback */
+ if (transfer->actual_length > 0 && transfer->buffer[0] != 0x40)
+ {
+ fpi_ssm_mark_failed (g_steal_pointer (&self->ssm),
+ fpi_device_error_new_msg (FP_DEVICE_ERROR_PROTO,
+ "Error receiving data "
+ "from sensor"));
+ }
+ else
+ {
+ g_assert_null (self->buffer_in);
+ self->buffer_in =
+ g_bytes_new_take (g_steal_pointer (&(transfer->buffer)),
+ transfer->actual_length);
+ fpi_ssm_next_state (self->ssm);
+ }
+ }
+}
+
+static void
+elanmoc2_cmd_transceive_full (FpDevice *device,
+ const Elanmoc2Cmd *cmd,
+ GByteArray *buffer_out,
+ gboolean short_is_error
+ )
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ g_assert (buffer_out->len == cmd->out_len);
+ g_assert_null (self->in_flight_cmd);
+ self->in_flight_cmd = cmd;
+
+ g_autoptr(FpiUsbTransfer) transfer_out = fpi_usb_transfer_new (device);
+ transfer_out->short_is_error = TRUE;
+ fpi_usb_transfer_fill_bulk_full (transfer_out,
+ ELANMOC2_EP_CMD_OUT,
+ g_byte_array_steal (buffer_out, NULL),
+ cmd->out_len,
+ g_free);
+
+ g_autoptr(GCancellable) cancellable =
+ cmd->cancellable ? fpi_device_get_cancellable (device) : NULL;
+
+ fpi_usb_transfer_submit (g_steal_pointer (&transfer_out),
+ ELANMOC2_USB_SEND_TIMEOUT,
+ g_steal_pointer (&cancellable),
+ elanmoc2_cmd_usb_callback,
+ (gpointer) (uintptr_t) short_is_error);
+}
+
+static void
+elanmoc2_cmd_transceive (FpDevice *device,
+ const Elanmoc2Cmd *cmd,
+ GByteArray *buffer_out)
+{
+ elanmoc2_cmd_transceive_full (device, cmd, buffer_out, TRUE);
+}
+
+static GByteArray *
+elanmoc2_prepare_cmd (FpiDeviceElanMoC2 *self, const Elanmoc2Cmd *cmd)
+{
+ if (cmd->devices != ELANMOC2_ALL_DEV && !(cmd->devices & self->dev_type))
+ return NULL;
+
+ g_assert (cmd->out_len > 0);
+
+ GByteArray *buffer = g_byte_array_new ();
+ g_byte_array_set_size (buffer, cmd->out_len);
+ memset (buffer->data, 0, buffer->len);
+
+ buffer->data[0] = 0x40;
+ memcpy (&buffer->data[1], cmd->cmd, cmd->is_single_byte_command ? 1 : 2);
+
+ return buffer;
+}
+
+static void
+elanmoc2_print_set_data (FpPrint *print,
+ guchar finger_id,
+ guchar user_id_len,
+ const guchar *user_id)
+{
+ fpi_print_set_type (print, FPI_PRINT_RAW);
+ fpi_print_set_device_stored (print, TRUE);
+
+ GVariant *user_id_v = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE,
+ user_id, user_id_len,
+ sizeof (guchar));
+ GVariant *fpi_data = g_variant_new ("(y@ay)", finger_id, user_id_v);
+ g_object_set (print, "fpi-data", fpi_data, NULL);
+}
+
+static GBytes *
+elanmoc2_print_get_data (FpPrint *print,
+ guchar *finger_id)
+{
+ g_autoptr(GVariant) fpi_data = NULL;
+ g_autoptr(GVariant) user_id_v = NULL;
+
+ g_object_get (print, "fpi-data", &fpi_data, NULL);
+ g_assert_nonnull (fpi_data);
+
+ g_variant_get (fpi_data, "(y@ay)", finger_id, &user_id_v);
+ g_assert_nonnull (user_id_v);
+
+ gsize user_id_len_s = 0;
+ gconstpointer user_id_tmp = g_variant_get_fixed_array (user_id_v,
+ &user_id_len_s,
+ sizeof (guchar));
+ g_assert (user_id_len_s <= 255);
+
+ g_autoptr(GByteArray) user_id = g_byte_array_new ();
+ g_byte_array_append (user_id, user_id_tmp, user_id_len_s);
+
+ return g_byte_array_free_to_bytes (g_steal_pointer (&user_id));
+}
+
+static FpPrint *
+elanmoc2_print_new_with_user_id (FpiDeviceElanMoC2 *self,
+ guchar finger_id,
+ guchar user_id_len,
+ const guchar *user_id)
+{
+ FpPrint *print = fp_print_new (FP_DEVICE (self));
+
+ elanmoc2_print_set_data (print, finger_id, user_id_len, user_id);
+ return g_steal_pointer (&print);
+}
+
+static guint
+elanmoc2_get_user_id_max_length (FpiDeviceElanMoC2 *self)
+{
+ return self->dev_type == ELANMOC2_DEV_0C5E ?
+ ELANMOC2_USER_ID_MAX_LEN_0C5E :
+ ELANMOC2_USER_ID_MAX_LEN;
+}
+
+static GBytes *
+elanmoc2_get_user_id_string (FpiDeviceElanMoC2 *self,
+ GBytes *finger_info_response)
+{
+ GByteArray *user_id = g_byte_array_new ();
+
+ guint offset = self->dev_type == ELANMOC2_DEV_0C5E ? 3 : 2;
+ guint max_len = MIN (elanmoc2_get_user_id_max_length (self),
+ g_bytes_get_size (finger_info_response) - offset);
+
+ g_byte_array_set_size (user_id, max_len);
+
+ /* The string must be copied since the input data is not guaranteed to be
+ * null-terminated */
+ const guint8 *data = g_bytes_get_data (finger_info_response, NULL);
+ memcpy (user_id->data, &data[offset], max_len);
+ user_id->data[max_len] = '\0';
+
+ return g_byte_array_free_to_bytes (user_id);
+}
+
+static FpPrint *
+elanmoc2_print_new_from_finger_info (FpiDeviceElanMoC2 *self,
+ guint8 finger_id,
+ GBytes *finger_info_resp)
+{
+ g_autoptr(GBytes) user_id = elanmoc2_get_user_id_string (self,
+ finger_info_resp);
+ guint8 user_id_len = g_bytes_get_size (user_id);
+ const char *user_id_data = g_bytes_get_data (user_id, NULL);
+
+ if (g_str_has_prefix ( user_id_data, "FP"))
+ {
+ user_id_len = strnlen (user_id_data, user_id_len);
+ fp_info ("Creating new print: finger %d, user id[%d]: %s",
+ finger_id,
+ user_id_len,
+ (char *) user_id_data);
+ }
+ else
+ {
+ fp_info ("Creating new print: finger %d, user id[%d]: raw data",
+ finger_id,
+ user_id_len);
+ }
+
+ FpPrint *print =
+ elanmoc2_print_new_with_user_id (self,
+ finger_id,
+ user_id_len,
+ (const guint8 *) user_id_data);
+
+ if (!fpi_print_fill_from_user_id (print, (const char *) user_id_data))
+ /* Fingerprint matched with on-sensor print, but the on-sensor print was
+ * not added by libfprint. Wipe it and report a failure. */
+ fp_info ("Finger info not generated by libfprint");
+ else
+ fp_info ("Finger info with libfprint user ID");
+
+ return g_steal_pointer (&print);
+}
+
+static gboolean
+elanmoc2_finger_info_is_present (FpiDeviceElanMoC2 *self,
+ GBytes *finger_info_response)
+{
+ int offset = self->dev_type == ELANMOC2_DEV_0C5E ? 3 : 2;
+
+ g_assert (g_bytes_get_size (finger_info_response) >= offset + 2);
+
+
+ /* If the user ID starts with "FP", report true. This is a heuristic: after
+ * wiping the sensor, the user IDs are not reset. */
+ const gchar *data = g_bytes_get_data (finger_info_response, NULL);
+ const gchar *user_id = &data[offset];
+
+ /* I'm intentionally not using `g_str_has_prefix` here because it uses
+ * `strlen` and this is binary data. */
+ return memcmp (user_id, "FP", 2) == 0;
+}
+
+
+static void
+elanmoc2_cancel (FpDevice *device)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ fp_info ("Cancelling any ongoing requests");
+
+ g_autoptr(GByteArray) buffer_out = elanmoc2_prepare_cmd (self, &cmd_abort);
+ elanmoc2_cmd_transceive (device, &cmd_abort, buffer_out);
+}
+
+static void
+elanmoc2_open (FpDevice *device)
+{
+ g_autoptr(GError) error = NULL;
+ FpiDeviceElanMoC2 *self;
+
+ if (!g_usb_device_reset (fpi_device_get_usb_device (device), &error))
+ return fpi_device_open_complete (device, g_steal_pointer (&error));
+
+ if (!g_usb_device_claim_interface (
+ fpi_device_get_usb_device (FP_DEVICE (device)), 0, 0, &error))
+ return fpi_device_open_complete (device, g_steal_pointer (&error));
+
+ self = FPI_DEVICE_ELANMOC2 (device);
+ self->dev_type = fpi_device_get_driver_data (FP_DEVICE (device));
+ fpi_device_open_complete (device, NULL);
+}
+
+static void
+elanmoc2_close (FpDevice *device)
+{
+ g_autoptr(GError) error = NULL;
+
+ fp_info ("Closing device");
+ elanmoc2_cancel (device);
+ g_usb_device_release_interface (fpi_device_get_usb_device (FP_DEVICE (device)),
+ 0, 0, &error);
+ fpi_device_close_complete (device, g_steal_pointer (&error));
+}
+
+static void
+elanmoc2_ssm_completed_callback (FpiSsm *ssm, FpDevice *device, GError *error)
+{
+ if (error)
+ fpi_device_action_error (device, error);
+}
+
+static void
+elanmoc2_perform_get_num_enrolled (FpiDeviceElanMoC2 *self, FpiSsm *ssm)
+{
+ self->enrolled_num_retries++;
+ g_autoptr(GByteArray) buffer_out =
+ elanmoc2_prepare_cmd (self,
+ &cmd_get_enrolled_count);
+
+ if (buffer_out == NULL)
+ {
+ fpi_ssm_next_state (ssm);
+ return;
+ }
+
+ fp_info ("Querying number of enrolled fingers");
+
+ elanmoc2_cmd_transceive_full (FP_DEVICE (self),
+ &cmd_get_enrolled_count,
+ buffer_out,
+ false);
+ fp_info ("Sent query for number of enrolled fingers");
+}
+
+static GError *
+elanmoc2_get_num_enrolled_retry_or_error (FpiDeviceElanMoC2 *self,
+ FpiSsm *ssm,
+ int retry_state)
+{
+ fp_info ("Device returned no data, retrying");
+ if (self->enrolled_num_retries >= ELANMOC2_MAX_RETRIES)
+ return fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL,
+ "Device refused to respond to query for "
+ "number of enrolled fingers");
+ fpi_ssm_jump_to_state (ssm, retry_state);
+ return NULL;
+}
+
+/**
+ * elanmoc2_get_finger_error:
+ * @self: #FpiDeviceElanMoC2 pointer
+ * @out_can_retry: Whether the current action should be retried (out)
+ *
+ * Checks a command status code and, if an error has occurred, creates a new
+ * error object. Returns whether the operation needs to be retried.
+ *
+ * Returns: #GError if failed, or %NULL
+ */
+static GError *
+elanmoc2_get_finger_error (GBytes *buffer_in, gboolean *out_can_retry)
+{
+ g_assert_nonnull (buffer_in);
+ g_assert (g_bytes_get_size (buffer_in) >= 2);
+
+ const guint8 *data_in = g_bytes_get_data (buffer_in, NULL);
+
+ /* Regular status codes never have the most-significant nibble set;
+ * errors do */
+ if ((data_in[1] & 0xF0) == 0)
+ {
+ *out_can_retry = TRUE;
+ return NULL;
+ }
+ switch ((unsigned char) data_in[1])
+ {
+ case ELANMOC2_RESP_MOVE_DOWN:
+ *out_can_retry = TRUE;
+ return fpi_device_retry_new_msg (FP_DEVICE_RETRY_CENTER_FINGER,
+ "Move your finger slightly downwards");
+
+ case ELANMOC2_RESP_MOVE_RIGHT:
+ *out_can_retry = TRUE;
+ return fpi_device_retry_new_msg (FP_DEVICE_RETRY_CENTER_FINGER,
+ "Move your finger slightly to the right");
+
+ case ELANMOC2_RESP_MOVE_UP:
+ *out_can_retry = TRUE;
+ return fpi_device_retry_new_msg (FP_DEVICE_RETRY_CENTER_FINGER,
+ "Move your finger slightly upwards");
+
+ case ELANMOC2_RESP_MOVE_LEFT:
+ *out_can_retry = TRUE;
+ return fpi_device_retry_new_msg (FP_DEVICE_RETRY_CENTER_FINGER,
+ "Move your finger slightly to the left");
+
+ case ELANMOC2_RESP_SENSOR_DIRTY:
+ *out_can_retry = TRUE;
+ return fpi_device_retry_new_msg (FP_DEVICE_RETRY_REMOVE_FINGER,
+ "Sensor is dirty or wet");
+
+ case ELANMOC2_RESP_NOT_ENOUGH_SURFACE:
+ *out_can_retry = TRUE;
+ return fpi_device_retry_new_msg (FP_DEVICE_RETRY_REMOVE_FINGER,
+ "Press your finger slightly harder on "
+ "the sensor");
+
+ case ELANMOC2_RESP_NOT_ENROLLED:
+ *out_can_retry = FALSE;
+ return fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_NOT_FOUND,
+ "Finger not recognized");
+
+ case ELANMOC2_RESP_MAX_ENROLLED_REACHED:
+ *out_can_retry = FALSE;
+ return fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_FULL,
+ "Maximum number of fingers already "
+ "enrolled");
+
+ default:
+ *out_can_retry = FALSE;
+ return fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL,
+ "Unknown error");
+ }
+}
+
+static void
+elanmoc2_identify_verify_complete (FpDevice *device, GError *error)
+{
+ if (fpi_device_get_current_action (device) == FPI_DEVICE_ACTION_IDENTIFY)
+ fpi_device_identify_complete (device, error);
+ else
+ fpi_device_verify_complete (device, error);
+}
+
+/**
+ * elanmoc2_identify_verify_report:
+ * @device: #FpDevice
+ * @print: Identified fingerprint
+ * @error: Optional error
+ *
+ * Calls the correct verify or identify report function based on the input data.
+ * Returns whether the action should be completed.
+ *
+ * Returns: Whether to complete the action.
+ */
+static gboolean
+elanmoc2_identify_verify_report (FpDevice *device, FpPrint *print,
+ GError **error)
+{
+ if (*error != NULL && (*error)->domain != FP_DEVICE_RETRY)
+ return TRUE;
+
+ if (fpi_device_get_current_action (device) == FPI_DEVICE_ACTION_IDENTIFY)
+ {
+ if (print != NULL)
+ {
+ GPtrArray * gallery = NULL;
+ fpi_device_get_identify_data (device, &gallery);
+
+ for (int i = 0; i < gallery->len; i++)
+ {
+ FpPrint *to_match = g_ptr_array_index (gallery, i);
+ if (fp_print_equal (to_match, print))
+ {
+ fp_info ("Identify: finger matches");
+ fpi_device_identify_report (device,
+ g_steal_pointer (&to_match),
+ print,
+ NULL);
+ return TRUE;
+ }
+ }
+ fp_info ("Identify: no match");
+ }
+ fpi_device_identify_report (device, NULL, NULL, *error);
+ return TRUE;
+ }
+ else
+ {
+ FpiMatchResult result = FPI_MATCH_FAIL;
+ if (print != NULL)
+ {
+ FpPrint *to_match = NULL;
+ fpi_device_get_verify_data (device, &to_match);
+ g_assert_nonnull (to_match);
+
+ if (fp_print_equal (to_match, print))
+ {
+ fp_info ("Verify: finger matches");
+ result = FPI_MATCH_SUCCESS;
+ }
+ else
+ {
+ fp_info ("Verify: finger does not match");
+ print = NULL;
+ }
+ }
+ fpi_device_verify_report (device, result, print, *error);
+ return result != FPI_MATCH_FAIL;
+ }
+}
+
+static void
+elanmoc2_identify_run_state (FpiSsm *ssm, FpDevice *device)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GBytes) buffer_in = g_steal_pointer (&self->buffer_in);
+
+ const guint8 *data_in =
+ buffer_in != NULL ? g_bytes_get_data (buffer_in, NULL) : NULL;
+ const gsize data_in_len =
+ buffer_in != NULL ? g_bytes_get_size (buffer_in) : 0;
+
+ switch (fpi_ssm_get_cur_state (ssm))
+ {
+ case IDENTIFY_GET_NUM_ENROLLED: {
+ elanmoc2_perform_get_num_enrolled (self, ssm);
+ break;
+ }
+
+ case IDENTIFY_CHECK_NUM_ENROLLED: {
+ if (data_in_len == 0)
+ {
+ error =
+ elanmoc2_get_num_enrolled_retry_or_error (self,
+ ssm,
+ IDENTIFY_GET_NUM_ENROLLED);
+ if (error != NULL)
+ {
+ elanmoc2_identify_verify_complete (device,
+ g_steal_pointer (&error));
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ }
+ break;
+ }
+
+ g_assert_nonnull (data_in);
+ g_assert (data_in_len >= 2);
+
+ self->enrolled_num = data_in[1];
+
+ if (self->enrolled_num == 0)
+ {
+ fp_info ("No fingers enrolled, no need to identify finger");
+ error = NULL;
+ elanmoc2_identify_verify_report (device, NULL, &error);
+ elanmoc2_identify_verify_complete (device, NULL);
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ break;
+ }
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+
+ case IDENTIFY_IDENTIFY: {
+ g_autoptr(GByteArray) buffer_out = elanmoc2_prepare_cmd (self,
+ &cmd_identify);
+ if (buffer_out == NULL)
+ {
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+ elanmoc2_cmd_transceive (device, &cmd_identify, buffer_out);
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_NEEDED);
+ fp_info ("Sent identification request");
+ break;
+ }
+
+ case IDENTIFY_GET_FINGER_INFO: {
+ g_assert_nonnull (buffer_in);
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_PRESENT);
+ gboolean can_retry = FALSE;
+ error = elanmoc2_get_finger_error (buffer_in, &can_retry);
+ if (error != NULL)
+ {
+ fp_info ("Identify failed: %s", error->message);
+ if (can_retry)
+ {
+ elanmoc2_identify_verify_report (device, NULL, &error);
+ fpi_ssm_jump_to_state (ssm, IDENTIFY_IDENTIFY);
+ }
+ else
+ {
+ elanmoc2_identify_verify_complete (device,
+ g_steal_pointer (&error));
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ }
+ break;
+ }
+
+ g_assert_nonnull (data_in);
+ g_assert (data_in_len >= 2);
+
+ self->print_index = data_in[1];
+
+ fp_info ("Identified finger %d; requesting finger info",
+ self->print_index);
+
+ g_autoptr(GByteArray) buffer_out =
+ elanmoc2_prepare_cmd (self, &cmd_finger_info);
+
+ if (buffer_out == NULL)
+ {
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+ g_assert (buffer_out->len >= 4);
+ buffer_out->data[3] = self->print_index;
+ elanmoc2_cmd_transceive (device, &cmd_finger_info, buffer_out);
+ break;
+ }
+
+ case IDENTIFY_CHECK_FINGER_INFO: {
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_NONE);
+
+ g_assert_nonnull (buffer_in);
+ g_autoptr(FpPrint) print =
+ elanmoc2_print_new_from_finger_info (self,
+ self->print_index,
+ buffer_in);
+
+ error = NULL;
+ elanmoc2_identify_verify_report (device,
+ g_steal_pointer (&print),
+ &error);
+ elanmoc2_identify_verify_complete (device, g_steal_pointer (&error));
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ break;
+ }
+
+ default:
+ break;
+ }
+}
+
+static void
+elanmoc2_identify_verify (FpDevice *device)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ fp_info ("[elanmoc2] New identify/verify operation");
+ self->ssm = fpi_ssm_new (device, elanmoc2_identify_run_state,
+ IDENTIFY_NUM_STATES);
+ self->enrolled_num_retries = 0;
+ fpi_ssm_start (self->ssm, elanmoc2_ssm_completed_callback);
+}
+
+static void
+elanmoc2_list_ssm_completed_callback (FpiSsm *ssm, FpDevice *device,
+ GError *error)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ g_clear_pointer (&self->list_result, g_ptr_array_unref);
+ elanmoc2_ssm_completed_callback (ssm, device, error);
+}
+
+static void
+elanmoc2_list_run_state (FpiSsm *ssm, FpDevice *device)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ g_autoptr(GBytes) buffer_in = g_steal_pointer (&self->buffer_in);
+
+ const guint8 *data_in =
+ buffer_in != NULL ? g_bytes_get_data (buffer_in, NULL) : NULL;
+ const gsize data_in_len =
+ buffer_in != NULL ? g_bytes_get_size (buffer_in) : 0;
+
+ switch (fpi_ssm_get_cur_state (ssm))
+ {
+ case LIST_GET_NUM_ENROLLED:
+ elanmoc2_perform_get_num_enrolled (self, ssm);
+ break;
+
+ case LIST_CHECK_NUM_ENROLLED: {
+ if (data_in_len == 0)
+ {
+ g_autoptr(GError) error =
+ elanmoc2_get_num_enrolled_retry_or_error (self,
+ ssm,
+ LIST_GET_NUM_ENROLLED);
+ if (error != NULL)
+ {
+ fpi_device_list_complete (device,
+ NULL,
+ g_steal_pointer (&error));
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ }
+ break;
+ }
+
+ g_assert_nonnull (data_in);
+ g_assert (data_in_len >= 2);
+
+ self->enrolled_num = data_in[1];
+
+ fp_info ("List: fingers enrolled: %d", self->enrolled_num);
+ if (self->enrolled_num == 0)
+ {
+ fpi_device_list_complete (device,
+ g_steal_pointer (&self->list_result),
+ NULL);
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ break;
+ }
+ self->print_index = 0;
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+
+ case LIST_GET_FINGER_INFO: {
+ g_autoptr(GByteArray) buffer_out =
+ elanmoc2_prepare_cmd (self, &cmd_finger_info);
+
+ if (buffer_out == NULL)
+ {
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+ g_assert (buffer_out->len >= 4);
+ buffer_out->data[3] = self->print_index;
+ elanmoc2_cmd_transceive_full (device,
+ &cmd_finger_info,
+ buffer_out,
+ FALSE);
+ fp_info ("Sent get finger info command for finger %d",
+ self->print_index);
+ break;
+ }
+
+ case LIST_CHECK_FINGER_INFO:
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_NONE);
+
+ if (data_in_len < cmd_finger_info.in_len)
+ {
+ GError *error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL,
+ "Reader refuses operation "
+ "before valid finger match");
+ fpi_device_list_complete (device, NULL, error);
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ break;
+ }
+
+ fp_info ("Successfully retrieved finger info for %d",
+ self->print_index);
+ g_assert_nonnull (buffer_in);
+ if (elanmoc2_finger_info_is_present (self, buffer_in))
+ {
+ FpPrint *print = elanmoc2_print_new_from_finger_info (self,
+ self->print_index,
+ buffer_in);
+ g_ptr_array_add (self->list_result, g_object_ref_sink (print));
+ }
+
+ self->print_index++;
+
+ if (self->print_index < MIN (ELANMOC2_MAX_PRINTS, self->enrolled_num))
+ {
+ fpi_ssm_jump_to_state (ssm, LIST_GET_FINGER_INFO);
+ }
+ else
+ {
+ fpi_device_list_complete (device,
+ g_steal_pointer (&self->list_result),
+ NULL);
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ }
+ break;
+ }
+}
+
+static void
+elanmoc2_list (FpDevice *device)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ fp_info ("[elanmoc2] New list operation");
+ self->ssm = fpi_ssm_new (device, elanmoc2_list_run_state, LIST_NUM_STATES);
+ self->list_result = g_ptr_array_new_with_free_func (g_object_unref);
+ self->enrolled_num_retries = 0;
+ fpi_ssm_start (self->ssm, elanmoc2_list_ssm_completed_callback);
+}
+
+static void
+elanmoc2_enroll_ssm_completed_callback (FpiSsm *ssm, FpDevice *device,
+ GError *error)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ /* Pointer is either stolen by fpi_device_enroll_complete() or otherwise
+ * unref'd by libfprint elsewhere not in this driver. */
+ self->enroll_print = NULL;
+ elanmoc2_ssm_completed_callback (ssm, device, error);
+}
+
+static void
+elanmoc2_enroll_run_state (FpiSsm *ssm, FpDevice *device)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GBytes) buffer_in = g_steal_pointer (&self->buffer_in);
+
+ const guint8 *data_in =
+ buffer_in != NULL ? g_bytes_get_data (buffer_in, NULL) : NULL;
+ const gsize data_in_len =
+ buffer_in != NULL ? g_bytes_get_size (buffer_in) : 0;
+
+ g_assert_nonnull (self->enroll_print);
+
+ switch (fpi_ssm_get_cur_state (ssm))
+ {
+ /* First check how many fingers are already enrolled */
+ case ENROLL_GET_NUM_ENROLLED: {
+ elanmoc2_perform_get_num_enrolled (self, ssm);
+ break;
+ }
+
+ case ENROLL_CHECK_NUM_ENROLLED: {
+ if (data_in_len == 0)
+ {
+ error =
+ elanmoc2_get_num_enrolled_retry_or_error (self,
+ ssm,
+ ENROLL_GET_NUM_ENROLLED);
+ if (error != NULL)
+ {
+ fpi_device_enroll_complete (device,
+ NULL,
+ g_steal_pointer (&error));
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ }
+ break;
+ }
+
+ g_assert_nonnull (data_in);
+ g_assert (data_in_len >= 2);
+ self->enrolled_num = data_in[1];
+
+ if (self->enrolled_num >= ELANMOC2_MAX_PRINTS)
+ {
+ fp_info ("Can't enroll, sensor storage is full");
+ error = fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_FULL,
+ "Sensor storage is full");
+ fpi_device_enroll_complete (device,
+ NULL,
+ g_steal_pointer (&error));
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ }
+ else if (self->enrolled_num == 0)
+ {
+ fp_info ("Enrolled count is 0, proceeding with enroll stage");
+ fpi_ssm_jump_to_state (ssm, ENROLL_ENROLL);
+ }
+ else
+ {
+ fp_info ("Fingers enrolled: %d, need to check for re-enroll",
+ self->enrolled_num);
+ fpi_ssm_next_state (ssm);
+ }
+ break;
+ }
+
+ case ENROLL_EARLY_REENROLL_CHECK: {
+ g_autoptr(GByteArray) buffer_out = elanmoc2_prepare_cmd (self,
+ &cmd_identify);
+ if (buffer_out == NULL)
+ {
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+ elanmoc2_cmd_transceive (device, &cmd_identify, buffer_out);
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_NEEDED);
+ fp_info ("Sent identification request");
+ break;
+ }
+
+ case ENROLL_GET_ENROLLED_FINGER_INFO: {
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_PRESENT);
+
+ g_assert_nonnull (data_in);
+ g_assert (g_bytes_get_size (buffer_in) >= 2);
+
+ /* Not enrolled - skip to enroll stage */
+ if (data_in[1] == ELANMOC2_RESP_NOT_ENROLLED)
+ {
+ fp_info ("Finger not enrolled, proceeding with enroll stage");
+ fpi_device_enroll_progress (device, self->enroll_stage, NULL,
+ NULL);
+ fpi_ssm_jump_to_state (ssm, ENROLL_ENROLL);
+ break;
+ }
+
+ /* Identification failed (i.e. dirty) - retry */
+ gboolean can_retry = FALSE;
+ error = elanmoc2_get_finger_error (buffer_in, &can_retry);
+ if (error != NULL)
+ {
+ fp_info ("Identify failed: %s", error->message);
+ if (can_retry)
+ {
+ fpi_device_enroll_progress (device, self->enroll_stage, NULL,
+ g_steal_pointer (&error));
+ fpi_ssm_jump_to_state (ssm, ENROLL_EARLY_REENROLL_CHECK);
+ }
+ else
+ {
+ fpi_device_enroll_complete (device, NULL,
+ g_steal_pointer (&error));
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ self->enroll_print = NULL;
+ }
+ break;
+ }
+
+ /* Finger already enrolled - fetch finger info for deletion */
+ self->print_index = data_in[1];
+ fp_info ("Finger enrolled as %d; fetching finger info",
+ self->print_index);
+ g_autoptr(GByteArray) buffer_out =
+ elanmoc2_prepare_cmd (self, &cmd_finger_info);
+ if (buffer_out == NULL)
+ {
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+ g_assert (buffer_out->len >= 4);
+ buffer_out->data[3] = self->print_index;
+ elanmoc2_cmd_transceive (device, &cmd_finger_info, buffer_out);
+ break;
+ }
+
+ case ENROLL_ATTEMPT_DELETE: {
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_NONE);
+ fp_info ("Deleting enrolled finger %d", self->print_index);
+ g_assert_nonnull (buffer_in);
+
+ /* Attempt to delete the finger */
+ g_autoptr(GBytes) user_id =
+ elanmoc2_get_user_id_string (self, buffer_in);
+ g_autoptr(GByteArray) buffer_out =
+ elanmoc2_prepare_cmd (self, &cmd_delete);
+ if (buffer_out == NULL)
+ {
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+ gsize user_id_bytes = MIN (cmd_delete.out_len - 4,
+ ELANMOC2_USER_ID_MAX_LEN);
+ g_assert (buffer_out->len >= 4 + user_id_bytes);
+ buffer_out->data[3] = 0xf0 | (self->print_index + 5);
+ memcpy (&buffer_out->data[4],
+ g_bytes_get_data (user_id, NULL),
+ user_id_bytes);
+ elanmoc2_cmd_transceive (device, &cmd_delete, buffer_out);
+
+ break;
+ }
+
+ case ENROLL_CHECK_DELETED: {
+ g_assert_nonnull (data_in);
+ g_assert (g_bytes_get_size (buffer_in) >= 2);
+
+ if (data_in[1] != 0)
+ {
+ fp_info ("Failed to delete finger %d, wiping sensor",
+ self->print_index);
+ fpi_ssm_jump_to_state (ssm, ENROLL_WIPE_SENSOR);
+ }
+ else
+ {
+ fp_info ("Finger %d deleted, proceeding with enroll stage",
+ self->print_index);
+ self->enrolled_num--;
+ fpi_device_enroll_progress (device, self->enroll_stage, NULL,
+ NULL);
+ fpi_ssm_jump_to_state (ssm, ENROLL_ENROLL);
+ }
+ break;
+ }
+
+ case ENROLL_WIPE_SENSOR: {
+ g_autoptr(GByteArray) buffer_out =
+ elanmoc2_prepare_cmd (self, &cmd_wipe_sensor);
+ if (buffer_out == NULL)
+ {
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+ elanmoc2_cmd_transceive (device, &cmd_wipe_sensor, buffer_out);
+ self->enrolled_num = 0;
+ self->print_index = 0;
+ fp_info (
+ "Wipe sensor command sent - next operation will take a while");
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+
+ case ENROLL_ENROLL: {
+ g_autoptr(GByteArray) buffer_out = elanmoc2_prepare_cmd (self,
+ &cmd_enroll);
+ if (buffer_out == NULL)
+ {
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+ g_assert (buffer_out->len >= 7);
+ buffer_out->data[3] = self->enrolled_num;
+ buffer_out->data[4] = ELANMOC2_ENROLL_TIMES;
+ buffer_out->data[5] = self->enroll_stage;
+ buffer_out->data[6] = 0;
+ elanmoc2_cmd_transceive (device, &cmd_enroll, buffer_out);
+ fp_info ("Enroll command sent: %d/%d", self->enroll_stage,
+ ELANMOC2_ENROLL_TIMES);
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_NEEDED);
+ break;
+ }
+
+ case ENROLL_CHECK_ENROLLED: {
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_PRESENT);
+
+ g_assert_nonnull (data_in);
+ g_assert (g_bytes_get_size (buffer_in) >= 2);
+
+ if (data_in[1] == 0)
+ {
+ /* Stage okay */
+ fp_info ("Enroll stage succeeded");
+ self->enroll_stage++;
+ fpi_device_enroll_progress (device, self->enroll_stage,
+ self->enroll_print, NULL);
+ if (self->enroll_stage >= ELANMOC2_ENROLL_TIMES)
+ {
+ fp_info ("Enroll completed");
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+ }
+ else
+ {
+ /* Detection error */
+ gboolean can_retry = FALSE;
+ error = elanmoc2_get_finger_error (buffer_in, &can_retry);
+ if (error != NULL)
+ {
+ fp_info ("Enroll stage failed: %s", error->message);
+ if (data_in[1] == ELANMOC2_RESP_NOT_ENROLLED)
+ {
+ /* Not enrolled is a fatal error for "identify" but not for
+ * "enroll" */
+ error->domain = FP_DEVICE_RETRY;
+ error->code = FP_DEVICE_RETRY_TOO_SHORT;
+ can_retry = FALSE;
+ }
+ if (can_retry)
+ {
+ fpi_device_enroll_progress (device, self->enroll_stage,
+ NULL, g_steal_pointer (&error));
+ }
+ else
+ {
+ fpi_device_enroll_complete (device, NULL,
+ g_steal_pointer (&error));
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ }
+ }
+ else
+ {
+ fp_info ("Enroll stage failed for unknown reasons");
+ }
+ }
+ fp_info ("Performing another enroll");
+ fpi_ssm_jump_to_state (ssm, ENROLL_ENROLL);
+ break;
+ }
+
+ case ENROLL_LATE_REENROLL_CHECK: {
+ fpi_device_report_finger_status (device, FP_FINGER_STATUS_NONE);
+ g_autoptr(GByteArray) buffer_out =
+ elanmoc2_prepare_cmd (self, &cmd_check_enroll_collision);
+ if (buffer_out == NULL)
+ {
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+ elanmoc2_cmd_transceive (device, &cmd_check_enroll_collision, buffer_out);
+ fp_info ("Check re-enroll command sent");
+ break;
+ }
+
+ case ENROLL_COMMIT: {
+ error = NULL;
+
+ g_assert_nonnull (data_in);
+ g_assert (g_bytes_get_size (buffer_in) >= 2);
+
+ if (data_in[1] != 0)
+ {
+ fp_info ("Finger is already enrolled at position %d, cannot commit",
+ data_in[2]);
+ error = fpi_device_error_new_msg (FP_DEVICE_ERROR_DATA_DUPLICATE,
+ "Finger is already enrolled");
+ fpi_device_enroll_complete (device, NULL,
+ g_steal_pointer (&error));
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ self->enroll_print = NULL;
+ break;
+ }
+
+ fp_info ("Finger is not enrolled, committing");
+ g_autoptr(GByteArray) buffer_out = elanmoc2_prepare_cmd (self,
+ &cmd_commit);
+ if (buffer_out == NULL)
+ {
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+ g_autofree gchar *user_id = fpi_print_generate_user_id (
+ self->enroll_print);
+ elanmoc2_print_set_data (self->enroll_print, self->enrolled_num,
+ strlen (user_id), (guint8 *) user_id);
+
+ g_assert (buffer_out->len == cmd_commit.out_len);
+ buffer_out->data[3] = 0xf0 | (self->enrolled_num + 5);
+ strncpy ((gchar *) &buffer_out->data[4], user_id, cmd_commit.out_len - 4);
+ elanmoc2_cmd_transceive (device, &cmd_commit, buffer_out);
+ fp_info ("Commit command sent");
+ break;
+ }
+
+ case ENROLL_CHECK_COMMITTED: {
+ error = NULL;
+
+ g_assert_nonnull (data_in);
+ g_assert (g_bytes_get_size (buffer_in) >= 2);
+
+ if (data_in[1] != 0)
+ {
+ fp_info ("Commit failed with error code %d", data_in[1]);
+ error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL,
+ "Failed to store fingerprint for "
+ "unknown reasons");
+ fpi_device_enroll_complete (device, NULL, error);
+ fpi_ssm_mark_failed (g_steal_pointer (&self->ssm),
+ g_steal_pointer (&error));
+ }
+ else
+ {
+ fp_info ("Commit succeeded");
+ fpi_device_enroll_complete (device,
+ g_object_ref (self->enroll_print),
+ NULL);
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ }
+ break;
+ }
+ }
+}
+
+static void
+elanmoc2_enroll (FpDevice *device)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ fp_info ("[elanmoc2] New enroll operation");
+
+ self->enroll_stage = 0;
+ fpi_device_get_enroll_data (device, &self->enroll_print);
+
+ self->ssm = fpi_ssm_new (device, elanmoc2_enroll_run_state,
+ ENROLL_NUM_STATES);
+ self->enrolled_num_retries = 0;
+ fpi_ssm_start (self->ssm, elanmoc2_enroll_ssm_completed_callback);
+}
+
+static void
+elanmoc2_delete_run_state (FpiSsm *ssm, FpDevice *device)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GBytes) buffer_in = g_steal_pointer (&self->buffer_in);
+
+ const guint8 *data_in =
+ buffer_in != NULL ? g_bytes_get_data (buffer_in, NULL) : NULL;
+ const gsize data_in_len =
+ buffer_in != NULL ? g_bytes_get_size (buffer_in) : 0;
+
+ switch (fpi_ssm_get_cur_state (ssm))
+ {
+ case DELETE_GET_NUM_ENROLLED:
+ elanmoc2_perform_get_num_enrolled (self, ssm);
+ break;
+
+ case DELETE_DELETE: {
+ if (data_in_len == 0)
+ {
+ error =
+ elanmoc2_get_num_enrolled_retry_or_error (self,
+ ssm,
+ DELETE_GET_NUM_ENROLLED);
+ if (error != NULL)
+ {
+ fpi_device_delete_complete (device, g_steal_pointer (&error));
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ }
+ break;
+ }
+
+ g_assert_nonnull (data_in);
+ g_assert (data_in_len >= 2);
+
+ self->enrolled_num = data_in[1];
+ if (self->enrolled_num == 0)
+ {
+ fp_info ("No fingers enrolled, nothing to delete");
+ fpi_device_delete_complete (device, NULL);
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ break;
+ }
+ FpPrint *print = NULL;
+ fpi_device_get_delete_data (device, &print);
+
+ guint8 finger_id = 0xFF;
+
+ g_autoptr(GBytes) user_id =
+ elanmoc2_print_get_data (print, &finger_id);
+ gsize user_id_bytes = MIN (cmd_delete.out_len - 4,
+ ELANMOC2_USER_ID_MAX_LEN);
+ user_id_bytes = MIN (user_id_bytes, g_bytes_get_size (user_id));
+
+ g_autoptr(GByteArray) buffer_out = elanmoc2_prepare_cmd (self,
+ &cmd_delete);
+ if (buffer_out == NULL)
+ {
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+
+ g_assert (buffer_out->len >= 4 + user_id_bytes);
+ buffer_out->data[3] = 0xf0 | (finger_id + 5);
+ memcpy (&buffer_out->data[4],
+ g_bytes_get_data (user_id, NULL),
+ user_id_bytes);
+ elanmoc2_cmd_transceive (device, &cmd_delete, buffer_out);
+ break;
+ }
+
+ case DELETE_CHECK_DELETED: {
+ error = NULL;
+
+ g_assert_nonnull (data_in);
+ g_assert (data_in_len >= 2);
+
+ /* If the finger is still enrolled, we don't want to fail the operation,
+ * but we also don't want to report success. We'll just report that the
+ * finger is no longer enrolled. */
+ if (data_in[1] != 0 &&
+ data_in[1] != ELANMOC2_RESP_NOT_ENROLLED)
+ fp_info (
+ "Delete failed with error code %d, assuming no longer enrolled",
+ data_in[1]);
+
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ fpi_device_delete_complete (device, g_steal_pointer (&error));
+
+ break;
+ }
+ }
+}
+
+static void
+elanmoc2_delete (FpDevice *device)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ fp_info ("[elanmoc2] New delete operation");
+ self->ssm = fpi_ssm_new (device, elanmoc2_delete_run_state,
+ DELETE_NUM_STATES);
+ self->enrolled_num_retries = 0;
+ fpi_ssm_start (self->ssm, elanmoc2_ssm_completed_callback);
+}
+
+static void
+elanmoc2_clear_storage_run_state (FpiSsm *ssm, FpDevice *device)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ g_autoptr(GByteArray) buffer_out = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GBytes) buffer_in = g_steal_pointer (&self->buffer_in);
+
+ switch (fpi_ssm_get_cur_state (ssm))
+ {
+ case CLEAR_STORAGE_WIPE_SENSOR:
+ buffer_out = elanmoc2_prepare_cmd (self, &cmd_wipe_sensor);
+ if (buffer_out == NULL)
+ {
+ fpi_ssm_next_state (ssm);
+ break;
+ }
+ elanmoc2_cmd_transceive (device, &cmd_wipe_sensor, buffer_out);
+ fp_info ("Sent sensor wipe command, sensor will hang for ~5 seconds");
+ break;
+
+ case CLEAR_STORAGE_GET_NUM_ENROLLED:
+ elanmoc2_perform_get_num_enrolled (self, ssm);
+ break;
+
+ case CLEAR_STORAGE_CHECK_NUM_ENROLLED: {
+ gsize buffer_in_len = g_bytes_get_size (buffer_in);
+
+ if (buffer_in_len == 0)
+ {
+ error =
+ elanmoc2_get_num_enrolled_retry_or_error (self,
+ ssm,
+ CLEAR_STORAGE_GET_NUM_ENROLLED);
+ if (error != NULL)
+ {
+ fpi_device_clear_storage_complete (device, g_steal_pointer (&error));
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ }
+ break;
+ }
+
+
+ /* It should take around 5 seconds to arrive here after the wipe sensor
+ * command */
+ g_assert_nonnull (buffer_in);
+ g_assert (buffer_in_len >= 2);
+
+ const guint8 *data_in = g_bytes_get_data (buffer_in, NULL);
+ self->enrolled_num = data_in[1];
+
+ if (self->enrolled_num == 0)
+ {
+ fpi_device_clear_storage_complete (device, NULL);
+ fpi_ssm_mark_completed (g_steal_pointer (&self->ssm));
+ }
+ else
+ {
+ error = fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL,
+ "Sensor erase requested but "
+ "storage is not empty");
+ fpi_device_clear_storage_complete (device, error);
+ fpi_ssm_mark_failed (g_steal_pointer (&self->ssm),
+ g_steal_pointer (&error));
+ break;
+ }
+ break;
+ }
+ }
+}
+
+static void
+elanmoc2_clear_storage (FpDevice *device)
+{
+ FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device);
+
+ fp_info ("[elanmoc2] New clear storage operation");
+ self->ssm = fpi_ssm_new (device, elanmoc2_clear_storage_run_state,
+ CLEAR_STORAGE_NUM_STATES);
+ self->enrolled_num_retries = 0;
+ fpi_ssm_start (self->ssm, elanmoc2_ssm_completed_callback);
+}
+
+static void
+fpi_device_elanmoc2_init (FpiDeviceElanMoC2 *self)
+{
+ G_DEBUG_HERE ();
+}
+
+static const FpIdEntry elanmoc2_id_table[] = {
+ {.vid = ELANMOC2_VEND_ID, .pid = 0x0c00, .driver_data = ELANMOC2_ALL_DEV},
+ {.vid = ELANMOC2_VEND_ID, .pid = 0x0c4c, .driver_data = ELANMOC2_ALL_DEV},
+ {.vid = ELANMOC2_VEND_ID, .pid = 0x0c5e, .driver_data = ELANMOC2_DEV_0C5E},
+ {.vid = 0, .pid = 0, .driver_data = 0}
+};
+
+static void
+fpi_device_elanmoc2_class_init (FpiDeviceElanMoC2Class *klass)
+{
+ FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass);
+
+ dev_class->id = FP_COMPONENT;
+ dev_class->full_name = ELANMOC2_DRIVER_FULLNAME;
+
+ dev_class->type = FP_DEVICE_TYPE_USB;
+ dev_class->scan_type = FP_SCAN_TYPE_PRESS;
+ dev_class->id_table = elanmoc2_id_table;
+
+ dev_class->nr_enroll_stages = ELANMOC2_ENROLL_TIMES;
+ dev_class->temp_hot_seconds = -1;
+
+ dev_class->open = elanmoc2_open;
+ dev_class->close = elanmoc2_close;
+ dev_class->identify = elanmoc2_identify_verify;
+ dev_class->verify = elanmoc2_identify_verify;
+ dev_class->enroll = elanmoc2_enroll;
+ dev_class->delete = elanmoc2_delete;
+ dev_class->clear_storage = elanmoc2_clear_storage;
+ dev_class->list = elanmoc2_list;
+ dev_class->cancel = elanmoc2_cancel;
+
+ fpi_device_class_auto_initialize_features (dev_class);
+ dev_class->features |= FP_DEVICE_FEATURE_DUPLICATES_CHECK;
+ dev_class->features |= FP_DEVICE_FEATURE_UPDATE_PRINT;
+}
diff --git a/libfprint/drivers/elanmoc2/elanmoc2.h b/libfprint/drivers/elanmoc2/elanmoc2.h
new file mode 100644
index 0000000..1047f6b
--- /dev/null
+++ b/libfprint/drivers/elanmoc2/elanmoc2.h
@@ -0,0 +1,210 @@
+/*
+ * Driver for ELAN Match-On-Chip sensors
+ * Copyright (C) 2021-2023 Davide Depau <davide@depau.eu>
+ *
+ * Based on original reverse-engineering work by Davide Depau. The protocol has
+ * been reverse-engineered from captures of the official Windows driver, and by
+ * testing commands on the sensor with a multiplatform Python prototype driver:
+ * https://github.com/depau/Elan-Fingerprint-0c4c-PoC/
+ *
+ * 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
+
+// Stdlib includes
+#include <stdbool.h>
+
+// Library includes
+#include <libusb.h>
+
+// Local includes
+#include "fpi-device.h"
+#include "fpi-ssm.h"
+
+#define ELANMOC2_DRIVER_FULLNAME "ELAN Match-on-Chip 2"
+#define ELANMOC2_VEND_ID 0x04f3
+
+#define ELANMOC2_ENROLL_TIMES 8
+#define ELANMOC2_CMD_MAX_LEN 2
+#define ELANMOC2_MAX_PRINTS 10
+#define ELANMOC2_MAX_RETRIES 3
+
+// USB parameters
+#define ELANMOC2_EP_CMD_OUT (0x1 | FPI_USB_ENDPOINT_OUT)
+#define ELANMOC2_EP_CMD_IN (0x3 | FPI_USB_ENDPOINT_IN)
+#define ELANMOC2_EP_MOC_CMD_IN (0x4 | FPI_USB_ENDPOINT_IN)
+#define ELANMOC2_USB_SEND_TIMEOUT 10000
+#define ELANMOC2_USB_RECV_TIMEOUT 10000
+
+// Response codes
+#define ELANMOC2_RESP_MOVE_DOWN 0x41
+#define ELANMOC2_RESP_MOVE_RIGHT 0x42
+#define ELANMOC2_RESP_MOVE_UP 0x43
+#define ELANMOC2_RESP_MOVE_LEFT 0x44
+#define ELANMOC2_RESP_MAX_ENROLLED_REACHED 0xdd
+#define ELANMOC2_RESP_SENSOR_DIRTY 0xfb
+#define ELANMOC2_RESP_NOT_ENROLLED 0xfd
+#define ELANMOC2_RESP_NOT_ENOUGH_SURFACE 0xfe
+
+// Currently only one device is supported, but I'd like to future-proof this driver for any new contributions.
+#define ELANMOC2_ALL_DEV 0
+#define ELANMOC2_DEV_0C5E (1 << 0)
+
+// Subtract the 2-byte header
+#define ELANMOC2_USER_ID_MAX_LEN (cmd_finger_info.in_len - 2)
+#define ELANMOC2_USER_ID_MAX_LEN_0C5E (cmd_finger_info.in_len - 3)
+
+G_DECLARE_FINAL_TYPE (FpiDeviceElanMoC2, fpi_device_elanmoc2, FPI, DEVICE_ELANMOC2, FpDevice)
+
+typedef struct elanmoc2_cmd
+{
+ unsigned char cmd[ELANMOC2_CMD_MAX_LEN];
+ gboolean is_single_byte_command;
+ int out_len;
+ int in_len;
+ int ep_in;
+ unsigned short devices;
+ gboolean cancellable;
+ gboolean ssm_not_required;
+} Elanmoc2Cmd;
+
+
+// Cancellable commands
+
+static const Elanmoc2Cmd cmd_identify = {
+ .cmd = {0xff, 0x03},
+ .out_len = 3,
+ .in_len = 2,
+ .ep_in = ELANMOC2_EP_MOC_CMD_IN,
+ .cancellable = TRUE,
+};
+
+static const Elanmoc2Cmd cmd_enroll = {
+ .cmd = {0xff, 0x01},
+ .out_len = 7,
+ .in_len = 2,
+ .ep_in = ELANMOC2_EP_MOC_CMD_IN,
+ .cancellable = TRUE,
+};
+
+
+// Not cancellable / quick commands
+
+static const Elanmoc2Cmd cmd_get_fw_ver = {
+ .cmd = {0x19},
+ .is_single_byte_command = TRUE,
+ .out_len = 2,
+ .in_len = 2,
+ .ep_in = ELANMOC2_EP_CMD_IN,
+};
+
+static const Elanmoc2Cmd cmd_finger_info = {
+ .cmd = {0xff, 0x12},
+ .out_len = 4,
+ .in_len = 64,
+ .ep_in = ELANMOC2_EP_CMD_IN,
+};
+
+static const Elanmoc2Cmd cmd_get_enrolled_count = {
+ .cmd = {0xff, 0x04},
+ .out_len = 3,
+ .in_len = 2,
+ .ep_in = ELANMOC2_EP_CMD_IN,
+};
+
+static const Elanmoc2Cmd cmd_abort = {
+ .cmd = {0xff, 0x02},
+ .out_len = 3,
+ .in_len = 2,
+ .ep_in = ELANMOC2_EP_CMD_IN,
+ .ssm_not_required = TRUE,
+};
+
+static const Elanmoc2Cmd cmd_commit = {
+ .cmd = {0xff, 0x11},
+ .out_len = 72,
+ .in_len = 2,
+ .ep_in = ELANMOC2_EP_CMD_IN,
+};
+
+static const Elanmoc2Cmd cmd_check_enroll_collision = {
+ .cmd = {0xff, 0x10},
+ .out_len = 3,
+ .in_len = 3,
+ .ep_in = ELANMOC2_EP_CMD_IN,
+};
+
+static const Elanmoc2Cmd cmd_delete = {
+ .cmd = {0xff, 0x13},
+ .out_len = 72,
+ .in_len = 2,
+ .ep_in = ELANMOC2_EP_CMD_IN,
+};
+
+static const Elanmoc2Cmd cmd_wipe_sensor = {
+ .cmd = {0xff, 0x99},
+ .out_len = 3,
+ .in_len = 0,
+ .ep_in = ELANMOC2_EP_CMD_IN,
+};
+
+
+enum IdentifyStates {
+ IDENTIFY_GET_NUM_ENROLLED,
+ IDENTIFY_CHECK_NUM_ENROLLED,
+ IDENTIFY_IDENTIFY,
+ IDENTIFY_GET_FINGER_INFO,
+ IDENTIFY_CHECK_FINGER_INFO,
+ IDENTIFY_NUM_STATES
+};
+
+enum ListStates {
+ LIST_GET_NUM_ENROLLED,
+ LIST_CHECK_NUM_ENROLLED,
+ LIST_GET_FINGER_INFO,
+ LIST_CHECK_FINGER_INFO,
+ LIST_NUM_STATES
+};
+
+enum EnrollStates {
+ ENROLL_GET_NUM_ENROLLED,
+ ENROLL_CHECK_NUM_ENROLLED,
+ ENROLL_EARLY_REENROLL_CHECK,
+ ENROLL_GET_ENROLLED_FINGER_INFO,
+ ENROLL_ATTEMPT_DELETE,
+ ENROLL_CHECK_DELETED,
+ ENROLL_WIPE_SENSOR,
+ ENROLL_ENROLL,
+ ENROLL_CHECK_ENROLLED,
+ ENROLL_LATE_REENROLL_CHECK,
+ ENROLL_COMMIT,
+ ENROLL_CHECK_COMMITTED,
+ ENROLL_NUM_STATES
+};
+
+enum DeleteStates {
+ DELETE_GET_NUM_ENROLLED,
+ DELETE_DELETE,
+ DELETE_CHECK_DELETED,
+ DELETE_NUM_STATES
+};
+
+enum ClearStorageStates {
+ CLEAR_STORAGE_WIPE_SENSOR,
+ CLEAR_STORAGE_GET_NUM_ENROLLED,
+ CLEAR_STORAGE_CHECK_NUM_ENROLLED,
+ CLEAR_STORAGE_NUM_STATES
+};
diff --git a/libfprint/fprint-list-udev-hwdb.c b/libfprint/fprint-list-udev-hwdb.c
index 5cb8d68..90dfbd8 100644
--- a/libfprint/fprint-list-udev-hwdb.c
+++ b/libfprint/fprint-list-udev-hwdb.c
@@ -35,10 +35,7 @@ static const FpIdEntry allowlist_id_table[] = {
{ .vid = 0x047d, .pid = 0x8055 },
{ .vid = 0x04e8, .pid = 0x730b },
{ .vid = 0x04f3, .pid = 0x036b },
- { .vid = 0x04f3, .pid = 0x0c00 },
- { .vid = 0x04f3, .pid = 0x0c4c },
{ .vid = 0x04f3, .pid = 0x0c57 },
- { .vid = 0x04f3, .pid = 0x0c5e },
{ .vid = 0x04f3, .pid = 0x0c5a },
{ .vid = 0x04f3, .pid = 0x0c60 },
{ .vid = 0x04f3, .pid = 0x0c6c },
diff --git a/libfprint/meson.build b/libfprint/meson.build
index 0ca1767..116c411 100644
--- a/libfprint/meson.build
+++ b/libfprint/meson.build
@@ -133,6 +133,8 @@ driver_sources = {
[ 'drivers/elan.c' ],
'elanmoc' :
[ 'drivers/elanmoc/elanmoc.c' ],
+ 'elanmoc2' :
+ [ 'drivers/elanmoc2/elanmoc2.c' ],
'elanspi' :
[ 'drivers/elanspi.c' ],
'nb1010' :
diff --git a/meson.build b/meson.build
index baafa19..9a0aaa4 100644
--- a/meson.build
+++ b/meson.build
@@ -135,6 +135,7 @@ default_drivers = [
'synaptics',
'elan',
'elanmoc',
+ 'elanmoc2',
'uru4000',
'upektc',
'upeksonly',
diff --git a/tests/elanmoc-0c00/custom.py b/tests/elanmoc-0c00/custom.py
new file mode 100644
index 0000000..3df8be7
--- /dev/null
+++ b/tests/elanmoc-0c00/custom.py
@@ -0,0 +1,48 @@
+#!/usr/bin/python3
+
+import sys
+import traceback
+
+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()
+
+d = devices[0]
+del devices
+
+assert d.get_driver() == "elanmoc2"
+assert not d.has_feature(FPrint.DeviceFeature.CAPTURE)
+assert d.has_feature(FPrint.DeviceFeature.IDENTIFY)
+assert d.has_feature(FPrint.DeviceFeature.VERIFY)
+assert d.has_feature(FPrint.DeviceFeature.DUPLICATES_CHECK)
+assert d.has_feature(FPrint.DeviceFeature.STORAGE)
+assert d.has_feature(FPrint.DeviceFeature.STORAGE_LIST)
+assert d.has_feature(FPrint.DeviceFeature.STORAGE_DELETE)
+assert d.has_feature(FPrint.DeviceFeature.STORAGE_CLEAR)
+
+d.open_sync()
+
+# The test aims to stress the "get enrolled count" command. Some devices occasionally respond with
+# an empty payload to this command and the driver should handle this gracefully by retrying the command.
+
+print("clearing device storage")
+d.clear_storage_sync()
+
+print("ensuring device storage is empty")
+stored = d.list_prints_sync()
+assert len(stored) == 0
+
+d.close_sync()
+del d
+del c
+del ctx
diff --git a/tests/elanmoc2/custom.pcapng b/tests/elanmoc2/custom.pcapng
new file mode 100644
index 0000000..3147751
Binary files /dev/null and b/tests/elanmoc2/custom.pcapng differ
diff --git a/tests/elanmoc2/custom.py b/tests/elanmoc2/custom.py
new file mode 100644
index 0000000..345eeac
--- /dev/null
+++ b/tests/elanmoc2/custom.py
@@ -0,0 +1,164 @@
+#!/usr/bin/python3
+
+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()
+
+d = devices[0]
+del devices
+
+assert d.get_driver() == "elanmoc2"
+assert not d.has_feature(FPrint.DeviceFeature.CAPTURE)
+assert d.has_feature(FPrint.DeviceFeature.IDENTIFY)
+assert d.has_feature(FPrint.DeviceFeature.VERIFY)
+assert d.has_feature(FPrint.DeviceFeature.DUPLICATES_CHECK)
+assert d.has_feature(FPrint.DeviceFeature.STORAGE)
+assert d.has_feature(FPrint.DeviceFeature.STORAGE_LIST)
+assert d.has_feature(FPrint.DeviceFeature.STORAGE_DELETE)
+assert d.has_feature(FPrint.DeviceFeature.STORAGE_CLEAR)
+
+d.open_sync()
+
+template = FPrint.Print.new(d)
+template.set_finger(FPrint.Finger.LEFT_INDEX)
+
+
+def dump_print(p: FPrint.Print):
+ print("Type: ", p.get_property("fpi-type"))
+ print("Finger: ", p.get_finger())
+ print("Driver: ", p.get_driver())
+ print("Device ID: ", p.get_device_id())
+ print("FPI data: ", p.get_property("fpi-data"))
+ print("User ID: ", bytes(p.get_property("fpi-data")[1]).decode("utf-8"))
+ print("Description: ", p.get_description())
+ print("Enroll date: ", p.get_enroll_date())
+ print()
+
+
+def enroll_progress(*args):
+ print("finger status: ", d.get_finger_status())
+ print('enroll progress: ' + str(args))
+
+
+def identify_done(dev, res):
+ global identified
+ identified = True
+ identify_match, identify_print = dev.identify_finish(res)
+ print('identification done: ', identify_match, identify_print)
+ assert identify_match.equal(identify_print)
+
+
+print("clearing device storage")
+d.clear_storage_sync()
+
+print("ensuring device storage is empty")
+stored = d.list_prints_sync()
+assert len(stored) == 0
+
+print("enrolling one finger")
+assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
+enrolled = d.enroll_sync(template, None, enroll_progress, None)
+assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
+print("enroll done")
+del template
+
+# Verify before listing since the device may not be in a good mood
+print("verifying the enrolled finger")
+assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
+verify_res, verify_print = d.verify_sync(enrolled)
+assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
+print(f"verify done, {verify_res}, {verify_print}")
+assert verify_res
+
+print("ensuring device storage has the enrolled finger")
+stored = d.list_prints_sync()
+assert len(stored) == 1
+assert stored[0].equal(enrolled)
+del enrolled
+del verify_print
+
+print("attempting to enroll the same finger again")
+template = FPrint.Print.new(d)
+template.set_finger(FPrint.Finger.LEFT_INDEX)
+assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
+new_enrolled = d.enroll_sync(template, None, enroll_progress, None)
+assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
+print("enroll done")
+
+print("ensuring device storage has the enrolled finger")
+stored = d.list_prints_sync()
+assert len(stored) == 1
+assert stored[0].equal(new_enrolled)
+
+print("enrolling another finger")
+template: FPrint.Print = FPrint.Print.new(d)
+template.set_finger(FPrint.Finger.RIGHT_LITTLE)
+assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
+enrolled2 = d.enroll_sync(template, None, enroll_progress, None)
+assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
+print("enroll done")
+del template
+
+print("verifying the enrolled finger")
+assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
+verify_res, verify_print = d.verify_sync(enrolled2)
+assert d.get_finger_status() == FPrint.FingerStatusFlags.NONE
+print("verify done")
+assert verify_res
+del verify_print
+
+print("ensuring device storage has both enrolled fingers")
+stored = d.list_prints_sync()
+assert len(stored) == 2
+for p in stored:
+ assert p.equal(new_enrolled) or p.equal(enrolled2)
+
+print("identifying the enrolled fingers")
+identified = False
+deserialized_prints = []
+for p in stored:
+ deserialized_prints.append(FPrint.Print.deserialize(p.serialize()))
+ assert deserialized_prints[-1].equal(p)
+del stored
+del p
+
+d.identify(deserialized_prints, callback=identify_done)
+del deserialized_prints
+
+while not identified:
+ ctx.iteration(True)
+
+print("delete the first enrolled finger")
+d.delete_print_sync(new_enrolled)
+
+print("ensuring device storage has only the second enrolled finger")
+stored = d.list_prints_sync()
+assert len(stored) == 1
+assert stored[0].equal(enrolled2)
+
+print("delete the second enrolled finger")
+d.delete_print_sync(enrolled2)
+
+print("ensuring device storage is empty")
+stored = d.list_prints_sync()
+assert len(stored) == 0
+
+del stored
+del enrolled2
+del new_enrolled
+d.close_sync()
+del d
+del c
+del ctx
diff --git a/tests/elanmoc2/device b/tests/elanmoc2/device
new file mode 100644
index 0000000..25c58e0
--- /dev/null
+++ b/tests/elanmoc2/device
@@ -0,0 +1,259 @@
+P: /devices/pci0000:00/0000:00:14.0/usb3/3-9
+N: bus/usb/003/004=1201000200000008F3044C0C04030102000109025300010100A0320904000008FF0000000921100100012215000705810240000107050102400001070582024000010705020240000107058302400001070503024000010705840240000107050402400001
+E: BUSNUM=003
+E: CURRENT_TAGS=:seat:
+E: DEVNAME=/dev/bus/usb/003/004
+E: DEVNUM=004
+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-usb-0_9
+E: ID_MODEL=ELAN:ARM-M4
+E: ID_MODEL_ENC=ELAN:ARM-M4
+E: ID_MODEL_ID=0c4c
+E: ID_PATH=pci-0000:00:14.0-usb-0:9
+E: ID_PATH_TAG=pci-0000_00_14_0-usb-0_9
+E: ID_PATH_WITH_USB_REVISION=pci-0000:00:14.0-usbv2-0:9
+E: ID_PERSIST=0
+E: ID_REVISION=0304
+E: ID_SERIAL=ELAN_ELAN:ARM-M4
+E: ID_USB_INTERFACES=:ff0000:
+E: ID_USB_MODEL=ELAN:ARM-M4
+E: ID_USB_MODEL_ENC=ELAN:ARM-M4
+E: ID_USB_MODEL_ID=0c4c
+E: ID_USB_REVISION=0304
+E: ID_USB_SERIAL=ELAN_ELAN:ARM-M4
+E: ID_USB_VENDOR=ELAN
+E: ID_USB_VENDOR_ENC=ELAN
+E: ID_USB_VENDOR_ID=04f3
+E: ID_VENDOR=ELAN
+E: ID_VENDOR_ENC=ELAN
+E: ID_VENDOR_FROM_DATABASE=Elan Microelectronics Corp.
+E: ID_VENDOR_ID=04f3
+E: MAJOR=189
+E: MINOR=259
+E: NVME_HOST_IFACE=none
+E: PRODUCT=4f3/c4c/304
+E: SUBSYSTEM=usb
+E: TAGS=:seat:
+E: TYPE=0/0/0
+A: authorized=1\n
+A: avoid_reset_quirk=0\n
+A: bConfigurationValue=1\n
+A: bDeviceClass=00\n
+A: bDeviceProtocol=00\n
+A: bDeviceSubClass=00\n
+A: bMaxPacketSize0=8\n
+A: bMaxPower=100mA\n
+A: bNumConfigurations=1\n
+A: bNumInterfaces= 1\n
+A: bcdDevice=0304\n
+A: bmAttributes=a0\n
+A: busnum=3\n
+A: configuration=
+H: descriptors=1201000200000008F3044C0C04030102000109025300010100A0320904000008FF0000000921100100012215000705810240000107050102400001070582024000010705020240000107058302400001070503024000010705840240000107050402400001
+A: dev=189:259\n
+A: devnum=4\n
+A: devpath=9\n
+L: driver=../../../../../bus/usb/drivers/usb
+L: firmware_node=../../../../LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:4e/device:4f/device:60
+A: idProduct=0c4c\n
+A: idVendor=04f3\n
+A: ltm_capable=no\n
+A: manufacturer=ELAN\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=top\n
+A: physical_location/vertical_position=upper\n
+L: port=../3-0:1.0/usb3-port9
+A: power/active_duration=74066\n
+A: power/autosuspend=2\n
+A: power/autosuspend_delay_ms=2000\n
+A: power/connected_duration=15594864\n
+A: power/control=auto\n
+A: power/level=auto\n
+A: power/persist=0\n
+A: power/runtime_active_time=74210\n
+A: power/runtime_status=active\n
+A: power/runtime_suspended_time=15458081\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=ELAN:ARM-M4\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=181\n
+A: version= 2.00\n
+
+P: /devices/pci0000:00/0000:00:14.0/usb3
+N: bus/usb/003/001=12010002090001406B1D020008060302010109021900010100E0000904000001090000000705810304000C
+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=0608
+E: ID_SERIAL=Linux_6.8.2-zen2-1-zen_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=0608
+E: ID_USB_SERIAL=Linux_6.8.2-zen2-1-zen_xhci-hcd_xHCI_Host_Controller_0000:00:14.0
+E: ID_USB_SERIAL_SHORT=0000:00:14.0
+E: ID_USB_VENDOR=Linux_6.8.2-zen2-1-zen_xhci-hcd
+E: ID_USB_VENDOR_ENC=Linux\x206.8.2-zen2-1-zen\x20xhci-hcd
+E: ID_USB_VENDOR_ID=1d6b
+E: ID_VENDOR=Linux_6.8.2-zen2-1-zen_xhci-hcd
+E: ID_VENDOR_ENC=Linux\x206.8.2-zen2-1-zen\x20xhci-hcd
+E: ID_VENDOR_FROM_DATABASE=Linux Foundation
+E: ID_VENDOR_ID=1d6b
+E: MAJOR=189
+E: MINOR=256
+E: PRODUCT=1d6b/2/608
+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=0608\n
+A: bmAttributes=e0\n
+A: busnum=3\n
+A: configuration=
+H: descriptors=12010002090001406B1D020008060302010109021900010100E0000904000001090000000705810304000C
+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:4e/device:4f
+A: idProduct=0002\n
+A: idVendor=1d6b\n
+A: interface_authorized_default=1\n
+A: ltm_capable=no\n
+A: manufacturer=Linux 6.8.2-zen2-1-zen xhci-hcd\n
+A: maxchild=12\n
+A: power/active_duration=15532764\n
+A: power/autosuspend=0\n
+A: power/autosuspend_delay_ms=0\n
+A: power/connected_duration=15595619\n
+A: power/control=auto\n
+A: power/level=auto\n
+A: power/runtime_active_time=15533289\n
+A: power/runtime_status=active\n
+A: power/runtime_suspended_time=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=490\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:v00008086d0000A0EDsv0000103Csd000087F7bc0Csc03i30
+E: PCI_CLASS=C0330
+E: PCI_ID=8086:A0ED
+E: PCI_SLOT_NAME=0000:00:14.0
+E: PCI_SUBSYS_ID=103C:87F7
+E: SUBSYSTEM=pci
+A: ari_enabled=0\n
+A: broken_parity_status=0\n
+A: class=0x0c0330\n
+H: config=8680EDA0060490022030030C000080000400263F6000000000000000000000000000000000000000000000003C10F787000000007000000000000000FF010000FD0134A089C27F8000000000000000003F6DDC0F000000000000000000000000316000000000000000000000000000000180C2C10800000000000000000000000590B7001806E0FE000000000000000009B014F01000400100000000C10A080000080E00001800008F50020000010000090000018680C00009001014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000B50F210112000000
+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:4e
+L: iommu=../../virtual/iommu/dmar3
+L: iommu_group=../../../kernel/iommu_groups/9
+A: irq=155\n
+A: local_cpulist=0-7\n
+A: local_cpus=ff\n
+A: modalias=pci:v00008086d0000A0EDsv0000103Csd000087F7bc0Csc03i30\n
+A: msi_bus=1\n
+A: msi_irqs/155=msi\n
+A: msi_irqs/156=msi\n
+A: msi_irqs/157=msi\n
+A: msi_irqs/158=msi\n
+A: msi_irqs/159=msi\n
+A: msi_irqs/160=msi\n
+A: msi_irqs/161=msi\n
+A: msi_irqs/162=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 28 29 2112 29\nxHCI ring segments 93 93 4096 93\nbuffer-2048 0 0 2048 0\nbuffer-512 3 8 512 1\nbuffer-128 4 32 128 1\nbuffer-32 0 0 32 0\n
+A: power/control=auto\n
+A: power/runtime_active_time=15533652\n
+A: power/runtime_status=active\n
+A: power/runtime_suspended_time=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=0\n
+A: power/wakeup_count=0\n
+A: power/wakeup_expire_count=0\n
+A: power/wakeup_last_time_ms=0\n
+A: power/wakeup_max_time_ms=0\n
+A: power/wakeup_total_time_ms=0\n
+A: power_state=D0\n
+A: resource=0x000000603f260000 0x000000603f26ffff 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=0x20\n
+A: subsystem_device=0x87f7\n
+A: subsystem_vendor=0x103c\n
+A: vendor=0x8086\n
+
diff --git a/tests/meson.build b/tests/meson.build
index 07c924b..34243bd 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -38,6 +38,7 @@ drivers_tests = [
'elan',
'elan-cobo',
'elanmoc',
+ 'elanmoc2',
'elanspi',
'synaptics',
'upektc_img',