diff --git a/libfprint/drivers_api.h b/libfprint/drivers_api.h index aef8c9d..7b00d2a 100644 --- a/libfprint/drivers_api.h +++ b/libfprint/drivers_api.h @@ -29,4 +29,5 @@ #include "fpi-log.h" #include "fpi-print.h" #include "fpi-usb-transfer.h" +#include "fpi-spi-transfer.h" #include "fpi-ssm.h" diff --git a/libfprint/fpi-spi-transfer.c b/libfprint/fpi-spi-transfer.c new file mode 100644 index 0000000..1ed3c24 --- /dev/null +++ b/libfprint/fpi-spi-transfer.c @@ -0,0 +1,432 @@ +/* + * FPrint SPI transfer handling + * Copyright (C) 2019-2020 Benjamin Berg + * + * 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 + */ + +#include "fpi-spi-transfer.h" +#include +#include +#include + +/** + * SECTION:fpi-spi-transfer + * @title: SPI transfer helpers + * @short_description: Helpers to ease SPI transfers + * + * #FpiSpiTransfer is a structure to simplify the SPI transfer handling + * for the linux spidev device. The main goal are to ease memory management + * and provide a usable asynchronous API to libfprint drivers. + * + * Currently only transfers with a write and subsequent read are supported. + * + * Drivers should always use this API rather than calling read/write/ioctl on + * the spidev device. + * + * Setting G_MESSAGES_DEBUG and FP_DEBUG_TRANSFER will result in the message + * content to be dumped. + */ + + +G_DEFINE_BOXED_TYPE (FpiSpiTransfer, fpi_spi_transfer, fpi_spi_transfer_ref, fpi_spi_transfer_unref) + +static void +dump_buffer (guchar *buf, gssize dump_len) +{ + g_autoptr(GString) line = NULL; + + line = g_string_new (""); + /* Dump the buffer. */ + for (gssize i = 0; i < dump_len; i++) + { + g_string_append_printf (line, "%02x ", buf[i]); + if ((i + 1) % 16 == 0) + { + g_debug ("%s", line->str); + g_string_set_size (line, 0); + } + } + + if (line->len) + g_debug ("%s", line->str); +} + +static void +log_transfer (FpiSpiTransfer *transfer, gboolean submit, GError *error) +{ + if (g_getenv ("FP_DEBUG_TRANSFER")) + { + if (submit) + { + g_debug ("Transfer %p submitted, write length %zd, read length %zd", + transfer, + transfer->length_wr, + transfer->length_rd); + + if (transfer->buffer_wr) + dump_buffer (transfer->buffer_wr, transfer->length_wr); + } + else + { + g_autofree gchar *error_str = NULL; + if (error) + error_str = g_strdup_printf ("with error (%s)", error->message); + else + error_str = g_strdup ("successfully"); + + g_debug ("Transfer %p completed %s, write length %zd, read length %zd", + transfer, + error_str, + transfer->length_wr, + transfer->length_rd); + if (transfer->buffer_rd) + dump_buffer (transfer->buffer_rd, transfer->length_rd); + } + } +} + +/** + * fpi_spi_transfer_new: + * @device: The #FpDevice the transfer is for + * @spidev_fd: The file descriptor for the spidev device + * + * Creates a new #FpiSpiTransfer. + * + * Returns: (transfer full): A newly created #FpiSpiTransfer + */ +FpiSpiTransfer * +fpi_spi_transfer_new (FpDevice * device, int spidev_fd) +{ + FpiSpiTransfer *self; + + g_assert (FP_IS_DEVICE (device)); + + self = g_slice_new0 (FpiSpiTransfer); + self->ref_count = 1; + + /* Purely to enhance the debug log output. */ + self->length_wr = -1; + self->length_rd = -1; + + self->device = device; + self->spidev_fd = spidev_fd; + + return self; +} + +static void +fpi_spi_transfer_free (FpiSpiTransfer *self) +{ + g_assert (self); + g_assert_cmpint (self->ref_count, ==, 0); + + if (self->free_buffer_wr && self->buffer_wr) + self->free_buffer_wr (self->buffer_wr); + if (self->free_buffer_rd && self->buffer_rd) + self->free_buffer_rd (self->buffer_rd); + self->buffer_wr = NULL; + self->buffer_rd = NULL; + + g_slice_free (FpiSpiTransfer, self); +} + +/** + * fpi_spi_transfer_ref: + * @self: A #FpiSpiTransfer + * + * Increments the reference count of @self by one. + * + * Returns: (transfer full): @self + */ +FpiSpiTransfer * +fpi_spi_transfer_ref (FpiSpiTransfer *self) +{ + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (self->ref_count, NULL); + + g_atomic_int_inc (&self->ref_count); + + return self; +} + +/** + * fpi_spi_transfer_unref: + * @self: A #FpiSpiTransfer + * + * Decrements the reference count of @self by one, freeing the structure when + * the reference count reaches zero. + */ +void +fpi_spi_transfer_unref (FpiSpiTransfer *self) +{ + g_return_if_fail (self); + g_return_if_fail (self->ref_count); + + if (g_atomic_int_dec_and_test (&self->ref_count)) + fpi_spi_transfer_free (self); +} + +/** + * fpi_spi_transfer_write: + * @transfer: The #FpiSpiTransfer + * @length: The buffer size to allocate + * + * Prepare the write part of an SPI transfer allocating a new buffer + * internally that will be free'ed automatically. + */ +void +fpi_spi_transfer_write (FpiSpiTransfer *transfer, + gsize length) +{ + fpi_spi_transfer_write_full (transfer, + g_malloc0 (length), + length, + g_free); +} + +/** + * fpi_spi_transfer_write_full: + * @transfer: The #FpiSpiTransfer + * @buffer: The data to write. + * @length: The size of @buffer + * @free_func: (destroy buffer): Destroy notify for @buffer + * + * Prepare the write part of an SPI transfer. + */ +void +fpi_spi_transfer_write_full (FpiSpiTransfer *transfer, + guint8 *buffer, + gsize length, + GDestroyNotify free_func) +{ + g_assert (buffer != NULL); + g_return_if_fail (transfer); + + /* Write is always before read, so ensure both are NULL. */ + g_return_if_fail (transfer->buffer_wr == NULL); + g_return_if_fail (transfer->buffer_rd == NULL); + + transfer->buffer_wr = buffer; + transfer->length_wr = length; + transfer->free_buffer_wr = free_func; +} + +/** + * fpi_spi_transfer_read: + * @transfer: The #FpiSpiTransfer + * @length: The buffer size to allocate + * + * Prepare the read part of an SPI transfer allocating a new buffer + * internally that will be free'ed automatically. + */ +void +fpi_spi_transfer_read (FpiSpiTransfer *transfer, + gsize length) +{ + fpi_spi_transfer_read_full (transfer, + g_malloc0 (length), + length, + g_free); +} + +/** + * fpi_spi_transfer_read_full: + * @transfer: The #FpiSpiTransfer + * @buffer: Buffer to read data into. + * @length: The size of @buffer + * @free_func: (destroy buffer): Destroy notify for @buffer + * + * Prepare the read part of an SPI transfer. + */ +void +fpi_spi_transfer_read_full (FpiSpiTransfer *transfer, + guint8 *buffer, + gsize length, + GDestroyNotify free_func) +{ + g_assert (buffer != NULL); + g_return_if_fail (transfer); + g_return_if_fail (transfer->buffer_rd == NULL); + + transfer->buffer_rd = buffer; + transfer->length_rd = length; + transfer->free_buffer_rd = free_func; +} + +static void +transfer_finish_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GTask *task = G_TASK (res); + FpiSpiTransfer *transfer = g_task_get_task_data (task); + GError *error = NULL; + FpiSpiTransferCallback callback; + + g_task_propagate_boolean (task, &error); + + log_transfer (transfer, FALSE, error); + + callback = transfer->callback; + transfer->callback = NULL; + callback (transfer, transfer->device, transfer->user_data, error); +} + +static void +transfer_thread_func (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + FpiSpiTransfer *transfer = (FpiSpiTransfer *) task_data; + struct spi_ioc_transfer xfer[2]; + int transfers = 0; + int status; + + if (transfer->buffer_wr == NULL && transfer->buffer_rd == NULL) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + "Transfer with neither write or read!"); + return; + } + + memset (xfer, 0, sizeof (xfer)); + + if (transfer->buffer_wr) + { + xfer[transfers].tx_buf = (guint64) transfer->buffer_wr; + xfer[transfers].len = transfer->length_wr; + + transfers += 1; + } + + if (transfer->buffer_rd) + { + xfer[transfers].rx_buf = (guint64) transfer->buffer_rd; + xfer[transfers].len = transfer->length_rd; + + transfers += 1; + } + + /* This ioctl cannot be interrupted. */ + status = ioctl (transfer->spidev_fd, SPI_IOC_MESSAGE (transfers), xfer); + + if (status < 0) + { + g_task_return_new_error (task, + G_IO_ERROR, + g_io_error_from_errno (errno), + "Error invoking ioctl for SPI transfer (%d)", + errno); + } + else + { + g_task_return_boolean (task, TRUE); + } +} + +/** + * fpi_spi_transfer_submit: + * @transfer: (transfer full): The transfer to submit, must have been filled. + * @cancellable: Cancellable to use, e.g. fpi_device_get_cancellable() + * @callback: Callback on completion or error + * @user_data: Data to pass to callback + * + * Submit an SPI transfer with a specific timeout and callback functions. + * + * The underlying transfer cannot be cancelled. The current implementation + * will only call @callback after the transfer has been completed. + * + * Note that #FpiSpiTransfer will be stolen when this function is called. + * So that all associated data will be free'ed automatically, after the + * callback ran unless fpi_usb_transfer_ref() is explicitly called. + */ +void +fpi_spi_transfer_submit (FpiSpiTransfer *transfer, + GCancellable *cancellable, + FpiSpiTransferCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (transfer); + g_return_if_fail (callback); + + /* Recycling is allowed, but not two at the same time. */ + g_return_if_fail (transfer->callback == NULL); + + transfer->callback = callback; + transfer->user_data = user_data; + + log_transfer (transfer, TRUE, NULL); + + task = g_task_new (transfer->device, + cancellable, + transfer_finish_cb, + NULL); + g_task_set_task_data (task, + g_steal_pointer (&transfer), + (GDestroyNotify) fpi_spi_transfer_unref); + + g_task_run_in_thread (task, transfer_thread_func); +} + +/** + * fpi_spi_transfer_submit_sync: + * @transfer: The transfer to submit, must have been filled. + * @error: Location to store #GError to + * + * Synchronously submit an SPI transfer. Use of this function is discouraged + * as it will block all other operations in the application. + * + * Note that you still need to fpi_spi_transfer_unref() the + * #FpiSpiTransfer afterwards. + * + * Returns: #TRUE on success, otherwise #FALSE and @error will be set + */ +gboolean +fpi_spi_transfer_submit_sync (FpiSpiTransfer *transfer, + GError **error) +{ + g_autoptr(GTask) task = NULL; + GError *err = NULL; + gboolean res; + + g_return_val_if_fail (transfer, FALSE); + + /* Recycling is allowed, but not two at the same time. */ + g_return_val_if_fail (transfer->callback == NULL, FALSE); + + log_transfer (transfer, TRUE, NULL); + + task = g_task_new (transfer->device, + NULL, + NULL, + NULL); + g_task_set_task_data (task, + fpi_spi_transfer_ref (transfer), + (GDestroyNotify) fpi_spi_transfer_unref); + + g_task_run_in_thread_sync (task, transfer_thread_func); + + res = g_task_propagate_boolean (task, &err); + + log_transfer (transfer, FALSE, err); + + g_propagate_error (error, err); + + return res; +} diff --git a/libfprint/fpi-spi-transfer.h b/libfprint/fpi-spi-transfer.h new file mode 100644 index 0000000..755b405 --- /dev/null +++ b/libfprint/fpi-spi-transfer.h @@ -0,0 +1,113 @@ +/* + * FPrint spidev transfer handling + * Copyright (C) 2019-2020 Benjamin Berg + * + * 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 + +#include "fpi-compat.h" +#include "fpi-device.h" + +G_BEGIN_DECLS + +#define FPI_TYPE_SPI_TRANSFER (fpi_spi_transfer_get_type ()) + +typedef struct _FpiSpiTransfer FpiSpiTransfer; +typedef struct _FpiSsm FpiSsm; + +typedef void (*FpiSpiTransferCallback)(FpiSpiTransfer *transfer, + FpDevice *dev, + gpointer user_data, + GError *error); + +/** + * FpiSpiTransfer: + * @device: The #FpDevice that the transfer belongs to. + * @ssm: Storage slot to associate the transfer with a state machine. + * Used by fpi_ssm_spi_transfer_cb() to modify the given state machine. + * @length_wr: The length of the write buffer + * @length_rd: The length of the read buffer + * @buffer_wr: The write buffer. + * @buffer_rd: The read buffer. + * + * Helper for handling SPI transfers. Currently transfers can either be pure + * write/read transfers or a write followed by a read (full duplex support + * can easily be added if desired). + */ +struct _FpiSpiTransfer +{ + /*< public >*/ + FpDevice *device; + + FpiSsm *ssm; + + gssize length_wr; + gssize length_rd; + + guchar *buffer_wr; + guchar *buffer_rd; + + /*< private >*/ + guint ref_count; + + int spidev_fd; + + /* Callbacks */ + gpointer user_data; + FpiSpiTransferCallback callback; + + /* Data free function */ + GDestroyNotify free_buffer_wr; + GDestroyNotify free_buffer_rd; +}; + +GType fpi_spi_transfer_get_type (void) G_GNUC_CONST; +FpiSpiTransfer *fpi_spi_transfer_new (FpDevice *device, + int spidev_fd); +FpiSpiTransfer *fpi_spi_transfer_ref (FpiSpiTransfer *self); +void fpi_spi_transfer_unref (FpiSpiTransfer *self); + +void fpi_spi_transfer_write (FpiSpiTransfer *transfer, + gsize length); + +FP_GNUC_ACCESS (read_only, 2, 3) +void fpi_spi_transfer_write_full (FpiSpiTransfer *transfer, + guint8 *buffer, + gsize length, + GDestroyNotify free_func); + +void fpi_spi_transfer_read (FpiSpiTransfer *transfer, + gsize length); + +FP_GNUC_ACCESS (write_only, 2, 3) +void fpi_spi_transfer_read_full (FpiSpiTransfer *transfer, + guint8 *buffer, + gsize length, + GDestroyNotify free_func); + +void fpi_spi_transfer_submit (FpiSpiTransfer *transfer, + GCancellable *cancellable, + FpiSpiTransferCallback callback, + gpointer user_data); + +gboolean fpi_spi_transfer_submit_sync (FpiSpiTransfer *transfer, + GError **error); + + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (FpiSpiTransfer, fpi_spi_transfer_unref) + +G_END_DECLS diff --git a/libfprint/fpi-ssm.c b/libfprint/fpi-ssm.c index 71c4e94..6a0698c 100644 --- a/libfprint/fpi-ssm.c +++ b/libfprint/fpi-ssm.c @@ -737,3 +737,56 @@ fpi_ssm_usb_transfer_with_weak_pointer_cb (FpiUsbTransfer *transfer, fpi_ssm_usb_transfer_cb (transfer, device, weak_ptr, error); } + +/** + * fpi_ssm_spi_transfer_cb: + * @transfer: a #FpiSpiTransfer + * @device: a #FpDevice + * @unused_data: User data (unused) + * @error: The #GError or %NULL + * + * Can be used in as a #FpiSpiTransfer callback handler to automatically + * advance or fail a statemachine on transfer completion. + * + * Make sure to set the #FpiSsm on the transfer. + */ +void +fpi_ssm_spi_transfer_cb (FpiSpiTransfer *transfer, FpDevice *device, + gpointer unused_data, GError *error) +{ + g_return_if_fail (transfer->ssm); + + if (error) + fpi_ssm_mark_failed (transfer->ssm, error); + else + fpi_ssm_next_state (transfer->ssm); +} + +/** + * fpi_ssm_spi_transfer_with_weak_pointer_cb: + * @transfer: a #FpiSpiTransfer + * @device: a #FpDevice + * @weak_ptr: A #gpointer pointer to nullify. You can pass a pointer to any + * #gpointer to nullify when the callback is completed. I.e a + * pointer to the current #FpiSpiTransfer. + * @error: The #GError or %NULL + * + * Can be used in as a #FpiSpiTransfer callback handler to automatically + * advance or fail a statemachine on transfer completion. + * Passing a #gpointer* as @weak_ptr permits to nullify it once we're done + * with the transfer. + * + * Make sure to set the #FpiSsm on the transfer. + */ +void +fpi_ssm_spi_transfer_with_weak_pointer_cb (FpiSpiTransfer *transfer, + FpDevice *device, gpointer weak_ptr, + GError *error) +{ + g_return_if_fail (transfer->ssm); + + if (weak_ptr) + g_nullify_pointer ((gpointer *) weak_ptr); + + fpi_ssm_spi_transfer_cb (transfer, device, weak_ptr, error); +} diff --git a/libfprint/fpi-ssm.h b/libfprint/fpi-ssm.h index 2c808ee..66871de 100644 --- a/libfprint/fpi-ssm.h +++ b/libfprint/fpi-ssm.h @@ -112,4 +112,15 @@ void fpi_ssm_usb_transfer_with_weak_pointer_cb (FpiUsbTransfer *transfer, gpointer weak_ptr, GError *error); +typedef struct _FpiSpiTransfer FpiSpiTransfer; + +void fpi_ssm_spi_transfer_cb (FpiSpiTransfer *transfer, + FpDevice *device, + gpointer unused_data, + GError *error); +void fpi_ssm_spi_transfer_with_weak_pointer_cb (FpiSpiTransfer *transfer, + FpDevice *device, + gpointer weak_ptr, + GError *error); + G_DEFINE_AUTOPTR_CLEANUP_FUNC (FpiSsm, fpi_ssm_free) diff --git a/libfprint/meson.build b/libfprint/meson.build index 0a24ffb..50d738e 100644 --- a/libfprint/meson.build +++ b/libfprint/meson.build @@ -16,6 +16,7 @@ libfprint_private_sources = [ 'fpi-print.c', 'fpi-ssm.c', 'fpi-usb-transfer.c', + 'fpi-spi-transfer.c', ] libfprint_public_headers = [ @@ -40,6 +41,7 @@ libfprint_private_headers = [ 'fpi-minutiae.h', 'fpi-print.h', 'fpi-usb-transfer.h', + 'fpi-spi-transfer.h', 'fpi-ssm.h', ]