dccb5b3ab2
During calibration, the internal state was stored as INACTIVE. This is not true though, the device is actively running state machines. Move the state update to the start of the operation, therefore ensuring we don't deactivate without completing the SSM. Note that this will prevent a crash, but the driver still does not behave quite correctly when such a state change does happen. However, this is just a safety measure as the state change should not happen in the first place. See: #203
1084 lines
29 KiB
C
1084 lines
29 KiB
C
/*
|
|
* Elan driver for libfprint
|
|
*
|
|
* Copyright (C) 2017 Igor Filatov <ia.filatov@gmail.com>
|
|
* Copyright (C) 2018 Sébastien Béchet <sebastien.bechet@osinix.com >
|
|
*
|
|
* 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
|
|
*/
|
|
|
|
/*
|
|
* The algorithm which libfprint uses to match fingerprints doesn't like small
|
|
* images like the ones these drivers produce. There's just not enough minutiae
|
|
* (recognizable print-specific points) on them for a reliable match. This means
|
|
* that unless another matching algo is found/implemented, these readers will
|
|
* not work as good with libfprint as they do with vendor drivers.
|
|
*
|
|
* To get bigger images the driver expects you to swipe the finger over the
|
|
* reader. This works quite well for readers with a rectangular 144x64 sensor.
|
|
* Worse than real swipe readers but good enough for day-to-day use. It needs
|
|
* a steady and relatively slow swipe. There are also square 96x96 sensors and
|
|
* I don't know whether they are in fact usable or not because I don't have one.
|
|
* I imagine they'd be less reliable because the resulting image is even
|
|
* smaller. If they can't be made usable with libfprint, I might end up dropping
|
|
* them because it's better than saying they work when they don't.
|
|
*/
|
|
|
|
#define FP_COMPONENT "elan"
|
|
|
|
#include "drivers_api.h"
|
|
#include "elan.h"
|
|
|
|
static unsigned char
|
|
elan_get_pixel (struct fpi_frame_asmbl_ctx *ctx,
|
|
struct fpi_frame *frame, unsigned int x,
|
|
unsigned int y)
|
|
{
|
|
return frame->data[x + y * ctx->frame_width];
|
|
}
|
|
|
|
static struct fpi_frame_asmbl_ctx assembling_ctx = {
|
|
.frame_width = 0,
|
|
.frame_height = 0,
|
|
.image_width = 0,
|
|
.get_pixel = elan_get_pixel,
|
|
};
|
|
|
|
struct _FpiDeviceElan
|
|
{
|
|
FpImageDevice parent;
|
|
|
|
/* device config */
|
|
unsigned short dev_type;
|
|
unsigned short fw_ver;
|
|
void (*process_frame) (unsigned short *raw_frame,
|
|
GSList ** frames);
|
|
/* end device config */
|
|
|
|
/* commands */
|
|
const struct elan_cmd *cmd;
|
|
int cmd_timeout;
|
|
/* end commands */
|
|
|
|
/* state */
|
|
gboolean deactivating;
|
|
FpImageDeviceState dev_state;
|
|
FpImageDeviceState dev_state_next;
|
|
unsigned char *last_read;
|
|
unsigned char calib_atts_left;
|
|
unsigned char calib_status;
|
|
unsigned short *background;
|
|
unsigned char frame_width;
|
|
unsigned char frame_height;
|
|
unsigned char raw_frame_height;
|
|
int num_frames;
|
|
GSList *frames;
|
|
/* end state */
|
|
};
|
|
G_DECLARE_FINAL_TYPE (FpiDeviceElan, fpi_device_elan, FPI, DEVICE_ELAN,
|
|
FpImageDevice);
|
|
G_DEFINE_TYPE (FpiDeviceElan, fpi_device_elan, FP_TYPE_IMAGE_DEVICE);
|
|
|
|
static int
|
|
cmp_short (const void *a, const void *b)
|
|
{
|
|
return (int) (*(short *) a - *(short *) b);
|
|
}
|
|
|
|
static void
|
|
elan_dev_reset_state (FpiDeviceElan *elandev)
|
|
{
|
|
G_DEBUG_HERE ();
|
|
|
|
elandev->cmd = NULL;
|
|
elandev->cmd_timeout = ELAN_CMD_TIMEOUT;
|
|
|
|
elandev->calib_status = 0;
|
|
|
|
g_free (elandev->last_read);
|
|
elandev->last_read = NULL;
|
|
|
|
g_slist_free_full (elandev->frames, g_free);
|
|
elandev->frames = NULL;
|
|
elandev->num_frames = 0;
|
|
}
|
|
|
|
static void
|
|
elan_save_frame (FpiDeviceElan *self, unsigned short *frame)
|
|
{
|
|
G_DEBUG_HERE ();
|
|
|
|
/* so far 3 types of readers by sensor dimensions and orientation have been
|
|
* seen in the wild:
|
|
* 1. 144x64. Raw images are in portrait orientation while readers themselves
|
|
* are placed (e.g. built into a touchpad) in landscape orientation. These
|
|
* need to be rotated before assembling.
|
|
* 2. 96x96 rotated. Like the first type but square. Likewise, need to be
|
|
* rotated before assembling.
|
|
* 3. 96x96 normal. Square and need NOT be rotated. So far there's only been
|
|
* 1 report of a 0c03 of this type. Hopefully this type can be identified
|
|
* by device id (and manufacturers don't just install the readers as they
|
|
* please).
|
|
* we also discard stripes of 'frame_margin' from bottom and top because
|
|
* assembling works bad for tall frames */
|
|
|
|
unsigned char frame_width = self->frame_width;
|
|
unsigned char frame_height = self->frame_height;
|
|
unsigned char raw_height = self->raw_frame_height;
|
|
unsigned char frame_margin = (raw_height - self->frame_height) / 2;
|
|
int frame_idx, raw_idx;
|
|
|
|
for (int y = 0; y < frame_height; y++)
|
|
for (int x = 0; x < frame_width; x++)
|
|
{
|
|
if (self->dev_type & ELAN_NOT_ROTATED)
|
|
raw_idx = x + (y + frame_margin) * frame_width;
|
|
else
|
|
raw_idx = frame_margin + y + x * raw_height;
|
|
frame_idx = x + y * frame_width;
|
|
frame[frame_idx] =
|
|
((unsigned short *) self->last_read)[raw_idx];
|
|
}
|
|
}
|
|
|
|
static void
|
|
elan_save_background (FpiDeviceElan *elandev)
|
|
{
|
|
G_DEBUG_HERE ();
|
|
|
|
g_free (elandev->background);
|
|
elandev->background =
|
|
g_malloc (elandev->frame_width * elandev->frame_height *
|
|
sizeof (short));
|
|
elan_save_frame (elandev, elandev->background);
|
|
}
|
|
|
|
/* save a frame as part of the fingerprint image
|
|
* background needs to have been captured for this routine to work
|
|
* Elantech recommends 2-step non-linear normalization in order to reduce
|
|
* 2^14 ADC resolution to 2^8 image:
|
|
*
|
|
* 1. background is subtracted (done here)
|
|
*
|
|
* 2. pixels are grouped in 3 groups by intensity and each group is mapped
|
|
* separately onto the normalized frame (done in elan_process_frame_*)
|
|
* ==== 16383 ____> ======== 255
|
|
* /
|
|
* ----- lvl3 __/
|
|
* 35% pixels
|
|
*
|
|
* ----- lvl2 --------> ======== 156
|
|
*
|
|
* 30% pixels
|
|
* ----- lvl1 --------> ======== 99
|
|
*
|
|
* 35% pixels
|
|
* ----- lvl0 __
|
|
* \
|
|
* ======== 0 \____> ======== 0
|
|
*
|
|
* For some devices we don't do 2. but instead do a simple linear mapping
|
|
* because it seems to produce better results (or at least as good):
|
|
* ==== 16383 ___> ======== 255
|
|
* /
|
|
* ------ max __/
|
|
*
|
|
*
|
|
* ------ min __
|
|
* \
|
|
* ======== 0 \___> ======== 0
|
|
*/
|
|
static int
|
|
elan_save_img_frame (FpiDeviceElan *elandev)
|
|
{
|
|
G_DEBUG_HERE ();
|
|
|
|
unsigned int frame_size = elandev->frame_width * elandev->frame_height;
|
|
unsigned short *frame = g_malloc (frame_size * sizeof (short));
|
|
elan_save_frame (elandev, frame);
|
|
unsigned int sum = 0;
|
|
|
|
for (int i = 0; i < frame_size; i++)
|
|
{
|
|
if (elandev->background[i] > frame[i])
|
|
frame[i] = 0;
|
|
else
|
|
frame[i] -= elandev->background[i];
|
|
sum += frame[i];
|
|
}
|
|
|
|
if (sum == 0)
|
|
{
|
|
fp_dbg
|
|
("frame darker than background; finger present during calibration?");
|
|
g_free (frame);
|
|
return -1;
|
|
}
|
|
|
|
elandev->frames = g_slist_prepend (elandev->frames, frame);
|
|
elandev->num_frames += 1;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
elan_process_frame_linear (unsigned short *raw_frame,
|
|
GSList ** frames)
|
|
{
|
|
unsigned int frame_size =
|
|
assembling_ctx.frame_width * assembling_ctx.frame_height;
|
|
struct fpi_frame *frame =
|
|
g_malloc (frame_size + sizeof (struct fpi_frame));
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
unsigned short min = 0xffff, max = 0;
|
|
for (int i = 0; i < frame_size; i++)
|
|
{
|
|
if (raw_frame[i] < min)
|
|
min = raw_frame[i];
|
|
if (raw_frame[i] > max)
|
|
max = raw_frame[i];
|
|
}
|
|
|
|
g_assert (max != min);
|
|
|
|
unsigned short px;
|
|
for (int i = 0; i < frame_size; i++)
|
|
{
|
|
px = raw_frame[i];
|
|
px = (px - min) * 0xff / (max - min);
|
|
frame->data[i] = (unsigned char) px;
|
|
}
|
|
|
|
*frames = g_slist_prepend (*frames, frame);
|
|
}
|
|
|
|
static void
|
|
elan_process_frame_thirds (unsigned short *raw_frame,
|
|
GSList ** frames)
|
|
{
|
|
G_DEBUG_HERE ();
|
|
|
|
unsigned int frame_size =
|
|
assembling_ctx.frame_width * assembling_ctx.frame_height;
|
|
struct fpi_frame *frame =
|
|
g_malloc (frame_size + sizeof (struct fpi_frame));
|
|
|
|
unsigned short lvl0, lvl1, lvl2, lvl3;
|
|
unsigned short *sorted = g_malloc (frame_size * sizeof (short));
|
|
memcpy (sorted, raw_frame, frame_size * sizeof (short));
|
|
qsort (sorted, frame_size, sizeof (short), cmp_short);
|
|
lvl0 = sorted[0];
|
|
lvl1 = sorted[frame_size * 3 / 10];
|
|
lvl2 = sorted[frame_size * 65 / 100];
|
|
lvl3 = sorted[frame_size - 1];
|
|
g_free (sorted);
|
|
|
|
unsigned short px;
|
|
for (int i = 0; i < frame_size; i++)
|
|
{
|
|
px = raw_frame[i];
|
|
if (lvl0 <= px && px < lvl1)
|
|
px = (px - lvl0) * 99 / (lvl1 - lvl0);
|
|
else if (lvl1 <= px && px < lvl2)
|
|
px = 99 + ((px - lvl1) * 56 / (lvl2 - lvl1));
|
|
else // (lvl2 <= px && px <= lvl3)
|
|
px = 155 + ((px - lvl2) * 100 / (lvl3 - lvl2));
|
|
frame->data[i] = (unsigned char) px;
|
|
}
|
|
|
|
*frames = g_slist_prepend (*frames, frame);
|
|
}
|
|
|
|
static void
|
|
elan_submit_image (FpImageDevice *dev)
|
|
{
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
GSList *raw_frames;
|
|
GSList *frames = NULL;
|
|
FpImage *img;
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
raw_frames = g_slist_nth (self->frames, ELAN_SKIP_LAST_FRAMES);
|
|
|
|
assembling_ctx.frame_width = self->frame_width;
|
|
assembling_ctx.frame_height = self->frame_height;
|
|
assembling_ctx.image_width = self->frame_width * 3 / 2;
|
|
g_slist_foreach (raw_frames, (GFunc) self->process_frame, &frames);
|
|
fpi_do_movement_estimation (&assembling_ctx, frames);
|
|
img = fpi_assemble_frames (&assembling_ctx, frames);
|
|
|
|
fpi_image_device_image_captured (dev, img);
|
|
}
|
|
|
|
static void
|
|
elan_cmd_done (FpiSsm *ssm)
|
|
{
|
|
G_DEBUG_HERE ();
|
|
fpi_ssm_next_state (ssm);
|
|
}
|
|
|
|
static void
|
|
elan_cmd_cb (FpiUsbTransfer *transfer, FpDevice *dev,
|
|
gpointer user_data, GError *error)
|
|
{
|
|
FpiSsm *ssm = transfer->ssm;
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
if (error)
|
|
{
|
|
/* XXX: In the cancellation case we used to not
|
|
* mark the SSM as failed?! */
|
|
fpi_ssm_mark_failed (transfer->ssm, error);
|
|
return;
|
|
}
|
|
|
|
/* XXX: We used to reset the device in error cases! */
|
|
if (transfer->endpoint & FPI_USB_ENDPOINT_IN)
|
|
{
|
|
/* just finished receiving */
|
|
self->last_read = g_memdup (transfer->buffer, transfer->actual_length);
|
|
elan_cmd_done (ssm);
|
|
}
|
|
else
|
|
{
|
|
/* just finished sending */
|
|
G_DEBUG_HERE ();
|
|
elan_cmd_read (ssm, dev);
|
|
}
|
|
}
|
|
|
|
static void
|
|
elan_cmd_read (FpiSsm *ssm, FpDevice *dev)
|
|
{
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
FpiUsbTransfer *transfer;
|
|
GCancellable *cancellable = NULL;
|
|
int response_len = self->cmd->response_len;
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
if (self->cmd->response_len == ELAN_CMD_SKIP_READ)
|
|
{
|
|
fp_dbg ("skipping read, not expecting anything");
|
|
elan_cmd_done (ssm);
|
|
return;
|
|
}
|
|
|
|
if (self->dev_type == ELAN_0C42)
|
|
{
|
|
/* ELAN_0C42 sends an extra byte in one byte responses */
|
|
if (self->cmd->response_len == 1)
|
|
response_len = 2;
|
|
}
|
|
|
|
if (self->cmd->cmd == get_image_cmd.cmd)
|
|
/* raw data has 2-byte "pixels" and the frame is vertical */
|
|
response_len =
|
|
self->raw_frame_height * self->frame_width * 2;
|
|
|
|
g_clear_pointer (&self->last_read, g_free);
|
|
|
|
transfer = fpi_usb_transfer_new (dev);
|
|
transfer->ssm = ssm;
|
|
transfer->short_is_error = TRUE;
|
|
|
|
fpi_usb_transfer_fill_bulk (transfer,
|
|
self->cmd->response_in,
|
|
response_len);
|
|
|
|
if (!self->cmd->never_cancel)
|
|
cancellable = fpi_device_get_cancellable (dev);
|
|
|
|
fpi_usb_transfer_submit (transfer, self->cmd_timeout, cancellable, elan_cmd_cb, NULL);
|
|
}
|
|
|
|
static void
|
|
elan_run_cmd (FpiSsm *ssm,
|
|
FpDevice *dev,
|
|
const struct elan_cmd *cmd,
|
|
int cmd_timeout)
|
|
{
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
FpiUsbTransfer *transfer;
|
|
GCancellable *cancellable = NULL;
|
|
|
|
self->cmd = cmd;
|
|
if (cmd_timeout != -1)
|
|
self->cmd_timeout = cmd_timeout;
|
|
|
|
if (cmd->devices != ELAN_ALL_DEV && !(cmd->devices & self->dev_type))
|
|
{
|
|
fp_dbg ("skipping command 0x%x 0x%x for this device (for devices 0x%x but device is 0x%x)",
|
|
cmd->cmd[0], cmd->cmd[1], cmd->devices, self->dev_type);
|
|
elan_cmd_done (ssm);
|
|
return;
|
|
}
|
|
|
|
transfer = fpi_usb_transfer_new (dev);
|
|
transfer->ssm = ssm;
|
|
transfer->short_is_error = TRUE;
|
|
|
|
fpi_usb_transfer_fill_bulk_full (transfer,
|
|
ELAN_EP_CMD_OUT,
|
|
(guint8 *) cmd->cmd,
|
|
ELAN_CMD_LEN,
|
|
NULL);
|
|
|
|
if (!self->cmd->never_cancel)
|
|
cancellable = fpi_device_get_cancellable (dev);
|
|
|
|
fpi_usb_transfer_submit (transfer,
|
|
self->cmd_timeout,
|
|
cancellable,
|
|
elan_cmd_cb,
|
|
NULL);
|
|
}
|
|
|
|
enum stop_capture_states {
|
|
STOP_CAPTURE,
|
|
STOP_CAPTURE_NUM_STATES,
|
|
};
|
|
|
|
static void
|
|
stop_capture_run_state (FpiSsm *ssm, FpDevice *dev)
|
|
{
|
|
G_DEBUG_HERE ();
|
|
|
|
switch (fpi_ssm_get_cur_state (ssm))
|
|
{
|
|
case STOP_CAPTURE:
|
|
elan_run_cmd (ssm, dev, &stop_cmd,
|
|
ELAN_CMD_TIMEOUT);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
stop_capture_complete (FpiSsm *ssm, FpDevice *_dev, GError *error)
|
|
{
|
|
FpImageDevice *dev = FP_IMAGE_DEVICE (_dev);
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
|
|
/* The device is inactive at this point. */
|
|
self->dev_state = FP_IMAGE_DEVICE_STATE_INACTIVE;
|
|
|
|
if (self->deactivating)
|
|
{
|
|
/* Simply complete the pending deactivation. */
|
|
self->deactivating = FALSE;
|
|
fpi_image_device_deactivate_complete (dev, error);
|
|
return;
|
|
}
|
|
|
|
if (!error)
|
|
fpi_image_device_report_finger_status (dev, FALSE);
|
|
else
|
|
/* NOTE: We cannot get a cancellation error here. */
|
|
fpi_image_device_session_error (dev, error);
|
|
}
|
|
|
|
static void
|
|
elan_stop_capture (FpDevice *dev)
|
|
{
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
elan_dev_reset_state (self);
|
|
|
|
FpiSsm *ssm =
|
|
fpi_ssm_new (dev, stop_capture_run_state, STOP_CAPTURE_NUM_STATES);
|
|
fpi_ssm_start (ssm, stop_capture_complete);
|
|
}
|
|
|
|
enum capture_states {
|
|
CAPTURE_LED_ON,
|
|
CAPTURE_WAIT_FINGER,
|
|
CAPTURE_READ_DATA,
|
|
CAPTURE_CHECK_ENOUGH_FRAMES,
|
|
CAPTURE_NUM_STATES,
|
|
};
|
|
|
|
static void
|
|
capture_run_state (FpiSsm *ssm, FpDevice *dev)
|
|
{
|
|
FpImageDevice *idev = FP_IMAGE_DEVICE (dev);
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
int r;
|
|
|
|
switch (fpi_ssm_get_cur_state (ssm))
|
|
{
|
|
case CAPTURE_LED_ON:
|
|
elan_run_cmd (ssm, dev, &led_on_cmd, ELAN_CMD_TIMEOUT);
|
|
break;
|
|
|
|
case CAPTURE_WAIT_FINGER:
|
|
elan_run_cmd (ssm, dev, &pre_scan_cmd, -1);
|
|
break;
|
|
|
|
case CAPTURE_READ_DATA:
|
|
self->dev_state = FP_IMAGE_DEVICE_STATE_CAPTURE;
|
|
|
|
/* 0x55 - finger present
|
|
* 0xff - device not calibrated (probably) */
|
|
if (self->last_read && self->last_read[0] == 0x55)
|
|
{
|
|
fpi_image_device_report_finger_status (idev, TRUE);
|
|
elan_run_cmd (ssm, dev, &get_image_cmd, ELAN_CMD_TIMEOUT);
|
|
}
|
|
else
|
|
{
|
|
fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_PROTO));
|
|
}
|
|
break;
|
|
|
|
case CAPTURE_CHECK_ENOUGH_FRAMES:
|
|
r = elan_save_img_frame (self);
|
|
if (r < 0)
|
|
{
|
|
fpi_ssm_mark_failed (ssm, fpi_device_error_new (FP_DEVICE_ERROR_GENERAL));
|
|
}
|
|
else if (self->num_frames < ELAN_MAX_FRAMES)
|
|
{
|
|
/* quickly stop if finger is removed */
|
|
self->cmd_timeout = ELAN_FINGER_TIMEOUT;
|
|
fpi_ssm_jump_to_state (ssm, CAPTURE_WAIT_FINGER);
|
|
}
|
|
else
|
|
{
|
|
fpi_ssm_next_state (ssm);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
capture_complete (FpiSsm *ssm, FpDevice *_dev, GError *error)
|
|
{
|
|
FpImageDevice *dev = FP_IMAGE_DEVICE (_dev);
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (_dev);
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
/* XXX: cancellation was specially handled by doing nothing! */
|
|
|
|
/* either max frames captured or timed out waiting for the next frame */
|
|
if (!error ||
|
|
(g_error_matches (error, G_USB_DEVICE_ERROR, G_USB_DEVICE_ERROR_TIMED_OUT) &&
|
|
fpi_ssm_get_cur_state (ssm) == CAPTURE_WAIT_FINGER))
|
|
{
|
|
if (self->num_frames >= ELAN_MIN_FRAMES)
|
|
{
|
|
elan_submit_image (dev);
|
|
}
|
|
else
|
|
{
|
|
fp_dbg ("swipe too short: want >= %d frames, got %d",
|
|
ELAN_MIN_FRAMES, self->num_frames);
|
|
fpi_image_device_retry_scan (dev, FP_DEVICE_RETRY_TOO_SHORT);
|
|
}
|
|
g_clear_error (&error);
|
|
}
|
|
else
|
|
{
|
|
fpi_image_device_session_error (dev, error);
|
|
}
|
|
|
|
}
|
|
|
|
static void
|
|
elan_capture (FpDevice *dev)
|
|
{
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
elan_dev_reset_state (self);
|
|
FpiSsm *ssm =
|
|
fpi_ssm_new (dev, capture_run_state, CAPTURE_NUM_STATES);
|
|
fpi_ssm_start (ssm, capture_complete);
|
|
}
|
|
|
|
/* this function needs to have elandev->background and elandev->last_read to be
|
|
* the calibration mean */
|
|
static int
|
|
elan_need_calibration (FpiDeviceElan *elandev)
|
|
{
|
|
G_DEBUG_HERE ();
|
|
|
|
unsigned short calib_mean =
|
|
elandev->last_read[0] * 0xff + elandev->last_read[1];
|
|
unsigned int bg_mean = 0, delta;
|
|
unsigned int frame_size = elandev->frame_width * elandev->frame_height;
|
|
|
|
g_assert (frame_size != 0);
|
|
|
|
if (elandev->dev_type == ELAN_0C42)
|
|
{
|
|
if (calib_mean > 5500 ||
|
|
calib_mean < 2500)
|
|
{
|
|
fp_dbg ("Forcing needed recalibration");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < frame_size; i++)
|
|
bg_mean += elandev->background[i];
|
|
bg_mean /= frame_size;
|
|
|
|
delta =
|
|
bg_mean > calib_mean ? bg_mean - calib_mean : calib_mean - bg_mean;
|
|
|
|
fp_dbg ("calibration mean: %d, bg mean: %d, delta: %d", calib_mean,
|
|
bg_mean, delta);
|
|
|
|
return delta > ELAN_CALIBRATION_MAX_DELTA ? 1 : 0;
|
|
}
|
|
|
|
enum calibrate_states {
|
|
CALIBRATE_GET_BACKGROUND,
|
|
CALIBRATE_SAVE_BACKGROUND,
|
|
CALIBRATE_GET_MEAN,
|
|
CALIBRATE_CHECK_NEEDED,
|
|
CALIBRATE_GET_STATUS,
|
|
CALIBRATE_CHECK_STATUS,
|
|
CALIBRATE_REPEAT_STATUS,
|
|
CALIBRATE_NUM_STATES,
|
|
};
|
|
|
|
static gboolean
|
|
elan_supports_calibration (FpiDeviceElan *elandev)
|
|
{
|
|
if (elandev->dev_type == ELAN_0C42)
|
|
return TRUE;
|
|
|
|
return elandev->fw_ver >= ELAN_MIN_CALIBRATION_FW;
|
|
}
|
|
|
|
static void
|
|
calibrate_run_state (FpiSsm *ssm, FpDevice *dev)
|
|
{
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
switch (fpi_ssm_get_cur_state (ssm))
|
|
{
|
|
case CALIBRATE_GET_BACKGROUND:
|
|
elan_run_cmd (ssm, dev, &get_image_cmd, ELAN_CMD_TIMEOUT);
|
|
break;
|
|
|
|
case CALIBRATE_SAVE_BACKGROUND:
|
|
elan_save_background (self);
|
|
if (!elan_supports_calibration (self))
|
|
{
|
|
fp_dbg ("FW does not support calibration");
|
|
fpi_ssm_mark_completed (ssm);
|
|
}
|
|
else
|
|
{
|
|
fpi_ssm_next_state (ssm);
|
|
}
|
|
break;
|
|
|
|
case CALIBRATE_GET_MEAN:
|
|
elan_run_cmd (ssm, dev, &get_calib_mean_cmd, ELAN_CMD_TIMEOUT);
|
|
break;
|
|
|
|
case CALIBRATE_CHECK_NEEDED:
|
|
if (elan_need_calibration (self))
|
|
{
|
|
self->calib_status = 0;
|
|
fpi_ssm_next_state (ssm);
|
|
}
|
|
else
|
|
{
|
|
fpi_ssm_mark_completed (ssm);
|
|
}
|
|
break;
|
|
|
|
case CALIBRATE_GET_STATUS:
|
|
self->calib_atts_left -= 1;
|
|
if (self->calib_atts_left)
|
|
{
|
|
elan_run_cmd (ssm, dev, &get_calib_status_cmd,
|
|
ELAN_CMD_TIMEOUT);
|
|
}
|
|
else
|
|
{
|
|
fp_dbg ("calibration failed");
|
|
fpi_ssm_mark_failed (ssm,
|
|
fpi_device_error_new_msg (FP_DEVICE_ERROR_GENERAL,
|
|
"Callibration failed!"));
|
|
}
|
|
break;
|
|
|
|
case CALIBRATE_CHECK_STATUS:
|
|
/* 0x01 - retry, 0x03 - ok
|
|
* It appears that when reading the response soon after 0x4023 the device
|
|
* can return 0x03, and only after some time (up to 100 ms) the response
|
|
* changes to 0x01. It stays that way for some time and then changes back
|
|
* to 0x03. Because of this we don't just expect 0x03, we want to see 0x01
|
|
* first. This is to make sure that a full calibration loop has completed */
|
|
fp_dbg ("calibration status: 0x%02x", self->last_read[0]);
|
|
if (self->calib_status == 0x01 &&
|
|
self->last_read[0] == 0x03)
|
|
{
|
|
self->calib_status = 0x03;
|
|
fpi_ssm_jump_to_state (ssm, CALIBRATE_GET_BACKGROUND);
|
|
}
|
|
else
|
|
{
|
|
if (self->calib_status == 0x00 &&
|
|
self->last_read[0] == 0x01)
|
|
self->calib_status = 0x01;
|
|
fpi_ssm_next_state_delayed (ssm, 50, NULL);
|
|
}
|
|
break;
|
|
|
|
case CALIBRATE_REPEAT_STATUS:
|
|
fpi_ssm_jump_to_state (ssm, CALIBRATE_GET_STATUS);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
calibrate_complete (FpiSsm *ssm, FpDevice *dev, GError *error)
|
|
{
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
if (error)
|
|
{
|
|
self->dev_state = FP_IMAGE_DEVICE_STATE_INACTIVE;
|
|
fpi_image_device_session_error (FP_IMAGE_DEVICE (dev), error);
|
|
}
|
|
else
|
|
{
|
|
elan_capture (dev);
|
|
}
|
|
|
|
}
|
|
|
|
static void
|
|
elan_calibrate (FpDevice *dev)
|
|
{
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
elan_dev_reset_state (self);
|
|
self->calib_atts_left = ELAN_CALIBRATION_ATTEMPTS;
|
|
|
|
FpiSsm *ssm = fpi_ssm_new (FP_DEVICE (dev), calibrate_run_state,
|
|
CALIBRATE_NUM_STATES);
|
|
fpi_ssm_start (ssm, calibrate_complete);
|
|
}
|
|
|
|
enum activate_states {
|
|
ACTIVATE_GET_FW_VER,
|
|
ACTIVATE_SET_FW_VER,
|
|
ACTIVATE_GET_SENSOR_DIM,
|
|
ACTIVATE_SET_SENSOR_DIM,
|
|
ACTIVATE_CMD_1,
|
|
ACTIVATE_NUM_STATES,
|
|
};
|
|
|
|
static void
|
|
activate_run_state (FpiSsm *ssm, FpDevice *dev)
|
|
{
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
switch (fpi_ssm_get_cur_state (ssm))
|
|
{
|
|
case ACTIVATE_GET_FW_VER:
|
|
elan_run_cmd (ssm, dev, &get_fw_ver_cmd, ELAN_CMD_TIMEOUT);
|
|
break;
|
|
|
|
case ACTIVATE_SET_FW_VER:
|
|
self->fw_ver =
|
|
(self->last_read[0] << 8 | self->last_read[1]);
|
|
fp_dbg ("FW ver 0x%04hx", self->fw_ver);
|
|
fpi_ssm_next_state (ssm);
|
|
break;
|
|
|
|
case ACTIVATE_GET_SENSOR_DIM:
|
|
elan_run_cmd (ssm, dev, &get_sensor_dim_cmd, ELAN_CMD_TIMEOUT);
|
|
break;
|
|
|
|
case ACTIVATE_SET_SENSOR_DIM:
|
|
/* see elan_save_frame for details */
|
|
if (self->dev_type & ELAN_NOT_ROTATED)
|
|
{
|
|
self->frame_width = self->last_read[0];
|
|
self->frame_height = self->raw_frame_height =
|
|
self->last_read[2];
|
|
}
|
|
else
|
|
{
|
|
self->frame_width = self->last_read[2];
|
|
self->frame_height = self->raw_frame_height =
|
|
self->last_read[0];
|
|
}
|
|
/* Work-around sensors returning the sizes as zero-based index
|
|
* rather than the number of pixels. */
|
|
if ((self->frame_width % 2 == 1) &&
|
|
(self->frame_height % 2 == 1))
|
|
{
|
|
self->frame_width++;
|
|
self->frame_height++;
|
|
self->raw_frame_height = self->frame_height;
|
|
}
|
|
if (self->frame_height > ELAN_MAX_FRAME_HEIGHT)
|
|
self->frame_height = ELAN_MAX_FRAME_HEIGHT;
|
|
fp_dbg ("sensor dimensions, WxH: %dx%d", self->frame_width,
|
|
self->raw_frame_height);
|
|
fpi_ssm_next_state (ssm);
|
|
break;
|
|
|
|
case ACTIVATE_CMD_1:
|
|
/* TODO: find out what this does, if we need it */
|
|
elan_run_cmd (ssm, dev, &activate_cmd_1, ELAN_CMD_TIMEOUT);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
activate_complete (FpiSsm *ssm, FpDevice *dev, GError *error)
|
|
{
|
|
FpImageDevice *idev = FP_IMAGE_DEVICE (dev);
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
fpi_image_device_activate_complete (idev, error);
|
|
|
|
}
|
|
|
|
static void
|
|
elan_activate (FpImageDevice *dev)
|
|
{
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
|
|
G_DEBUG_HERE ();
|
|
elan_dev_reset_state (self);
|
|
|
|
FpiSsm *ssm =
|
|
fpi_ssm_new (FP_DEVICE (dev), activate_run_state,
|
|
ACTIVATE_NUM_STATES);
|
|
fpi_ssm_start (ssm, activate_complete);
|
|
}
|
|
|
|
static void
|
|
dev_init (FpImageDevice *dev)
|
|
{
|
|
GError *error = NULL;
|
|
FpiDeviceElan *self;
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
if (!g_usb_device_claim_interface (fpi_device_get_usb_device (FP_DEVICE (dev)), 0, 0, &error))
|
|
{
|
|
fpi_image_device_open_complete (dev, error);
|
|
return;
|
|
}
|
|
|
|
self = FPI_DEVICE_ELAN (dev);
|
|
|
|
/* common params */
|
|
self->dev_type = fpi_device_get_driver_data (FP_DEVICE (dev));
|
|
self->background = NULL;
|
|
self->process_frame = elan_process_frame_thirds;
|
|
|
|
switch (self->dev_type)
|
|
{
|
|
case ELAN_0907:
|
|
self->process_frame = elan_process_frame_linear;
|
|
break;
|
|
}
|
|
|
|
fpi_image_device_open_complete (dev, NULL);
|
|
}
|
|
|
|
static void
|
|
dev_deinit (FpImageDevice *dev)
|
|
{
|
|
GError *error = NULL;
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
elan_dev_reset_state (self);
|
|
g_free (self->background);
|
|
g_usb_device_release_interface (fpi_device_get_usb_device (FP_DEVICE (dev)),
|
|
0, 0, &error);
|
|
fpi_image_device_close_complete (dev, error);
|
|
}
|
|
|
|
static void
|
|
dev_activate (FpImageDevice *dev)
|
|
{
|
|
G_DEBUG_HERE ();
|
|
elan_activate (dev);
|
|
}
|
|
|
|
static void
|
|
elan_change_state (FpImageDevice *idev)
|
|
{
|
|
FpDevice *dev = FP_DEVICE (idev);
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
FpImageDeviceState next_state = self->dev_state_next;
|
|
|
|
if (self->dev_state == next_state)
|
|
{
|
|
fp_dbg ("already in %d", next_state);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
fp_dbg ("changing to %d", next_state);
|
|
}
|
|
|
|
switch (next_state)
|
|
{
|
|
case FP_IMAGE_DEVICE_STATE_AWAIT_FINGER_ON:
|
|
/* activation completed or another enroll stage started */
|
|
self->dev_state = FP_IMAGE_DEVICE_STATE_AWAIT_FINGER_ON;
|
|
elan_calibrate (dev);
|
|
break;
|
|
|
|
case FP_IMAGE_DEVICE_STATE_CAPTURE:
|
|
/* not used */
|
|
break;
|
|
|
|
case FP_IMAGE_DEVICE_STATE_INACTIVE:
|
|
case FP_IMAGE_DEVICE_STATE_AWAIT_FINGER_OFF:
|
|
elan_stop_capture (dev);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
elan_change_state_async (FpDevice *dev,
|
|
void *data)
|
|
{
|
|
g_message ("state change dev: %p", dev);
|
|
elan_change_state (FP_IMAGE_DEVICE (dev));
|
|
}
|
|
|
|
static void
|
|
dev_change_state (FpImageDevice *dev, FpImageDeviceState state)
|
|
{
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
GSource *timeout;
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
/* Inactive and await finger off are equivalent for the elan driver. */
|
|
if (state == FP_IMAGE_DEVICE_STATE_AWAIT_FINGER_OFF)
|
|
state = FP_IMAGE_DEVICE_STATE_INACTIVE;
|
|
|
|
if (self->dev_state_next == state)
|
|
fp_dbg ("change to state %d already queued", state);
|
|
|
|
switch (state)
|
|
{
|
|
case FP_IMAGE_DEVICE_STATE_INACTIVE:
|
|
case FP_IMAGE_DEVICE_STATE_AWAIT_FINGER_ON:
|
|
case FP_IMAGE_DEVICE_STATE_AWAIT_FINGER_OFF: {
|
|
char *name;
|
|
|
|
/* schedule state change instead of calling it directly to allow all actions
|
|
* related to the previous state to complete */
|
|
self->dev_state_next = state;
|
|
timeout = fpi_device_add_timeout (FP_DEVICE (dev), 10,
|
|
elan_change_state_async,
|
|
NULL, NULL);
|
|
|
|
name = g_strdup_printf ("dev_change_state to %d", state);
|
|
g_source_set_name (timeout, name);
|
|
g_free (name);
|
|
|
|
break;
|
|
}
|
|
|
|
case FP_IMAGE_DEVICE_STATE_CAPTURE:
|
|
/* TODO MAYBE: split capture ssm into smaller ssms and use this state */
|
|
self->dev_state = state;
|
|
self->dev_state_next = state;
|
|
break;
|
|
|
|
default:
|
|
g_assert_not_reached ();
|
|
}
|
|
}
|
|
|
|
static void
|
|
dev_deactivate (FpImageDevice *dev)
|
|
{
|
|
FpiDeviceElan *self = FPI_DEVICE_ELAN (dev);
|
|
|
|
G_DEBUG_HERE ();
|
|
|
|
if (self->dev_state == FP_IMAGE_DEVICE_STATE_INACTIVE)
|
|
{
|
|
/* The device is inactive already, complete the operation immediately. */
|
|
fpi_image_device_deactivate_complete (dev, NULL);
|
|
}
|
|
else
|
|
{
|
|
/* The device is not yet inactive, flag that we are deactivating (and
|
|
* need to signal back deactivation) and then ensure we will change
|
|
* to the inactive state eventually. */
|
|
self->deactivating = TRUE;
|
|
dev_change_state (dev, FP_IMAGE_DEVICE_STATE_INACTIVE);
|
|
}
|
|
}
|
|
|
|
static void
|
|
fpi_device_elan_init (FpiDeviceElan *self)
|
|
{
|
|
}
|
|
static void
|
|
fpi_device_elan_class_init (FpiDeviceElanClass *klass)
|
|
{
|
|
FpDeviceClass *dev_class = FP_DEVICE_CLASS (klass);
|
|
FpImageDeviceClass *img_class = FP_IMAGE_DEVICE_CLASS (klass);
|
|
|
|
dev_class->id = "elan";
|
|
dev_class->full_name = "ElanTech Fingerprint Sensor";
|
|
dev_class->type = FP_DEVICE_TYPE_USB;
|
|
dev_class->id_table = elan_id_table;
|
|
dev_class->scan_type = FP_SCAN_TYPE_SWIPE;
|
|
|
|
img_class->img_open = dev_init;
|
|
img_class->img_close = dev_deinit;
|
|
img_class->activate = dev_activate;
|
|
img_class->deactivate = dev_deactivate;
|
|
img_class->change_state = dev_change_state;
|
|
|
|
img_class->bz3_threshold = 24;
|
|
}
|