From 50cc336455e66f0f14210fa48d3a045fa7edd1a0 Mon Sep 17 00:00:00 2001 From: Davide Depau Date: Sun, 19 Sep 2021 02:52:16 +0200 Subject: [PATCH] TEMP elanmoc2 --- libfprint/drivers/elanmoc2/elanmoc2.c | 827 ++++++++++++++++++++++++++ libfprint/drivers/elanmoc2/elanmoc2.h | 195 ++++++ libfprint/meson.build | 2 + meson.build | 1 + 4 files changed, 1025 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 0000000..df6a91c --- /dev/null +++ b/libfprint/drivers/elanmoc2/elanmoc2.c @@ -0,0 +1,827 @@ +/* + * Driver for ELAN Match-On-Chip sensors + * Copyright (C) 2021 Davide Depau + * + * 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 short dev_type; + + /* USB response data */ + unsigned char *buffer_in; + gssize buffer_in_len; + + /* Command status data */ + FpiSsm *ssm; + unsigned char enrolled_num; + unsigned char print_index; + GPtrArray *list_result; + + // Enroll + gint enroll_stage; + FpPrint *enroll_print; +}; + +G_DEFINE_TYPE (FpiDeviceElanMoC2, fpi_device_elanmoc2, FP_TYPE_DEVICE); + + +static void +elanmoc2_cmd_usb_receive_callback(FpiUsbTransfer *transfer, FpDevice *device, gpointer user_data, GError *error) { + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2(device); + + if (self->ssm == NULL) { + fp_info("Received USB callback with no ongoing action"); + if (error) + fp_info ("USB callback error: %s", error->message); + return; + } + + if (error) { + fpi_ssm_mark_failed(g_steal_pointer(&self->ssm), error); + } else 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 { + self->buffer_in = g_memdup(transfer->buffer, transfer->actual_length); + self->buffer_in_len = transfer->actual_length; + fpi_ssm_next_state(self->ssm); + } +} + +static gboolean +elanmoc2_cmd_send_sync(FpDevice *device, const struct elanmoc2_cmd *cmd, guint8 *buffer_out, GError **error) { + 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_steal_pointer(&buffer_out), cmd->out_len, + g_free); + return fpi_usb_transfer_submit_sync(transfer_out, ELANMOC2_USB_SEND_TIMEOUT, error); +} + +static void +elanmoc2_cmd_transceive(FpDevice *device, FpiSsm *ssm, const struct elanmoc2_cmd *cmd, guint8 *buffer_out) { + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2(device); + + GError *error = NULL; + gboolean send_status = elanmoc2_cmd_send_sync(device, cmd, g_steal_pointer(&buffer_out), &error); + if (!send_status) + return fpi_ssm_mark_failed(g_steal_pointer(&self->ssm), error); + + FpiUsbTransfer *transfer_in = fpi_usb_transfer_new(device); + transfer_in->short_is_error = TRUE; + + fpi_usb_transfer_fill_bulk(transfer_in, cmd->ep_in, cmd->in_len); + fpi_usb_transfer_submit(transfer_in, + cmd->cancellable ? ELANMOC2_USB_RECV_CANCELLABLE_TIMEOUT : ELANMOC2_USB_RECV_TIMEOUT, + cmd->cancellable ? fpi_device_get_cancellable(device) : NULL, + elanmoc2_cmd_usb_receive_callback, + NULL); +} + +static uint8_t * +elanmoc2_prepare_cmd(FpiDeviceElanMoC2 *self, const struct elanmoc2_cmd *cmd) { + if (cmd->devices != ELANMOC2_ALL_DEV && !(cmd->devices & self->dev_type)) + return NULL; + + g_autofree uint8_t *buffer = g_malloc0(cmd->out_len); + buffer[0] = 0x40; + memcpy(&buffer[1], cmd->cmd, cmd->short_command ? 1 : 2); + return g_steal_pointer(&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 void +elanmoc2_print_get_data(FpPrint *print, guchar *finger_id, guchar *user_id_len, const guchar **user_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); + + *user_id_len = user_id_len_s; + *user_id = g_memdup(user_id_tmp, user_id_len_s); +} + +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 FpPrint * +elanmoc2_print_new_from_finger_info(FpiDeviceElanMoC2 *self, guint8 finger_id, const guint8 *finger_info_response) { + guint8 user_id_max_len = cmd_finger_info.in_len - 2; // 2-byte header + + g_autofree guint8 *user_id = g_malloc(user_id_max_len + 1); + memcpy(user_id, &finger_info_response[2], user_id_max_len); + user_id[user_id_max_len] = '\0'; + + guint8 user_id_len = user_id_max_len; + if (g_str_has_prefix((const gchar *) user_id, "FP1-")) + user_id_len = strnlen((const char *) user_id, user_id_max_len); + + fp_info("Creating new print: finger %d, user id[%d]: %s", finger_id, user_id_len, user_id); + + FpPrint *print = elanmoc2_print_new_with_user_id(self, finger_id, user_id_len, user_id); + + if (!fpi_print_fill_from_user_id(print, (const char *) user_id)) { + // 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(const guint8 *finger_info_response) { + // Response for not enrolled finger is either all 0x00 or 0xFF + guchar first_byte = finger_info_response[2]; + if (first_byte != 0x00 && first_byte != 0xFF) + return TRUE; + + for (gsize i = 3; i < cmd_finger_info.in_len; i++) { + if (finger_info_response[i] != first_byte) + return TRUE; + } + return FALSE; +} + +static void +elanmoc2_cancel(FpDevice *device) { + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2(device); + fp_info("Cancelling any ongoing requests"); + + GError *error = NULL; + g_autofree uint8_t *buffer_out = elanmoc2_prepare_cmd(self, &cmd_abort); + elanmoc2_cmd_send_sync(device, &cmd_abort, g_steal_pointer(&buffer_out), &error); + + if (error) { + fp_warn("Error while cancelling action: %s", error->message); + g_clear_error(&error); + } +} + +static void +elanmoc2_open(FpDevice *device) { + GError *error = NULL; + FpiDeviceElanMoC2 *self; + + G_DEBUG_HERE (); + + if (!g_usb_device_reset(fpi_device_get_usb_device(device), &error)) + return fpi_device_open_complete(device, error); + + if (!g_usb_device_claim_interface(fpi_device_get_usb_device(FP_DEVICE(device)), 0, 0, &error)) + return fpi_device_open_complete(device, 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) { + 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, 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) { + g_autofree uint8_t *buffer_out = NULL; + if ((buffer_out = elanmoc2_prepare_cmd(self, &cmd_get_enrolled_count)) == NULL) { + fpi_ssm_next_state(ssm); + return; + } + elanmoc2_cmd_transceive(FP_DEVICE(self), ssm, &cmd_get_enrolled_count, g_steal_pointer(&buffer_out)); + fp_info("Sent query for number of enrolled fingers"); +} + +/** + * Checks a command status code and, if an error has occurred, creates a new error object. + * Returns whether the operation needs to be retried.. + * @param self FpiDeviceElanMoC2 pointer + * @param error A pointer where the GError will be placed in case of errors + * @return Whether the current action should be retried + */ +static gboolean +elanmoc2_get_finger_error(FpiDeviceElanMoC2 *self, GError **error) { + g_assert(self->buffer_in != NULL); + + // Regular status codes never have the most-significant nibble set; errors do + if ((self->buffer_in[1] & 0xF0) == 0) { + *error = NULL; + return TRUE; + } + switch ((unsigned char) self->buffer_in[1]) { + case ELANMOC2_RESP_MOVE_DOWN: + *error = fpi_device_retry_new_msg(FP_DEVICE_RETRY_CENTER_FINGER, + "Move your finger slightly downwards"); + return TRUE; + case ELANMOC2_RESP_MOVE_RIGHT: + *error = fpi_device_retry_new_msg(FP_DEVICE_RETRY_CENTER_FINGER, + "Move your finger slightly to the right"); + return TRUE; + case ELANMOC2_RESP_MOVE_UP: + *error = fpi_device_retry_new_msg(FP_DEVICE_RETRY_CENTER_FINGER, + "Move your finger slightly upwards"); + return TRUE; + case ELANMOC2_RESP_MOVE_LEFT: + *error = fpi_device_retry_new_msg(FP_DEVICE_RETRY_CENTER_FINGER, + "Move your finger slightly to the left"); + return TRUE; + case ELANMOC2_RESP_MAX_ENROLLED_REACHED: + *error = fpi_device_retry_new_msg(FP_DEVICE_RETRY_CENTER_FINGER, + "Move your finger slightly to the right"); + return TRUE; + case ELANMOC2_RESP_SENSOR_DIRTY: + *error = fpi_device_retry_new_msg(FP_DEVICE_RETRY_REMOVE_FINGER, + "Sensor is dirty or wet"); + return TRUE; + case ELANMOC2_RESP_NOT_ENOUGH_SURFACE: + *error = fpi_device_retry_new_msg(FP_DEVICE_RETRY_REMOVE_FINGER, + "Press your finger slightly harder on the sensor"); + return TRUE; + case ELANMOC2_RESP_NOT_ENROLLED: + *error = fpi_device_error_new_msg(FP_DEVICE_ERROR_DATA_NOT_FOUND, + "Finger not recognized"); + return FALSE; + default: + *error = fpi_device_error_new_msg(FP_DEVICE_ERROR_GENERAL, + "Unknown error"); + return FALSE; + } +} + +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 { + if (error != NULL && error->domain == FP_DEVICE_ERROR && error->code == FP_DEVICE_ERROR_DATA_NOT_FOUND) { + g_clear_error(&error); + FpPrint *expected = NULL; + fpi_device_get_verify_data(device, &expected); + fpi_device_verify_report(device, FPI_MATCH_FAIL, g_steal_pointer(&expected), NULL); + } + fpi_device_verify_complete(device, error); + } +} + +/** + * Calls the correct verify or identify report function based on the input data. + * Returns whether the action should be completed. + * @param device FpDevice + * @param print Identified fingerprint + * @param error Optional error + * @return 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) { + g_autoptr(GPtrArray) gallery = NULL; + fpi_device_get_identify_data(device, &gallery); + g_ptr_array_ref(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)) { + fpi_device_identify_report(device, to_match, print, NULL); + return TRUE; + } + } + } + fpi_device_identify_report(device, NULL, print, NULL); + return TRUE; + } else { + FpiMatchResult result = FPI_MATCH_FAIL; + if (print != NULL) { + FpPrint *to_match = NULL; + fpi_device_get_verify_data(device, &to_match); + if (fp_print_equal(to_match, print)) + result = FPI_MATCH_SUCCESS; + } + 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_autofree uint8_t *buffer_out = NULL; + GError *error = NULL; + + 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: { + self->enrolled_num = self->buffer_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); + } + case IDENTIFY_IDENTIFY: { + if ((buffer_out = elanmoc2_prepare_cmd(self, &cmd_identify)) == NULL) { + fpi_ssm_next_state(ssm); + break; + } + elanmoc2_cmd_transceive(device, ssm, &cmd_identify, g_steal_pointer(&buffer_out)); + fpi_device_report_finger_status(device, FP_FINGER_STATUS_NEEDED); + fp_info("Sent identification request"); + break; + } + case IDENTIFY_GET_FINGER_INFO: { + fpi_device_report_finger_status(device, FP_FINGER_STATUS_PRESENT); + error = NULL; + gboolean retry = elanmoc2_get_finger_error(self, &error); + if (error != NULL) { + fp_info("Identify failed: %s", error->message); + if (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; + } + self->print_index = self->buffer_in[1]; + fp_info("Identified finger %d; requesting finger info", self->print_index); + if ((buffer_out = elanmoc2_prepare_cmd(self, &cmd_finger_info)) == NULL) { + fpi_ssm_next_state(ssm); + break; + } + buffer_out[3] = self->print_index; + elanmoc2_cmd_transceive(device, ssm, &cmd_finger_info, g_steal_pointer(&buffer_out)); + break; + } + case IDENTIFY_CHECK_FINGER_INFO: { + fpi_device_report_finger_status(device, FP_FINGER_STATUS_NONE); + + FpPrint *print = elanmoc2_print_new_from_finger_info(self, self->print_index, self->buffer_in); + + error = NULL; + if (elanmoc2_identify_verify_report(device, print, &error)) { + elanmoc2_identify_verify_complete(device, NULL); + fpi_ssm_mark_completed(g_steal_pointer(&self->ssm)); + } else { + fpi_ssm_jump_to_state(ssm, IDENTIFY_IDENTIFY); + } + + break; + } + default: + break; + } + + g_clear_pointer(&self->buffer_in, g_free); +} + +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); + 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_free); + elanmoc2_ssm_completed_callback(ssm, device, error); +} + +static void +elanmoc2_list_run_state(FpiSsm *ssm, FpDevice *device) { + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2(device); + g_autofree uint8_t *buffer_out = NULL; + + 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: + self->enrolled_num = self->buffer_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: + if ((buffer_out = elanmoc2_prepare_cmd(self, &cmd_finger_info)) == NULL) { + fpi_ssm_next_state(ssm); + break; + } + buffer_out[3] = self->print_index; + elanmoc2_cmd_transceive(device, ssm, &cmd_finger_info, g_steal_pointer(&buffer_out)); + 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); + fp_info("Successfully retrieved finger info for %d", self->print_index); + + if (elanmoc2_finger_info_is_present(self->buffer_in)) { + FpPrint *print = elanmoc2_print_new_from_finger_info(self, self->print_index, self->buffer_in); + g_ptr_array_add(self->list_result, g_object_ref_sink(print)); + } + + 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; + } + + g_clear_pointer(&self->buffer_in, g_free); +} + +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); + 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 behind the scenes. + 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_assert_nonnull(self->enroll_print); + + g_autofree uint8_t *buffer_out = NULL; + GError *error = NULL; + + switch (fpi_ssm_get_cur_state(ssm)) { + case ENROLL_GET_NUM_ENROLLED: + elanmoc2_perform_get_num_enrolled(self, ssm); + break; + case ENROLL_CHECK_NUM_ENROLLED: + self->enrolled_num = self->buffer_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_ENROLL: + if ((buffer_out = elanmoc2_prepare_cmd(self, &cmd_enroll)) == NULL) { + fpi_ssm_next_state(ssm); + break; + } + buffer_out[3] = self->enrolled_num; + buffer_out[4] = ELANMOC2_ENROLL_TIMES; + buffer_out[5] = self->enroll_stage; + buffer_out[6] = 0; + elanmoc2_cmd_transceive(device, ssm, &cmd_enroll, g_steal_pointer(&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); + + if (self->buffer_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 + error = NULL; + gboolean retry = elanmoc2_get_finger_error(self, &error); + if (error != NULL) { + fp_info("Enroll stage failed: %s", error->message); + if (self->buffer_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; + retry = false; + } + if (retry) { + fpi_device_enroll_progress(device, self->enroll_stage, NULL, 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_UNK_AFTER_ENROLL: + fpi_device_report_finger_status(device, FP_FINGER_STATUS_NONE); + if ((buffer_out = elanmoc2_prepare_cmd(self, &cmd_unk_after_enroll)) == NULL) { + fpi_ssm_next_state(ssm); + break; + } + elanmoc2_cmd_transceive(device, ssm, &cmd_unk_after_enroll, g_steal_pointer(&buffer_out)); + fp_info("Unknown after-enroll command sent"); + break; + case ENROLL_COMMIT: + if ((buffer_out = elanmoc2_prepare_cmd(self, &cmd_commit)) == NULL) { + fpi_ssm_next_state(ssm); + break; + } + 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); + + buffer_out[3] = 0xf0 | (self->enrolled_num + 5); + strncpy((char *) &buffer_out[4], user_id, cmd_commit.out_len - 4); + elanmoc2_cmd_transceive(device, ssm, &cmd_commit, g_steal_pointer(&buffer_out)); + fp_info("Commit command sent"); + break; + case ENROLL_CHECK_COMMITTED: + error = NULL; + if (self->buffer_in[1] != 0) { + fp_info("Commit failed with error code %d", self->buffer_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), error); + self->enroll_print = NULL; + } else { + fp_info("Commit succeeded"); + fpi_device_enroll_complete(device, g_steal_pointer(&self->enroll_print), NULL); + fpi_ssm_mark_completed(g_steal_pointer(&self->ssm)); + } + break; + } + + g_clear_pointer(&self->buffer_in, g_free); +} + +static void +elanmoc2_enroll(FpDevice *device) { + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2(device); + fp_info("[elanmoc2] New enroll operation"); + + fpi_device_get_enroll_data(device, &self->enroll_print); + + self->ssm = fpi_ssm_new(device, elanmoc2_enroll_run_state, ENROLL_NUM_STATES); + 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_autofree guint8 *buffer_out = NULL; + g_autofree const guint8 *user_id = NULL; + GError *error = NULL; + + switch (fpi_ssm_get_cur_state(ssm)) { + case DELETE_GET_NUM_ENROLLED: + elanmoc2_perform_get_num_enrolled(self, ssm); + break; + case DELETE_DELETE: + self->enrolled_num = self->buffer_in[1]; + if (self->enrolled_num == 0) { + error = fpi_device_error_new_msg(FP_DEVICE_ERROR_DATA_NOT_FOUND, + "Sensor storage is empty, nothing to delete"); + fpi_device_delete_complete(device, error); + fpi_ssm_mark_failed(g_steal_pointer(&self->ssm), error); + break; + } + FpPrint *print = NULL; + fpi_device_get_delete_data(device, &print); + + guint8 finger_id = 0xFF; + guint8 user_id_len = 0; + elanmoc2_print_get_data(print, &finger_id, &user_id_len, &user_id); + + if ((buffer_out = elanmoc2_prepare_cmd(self, &cmd_delete)) == NULL) { + fpi_ssm_next_state(ssm); + break; + } + buffer_out[3] = 0xf0 | (finger_id + 5); + memcpy((char *) &buffer_out[4], (char *) user_id, MIN(cmd_delete.out_len - 4, user_id_len)); + elanmoc2_cmd_transceive(device, ssm, &cmd_delete, g_steal_pointer(&buffer_out)); + break; + case DELETE_CHECK_DELETED: + error = NULL; + if (self->buffer_in[1] != 0) { + error = fpi_device_error_new_msg(FP_DEVICE_ERROR_DATA_NOT_FOUND, + "Failed to delete fingerprint"); + fpi_ssm_mark_failed(g_steal_pointer(&self->ssm), error); + } else { + fpi_ssm_mark_completed(g_steal_pointer(&self->ssm)); + } + fpi_device_delete_complete(device, error); + break; + } + + g_clear_pointer(&self->buffer_in, g_free); +} + +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); + 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_autofree uint8_t *buffer_out = NULL; + GError *error = NULL; + + switch (fpi_ssm_get_cur_state(ssm)) { + case CLEAR_STORAGE_GET_NUM_ENROLLED: + elanmoc2_perform_get_num_enrolled(self, ssm); + break; + case CLEAR_STORAGE_CHECK_NUM_ENROLLED: + self->enrolled_num = self->buffer_in[1]; + if (self->enrolled_num == 0) { + fpi_device_clear_storage_complete(device, NULL); + fpi_ssm_mark_completed(g_steal_pointer(&self->ssm)); + break; + } + fpi_ssm_next_state(ssm); + break; + case CLEAR_STORAGE_GET_FINGER_INFO: + if (self->print_index >= ELANMOC2_MAX_PRINTS) { + fpi_device_clear_storage_complete(device, NULL); + fpi_ssm_mark_completed(g_steal_pointer(&self->ssm)); + break; + } + + if ((buffer_out = elanmoc2_prepare_cmd(self, &cmd_finger_info)) == NULL) { + fpi_ssm_next_state(ssm); + break; + } + buffer_out[3] = self->print_index; + elanmoc2_cmd_transceive(device, ssm, &cmd_finger_info, g_steal_pointer(&buffer_out)); + break; + case CLEAR_STORAGE_DELETE: + fpi_device_report_finger_status(device, FP_FINGER_STATUS_NONE); + + if (!elanmoc2_finger_info_is_present(self->buffer_in)) { + // Not enrolled + self->print_index++; + fpi_ssm_jump_to_state(ssm, CLEAR_STORAGE_GET_FINGER_INFO); + break; + } + + if ((buffer_out = elanmoc2_prepare_cmd(self, &cmd_delete)) == NULL) { + fpi_ssm_next_state(ssm); + break; + } + buffer_out[3] = 0xf0 | (self->print_index + 5); + memcpy(&buffer_out[4], &self->buffer_in[2], MIN(self->buffer_in_len - 2, cmd_delete.out_len - 4)); + + elanmoc2_cmd_transceive(device, ssm, &cmd_delete, g_steal_pointer(&buffer_out)); + break; + case CLEAR_STORAGE_CHECK_DELETED: + if (self->buffer_in[1] != 0) { + error = fpi_device_error_new_msg(FP_DEVICE_ERROR_GENERAL, + "Failed to delete fingerprint"); + fpi_device_clear_storage_complete(device, error); + fpi_ssm_mark_failed(g_steal_pointer(&self->ssm), error); + break; + } + self->print_index++; + fpi_ssm_jump_to_state(ssm, CLEAR_STORAGE_GET_FINGER_INFO); + break; + } + + g_clear_pointer(&self->buffer_in, g_free); +} + +static void +elanmoc2_clear_storage(FpDevice *device) { + FpiDeviceElanMoC2 *self = FPI_DEVICE_ELANMOC2(device); + fp_info("[elanmoc2] New clear storage operation"); + self->print_index = 0; + self->ssm = fpi_ssm_new(device, elanmoc2_clear_storage_run_state, CLEAR_STORAGE_NUM_STATES); + fpi_ssm_start(self->ssm, elanmoc2_ssm_completed_callback); +} + +static void +fpi_device_elanmoc2_init(FpiDeviceElanMoC2 *self) { + G_DEBUG_HERE (); +} + +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); +} diff --git a/libfprint/drivers/elanmoc2/elanmoc2.h b/libfprint/drivers/elanmoc2/elanmoc2.h new file mode 100644 index 0000000..ce53bd7 --- /dev/null +++ b/libfprint/drivers/elanmoc2/elanmoc2.h @@ -0,0 +1,195 @@ +/* + * Driver for ELAN Match-On-Chip sensors + * Copyright (C) 2021 Davide Depau + * + * 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_LIST_VERIFY_RETRIES 3 + +// USB parameters +#define ELANMOC2_EP_CMD_OUT (0x1 | LIBUSB_ENDPOINT_OUT) +#define ELANMOC2_EP_CMD_IN (0x3 | LIBUSB_ENDPOINT_IN) +#define ELANMOC2_EP_MOC_CMD_IN (0x4 | LIBUSB_ENDPOINT_IN) +#define ELANMOC2_USB_SEND_TIMEOUT 10000 +#define ELANMOC2_USB_RECV_TIMEOUT 10000 +#define ELANMOC2_USB_RECV_CANCELLABLE_TIMEOUT 5000 + +// 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_0C4C (1 << 0) + +G_DECLARE_FINAL_TYPE (FpiDeviceElanMoC2, fpi_device_elanmoc2, FPI, DEVICE_ELANMOC2, FpDevice) + +struct elanmoc2_cmd { + unsigned char cmd[ELANMOC2_CMD_MAX_LEN]; + gboolean short_command; + int out_len; + int in_len; + int ep_in; + unsigned short devices; + gboolean cancellable; +}; + + +// Cancellable commands + +static const struct elanmoc2_cmd cmd_identify = { + .cmd = {0xff, 0x03}, + .out_len = 3, + .in_len = 2, + .ep_in = ELANMOC2_EP_MOC_CMD_IN, + .cancellable = true, +}; + +static const struct elanmoc2_cmd 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 struct elanmoc2_cmd cmd_get_fw_ver = { + .cmd = {0x19}, + .short_command = true, + .out_len = 2, + .in_len = 2, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + +static const struct elanmoc2_cmd cmd_finger_info = { + .cmd = {0xff, 0x12}, + .out_len = 4, + .in_len = 64, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + +static const struct elanmoc2_cmd cmd_get_enrolled_count = { + .cmd = {0xff, 0x04}, + .out_len = 3, + .in_len = 2, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + +static const struct elanmoc2_cmd cmd_abort = { + .cmd = {0xff, 0x02}, + .out_len = 3, + .in_len = 2, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + +static const struct elanmoc2_cmd cmd_commit = { + .cmd = {0xff, 0x11}, + .out_len = 72, + .in_len = 2, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + +static const struct elanmoc2_cmd cmd_unk_after_enroll = { + .cmd = {0xff, 0x10}, + .out_len = 3, + .in_len = 3, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + +static const struct elanmoc2_cmd cmd_delete = { + .cmd = {0xff, 0x13}, + .out_len = 72, + .in_len = 2, + .ep_in = ELANMOC2_EP_CMD_IN, +}; + + +enum identify_states { + IDENTIFY_GET_NUM_ENROLLED, + IDENTIFY_CHECK_NUM_ENROLLED, + IDENTIFY_IDENTIFY, + IDENTIFY_GET_FINGER_INFO, + IDENTIFY_CHECK_FINGER_INFO, + IDENTIFY_NUM_STATES +}; + +enum list_states { + LIST_GET_NUM_ENROLLED, + LIST_CHECK_NUM_ENROLLED, + LIST_GET_FINGER_INFO, + LIST_CHECK_FINGER_INFO, + LIST_NUM_STATES +}; + +enum enroll_states { + ENROLL_GET_NUM_ENROLLED, + ENROLL_CHECK_NUM_ENROLLED, + ENROLL_ENROLL, + ENROLL_CHECK_ENROLLED, + ENROLL_UNK_AFTER_ENROLL, + ENROLL_COMMIT, + ENROLL_CHECK_COMMITTED, + ENROLL_NUM_STATES +}; + +enum delete_states { + DELETE_GET_NUM_ENROLLED, + DELETE_DELETE, + DELETE_CHECK_DELETED, + DELETE_NUM_STATES +}; + +enum clear_storage_states { + CLEAR_STORAGE_GET_NUM_ENROLLED, + CLEAR_STORAGE_CHECK_NUM_ENROLLED, + CLEAR_STORAGE_GET_FINGER_INFO, + CLEAR_STORAGE_DELETE, + CLEAR_STORAGE_CHECK_DELETED, + CLEAR_STORAGE_NUM_STATES +}; + +static const FpIdEntry elanmoc2_id_table[] = { + {.vid = ELANMOC2_VEND_ID, .pid = 0x0c4c, .driver_data = ELANMOC2_ALL_DEV}, + {.vid = 0, .pid = 0, .driver_data = ELANMOC2_DEV_0C4C} +}; diff --git a/libfprint/meson.build b/libfprint/meson.build index 25ed10f..9963979 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -125,6 +125,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 52d3c7b..855c680 100644 --- a/meson.build +++ b/meson.build @@ -118,6 +118,7 @@ default_drivers = [ 'synaptics', 'elan', 'elanmoc', + 'elanmoc2', 'uru4000', 'upektc', 'upeksonly',