From fb214669a1a04debaa6386809af1cb9dc75b5a1f Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Sun, 19 Sep 2021 02:52:16 +0200 Subject: [PATCH 1/8] elanmoc2: Add new driver for ELAN 0C4C --- libfprint/drivers/elanmoc2/elanmoc2.c | 1489 +++++++++++++++++++++++++ libfprint/drivers/elanmoc2/elanmoc2.h | 210 ++++ libfprint/meson.build | 2 + meson.build | 1 + 4 files changed, 1702 insertions(+) create mode 100644 libfprint/drivers/elanmoc2/elanmoc2.c create mode 100644 libfprint/drivers/elanmoc2/elanmoc2.h diff --git a/libfprint/drivers/elanmoc2/elanmoc2.c b/libfprint/drivers/elanmoc2/elanmoc2.c new file mode 100644 index 00000000..fe2aca4f --- /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 + * + * 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 +#include + +// 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 < ELANMOC2_MAX_PRINTS) + { + 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 00000000..1047f6b6 --- /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 + * + * 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 + +// Library includes +#include + +// 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/meson.build b/libfprint/meson.build index 0ca17674..116c4119 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 baafa19c..9a0aaa4b 100644 --- a/meson.build +++ b/meson.build @@ -135,6 +135,7 @@ default_drivers = [ 'synaptics', 'elan', 'elanmoc', + 'elanmoc2', 'uru4000', 'upektc', 'upeksonly', From 3cd942997bf0f7a9e6c13fc7a8d9a596340c248a Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Mon, 20 Mar 2023 20:50:27 +0100 Subject: [PATCH 2/8] elanmoc2: Add supported devices to udev rules --- data/autosuspend.hwdb | 10 +++++++--- libfprint/fprint-list-udev-hwdb.c | 3 --- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/data/autosuspend.hwdb b/data/autosuspend.hwdb index 9115fb01..04cd2b4e 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/fprint-list-udev-hwdb.c b/libfprint/fprint-list-udev-hwdb.c index 5cb8d68a..90dfbd8d 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 }, From cd00ff8df6f80ea94bb33220d1c3c556053dfd5c Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Wed, 22 Mar 2023 22:51:21 +0100 Subject: [PATCH 3/8] elanmoc2: Add tests --- tests/elanmoc2/custom.pcapng | Bin 0 -> 46948 bytes tests/elanmoc2/custom.py | 164 ++++++++++++++++++++++ tests/elanmoc2/device | 259 +++++++++++++++++++++++++++++++++++ tests/meson.build | 1 + 4 files changed, 424 insertions(+) create mode 100644 tests/elanmoc2/custom.pcapng create mode 100644 tests/elanmoc2/custom.py create mode 100644 tests/elanmoc2/device diff --git a/tests/elanmoc2/custom.pcapng b/tests/elanmoc2/custom.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..31477515a45d4502b7b8d316ba003c6f520e66ed GIT binary patch literal 46948 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 literal 0 HcmV?d00001 diff --git a/tests/elanmoc2/custom.py b/tests/elanmoc2/custom.py new file mode 100644 index 00000000..345eeac5 --- /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 00000000..25c58e05 --- /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 07c924be..34243bd0 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -38,6 +38,7 @@ drivers_tests = [ 'elan', 'elan-cobo', 'elanmoc', + 'elanmoc2', 'elanspi', 'synaptics', 'upektc_img', From 6ddf72e0f4126b37053f90e1c9a6f49e1469541d Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Sun, 7 Apr 2024 19:13:22 +0200 Subject: [PATCH 4/8] WIP NEW TEST --- tests/elanmoc-0c00/custom.py | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/elanmoc-0c00/custom.py diff --git a/tests/elanmoc-0c00/custom.py b/tests/elanmoc-0c00/custom.py new file mode 100644 index 00000000..3df8be70 --- /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 From f0b8bbc60e754d5b34aef3cb6b02d9eb275e5af6 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Sun, 7 Apr 2024 20:28:08 +0200 Subject: [PATCH 5/8] WIP fix list finger count --- libfprint/drivers/elanmoc2/elanmoc2.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libfprint/drivers/elanmoc2/elanmoc2.c b/libfprint/drivers/elanmoc2/elanmoc2.c index fe2aca4f..62a2c1ae 100644 --- a/libfprint/drivers/elanmoc2/elanmoc2.c +++ b/libfprint/drivers/elanmoc2/elanmoc2.c @@ -834,7 +834,7 @@ elanmoc2_list_run_state (FpiSsm *ssm, FpDevice *device) self->print_index++; - if (self->print_index < ELANMOC2_MAX_PRINTS) + if (self->print_index < MIN (ELANMOC2_MAX_PRINTS, self->enrolled_num)) { fpi_ssm_jump_to_state (ssm, LIST_GET_FINGER_INFO); } From a9b1450b5a31ea5c47a323bfb290759e8683b1ff Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Sun, 27 Jul 2025 19:51:07 +0200 Subject: [PATCH 6/8] WIP minor fixes + remove list & delete --- libfprint/drivers/elanmoc2/elanmoc2.c | 311 +------------------------- libfprint/drivers/elanmoc2/elanmoc2.h | 21 +- 2 files changed, 10 insertions(+), 322 deletions(-) diff --git a/libfprint/drivers/elanmoc2/elanmoc2.c b/libfprint/drivers/elanmoc2/elanmoc2.c index 62a2c1ae..637b2b4a 100644 --- a/libfprint/drivers/elanmoc2/elanmoc2.c +++ b/libfprint/drivers/elanmoc2/elanmoc2.c @@ -66,7 +66,7 @@ elanmoc2_cmd_usb_callback (FpiUsbTransfer *transfer, GError *error) { FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2 (device); - gboolean short_is_error = (gboolean) (uintptr_t) user_data; + const gboolean short_is_error = GPOINTER_TO_INT (user_data); if (self->ssm == NULL) { @@ -109,12 +109,10 @@ elanmoc2_cmd_usb_callback (FpiUsbTransfer *transfer, 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), + cmd->is_cancellable ? + fpi_device_get_cancellable (device) : NULL, elanmoc2_cmd_usb_callback, NULL); } @@ -160,14 +158,12 @@ elanmoc2_cmd_transceive_full (FpDevice *device, 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), + cmd->is_cancellable ? + fpi_device_get_cancellable (device) : NULL, elanmoc2_cmd_usb_callback, - (gpointer) (uintptr_t) short_is_error); + GINT_TO_POINTER (short_is_error)); } static void @@ -212,31 +208,6 @@ elanmoc2_print_set_data (FpPrint *print, 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, @@ -319,26 +290,6 @@ elanmoc2_print_new_from_finger_info (FpiDeviceElanMoC2 *self, 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) { @@ -549,7 +500,7 @@ elanmoc2_identify_verify_report (FpDevice *device, FpPrint *print, } fp_info ("Identify: no match"); } - fpi_device_identify_report (device, NULL, NULL, *error); + fpi_device_identify_report (device, NULL, print, *error); return TRUE; } else @@ -724,143 +675,6 @@ elanmoc2_identify_verify (FpDevice *device) 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) @@ -1253,114 +1067,6 @@ elanmoc2_enroll (FpDevice *device) 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) { @@ -1448,7 +1154,6 @@ elanmoc2_clear_storage (FpDevice *device) static void fpi_device_elanmoc2_init (FpiDeviceElanMoC2 *self) { - G_DEBUG_HERE (); } static const FpIdEntry elanmoc2_id_table[] = { @@ -1478,9 +1183,7 @@ fpi_device_elanmoc2_class_init (FpiDeviceElanMoC2Class *klass) 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); diff --git a/libfprint/drivers/elanmoc2/elanmoc2.h b/libfprint/drivers/elanmoc2/elanmoc2.h index 1047f6b6..2a99089d 100644 --- a/libfprint/drivers/elanmoc2/elanmoc2.h +++ b/libfprint/drivers/elanmoc2/elanmoc2.h @@ -77,7 +77,7 @@ typedef struct elanmoc2_cmd int in_len; int ep_in; unsigned short devices; - gboolean cancellable; + gboolean is_cancellable; gboolean ssm_not_required; } Elanmoc2Cmd; @@ -89,7 +89,7 @@ static const Elanmoc2Cmd cmd_identify = { .out_len = 3, .in_len = 2, .ep_in = ELANMOC2_EP_MOC_CMD_IN, - .cancellable = TRUE, + .is_cancellable = TRUE, }; static const Elanmoc2Cmd cmd_enroll = { @@ -97,7 +97,7 @@ static const Elanmoc2Cmd cmd_enroll = { .out_len = 7, .in_len = 2, .ep_in = ELANMOC2_EP_MOC_CMD_IN, - .cancellable = TRUE, + .is_cancellable = TRUE, }; @@ -171,14 +171,6 @@ enum IdentifyStates { 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, @@ -195,13 +187,6 @@ enum EnrollStates { 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, From c20c7bf5c70fe79bf00f3b1327ff0f9a4f96e7f6 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Sun, 27 Jul 2025 20:02:00 +0200 Subject: [PATCH 7/8] WIP add user reported usb:v04F3p0C90* --- data/autosuspend.hwdb | 1 + libfprint/drivers/elanmoc2/elanmoc2.c | 1 + 2 files changed, 2 insertions(+) diff --git a/data/autosuspend.hwdb b/data/autosuspend.hwdb index 04cd2b4e..b52847dd 100644 --- a/data/autosuspend.hwdb +++ b/data/autosuspend.hwdb @@ -170,6 +170,7 @@ usb:v04F3p0CA3* usb:v04F3p0C00* usb:v04F3p0C4C* usb:v04F3p0C5E* +usb:v04F3p0C90* ID_AUTOSUSPEND=1 ID_PERSIST=0 diff --git a/libfprint/drivers/elanmoc2/elanmoc2.c b/libfprint/drivers/elanmoc2/elanmoc2.c index 637b2b4a..e7c2adf4 100644 --- a/libfprint/drivers/elanmoc2/elanmoc2.c +++ b/libfprint/drivers/elanmoc2/elanmoc2.c @@ -1160,6 +1160,7 @@ 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 = ELANMOC2_VEND_ID, .pid = 0x0c90, .driver_data = ELANMOC2_ALL_DEV}, {.vid = 0, .pid = 0, .driver_data = 0} }; From 11f0316d069cc90c154c8cb0e46478388c5e2a74 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Sun, 27 Jul 2025 20:03:55 +0200 Subject: [PATCH 8/8] WIP add 0c7c https://gitlab.freedesktop.org/depau/libfprint/-/issues/6 --- data/autosuspend.hwdb | 1 + libfprint/drivers/elanmoc2/elanmoc2.c | 1 + libfprint/fp-device.c | 5 +++-- libfprint/fpi-device.c | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/data/autosuspend.hwdb b/data/autosuspend.hwdb index b52847dd..1c4a7fdb 100644 --- a/data/autosuspend.hwdb +++ b/data/autosuspend.hwdb @@ -170,6 +170,7 @@ usb:v04F3p0CA3* usb:v04F3p0C00* usb:v04F3p0C4C* usb:v04F3p0C5E* +usb:v04F3p0C7C* usb:v04F3p0C90* ID_AUTOSUSPEND=1 ID_PERSIST=0 diff --git a/libfprint/drivers/elanmoc2/elanmoc2.c b/libfprint/drivers/elanmoc2/elanmoc2.c index e7c2adf4..bec6aeb2 100644 --- a/libfprint/drivers/elanmoc2/elanmoc2.c +++ b/libfprint/drivers/elanmoc2/elanmoc2.c @@ -1160,6 +1160,7 @@ 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 = ELANMOC2_VEND_ID, .pid = 0x0c7c, .driver_data = ELANMOC2_ALL_DEV}, {.vid = ELANMOC2_VEND_ID, .pid = 0x0c90, .driver_data = ELANMOC2_ALL_DEV}, {.vid = 0, .pid = 0, .driver_data = 0} }; diff --git a/libfprint/fp-device.c b/libfprint/fp-device.c index ab06e7f5..755d1e01 100644 --- a/libfprint/fp-device.c +++ b/libfprint/fp-device.c @@ -1644,8 +1644,9 @@ fp_device_delete_print (FpDevice *device, /* Succeed immediately if delete is not implemented. */ if (!cls->delete || !(priv->features & FP_DEVICE_FEATURE_STORAGE_DELETE)) - { - g_task_return_boolean (task, TRUE); + { + g_task_return_error (task, + fpi_device_error_new (FP_DEVICE_ERROR_NOT_SUPPORTED)); return; } diff --git a/libfprint/fpi-device.c b/libfprint/fpi-device.c index a1be30c0..d2efce62 100644 --- a/libfprint/fpi-device.c +++ b/libfprint/fpi-device.c @@ -80,7 +80,7 @@ fpi_device_class_auto_initialize_features (FpDeviceClass *device_class) if (device_class->clear_storage) device_class->features |= FP_DEVICE_FEATURE_STORAGE_CLEAR; - if (device_class->delete && (device_class->list || device_class->clear_storage)) + if (device_class->clear_storage || (device_class->delete && device_class->list)) device_class->features |= FP_DEVICE_FEATURE_STORAGE; if (device_class->temp_hot_seconds < 0)