fwupd/plugins/fpc/fu-fpc-device.c

584 lines
16 KiB
C

/*
* Copyright (C) 2022 Haowei Lo <haowei.lo@fingerprints.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <fwupdplugin.h>
#include "fu-fpc-device.h"
#define FPC_USB_INTERFACE 0
#define FPC_USB_TRANSFER_TIMEOUT 1500 /* ms */
#define FPC_FLASH_BLOCK_SIZE_DEFAULT 2048 /* 2048 */
#define FPC_FLASH_BLOCK_SIZE_4096 4096 /* 4096 */
#define FPC_CMD_DFU_DETACH 0x00
#define FPC_CMD_DFU_DNLOAD 0x01
#define FPC_CMD_DFU_GETSTATUS 0x03
#define FPC_CMD_DFU_CLRSTATUS 0x04
#define FPC_CMD_DFU_GET_FW_STATUS 0x09
#define FPC_CMD_BOOT0 0x04
#define FPC_CMD_GET_STATE 0x0B
#define FPC_CMD_GET_STATE_LENFY 0x50
#define FPC_DEVICE_MOC_STATE_LEN 68
#define FPC_DEVICE_MOH_STATE_LEN 72
#define FPC_DEVICE_DFU_FW_STATUS_LEN 8
#define FPC_DFU_MAX_ATTEMPTS 50
#define FPC_DEVICE_DFU_MODE_CLASS 0xFE
#define FPC_DEVICE_DFU_MODE_PORT 0x02
#define FPC_DEVICE_NORMAL_MODE_CLASS 0xFF
#define FPC_DEVICE_NORMAL_MODE_PORT 0xFF
#define FPC_DFU_DNBUSY 0x04
/**
* FU_FPC_DEVICE_FLAG_MOH_DEVICE:
*
* Device is a moh device
*/
#define FU_FPC_DEVICE_FLAG_MOH_DEVICE (1 << 0)
/**
* FU_FPC_DEVICE_FLAG_LEGACY_DFU:
*
* Device supports legacy dfu mode
*/
#define FU_FPC_DEVICE_FLAG_LEGACY_DFU (1 << 1)
/**
* FU_FPC_DEVICE_FLAG_RTS_DEVICE:
*
* Device is a RTS device
*/
#define FU_FPC_DEVICE_FLAG_RTS_DEVICE (1 << 2)
/**
* FU_FPC_DEVICE_FLAG_LENFY_DEVICE:
*
* Device is a LENFY MOH device
*/
#define FU_FPC_DEVICE_FLAG_LENFY_DEVICE (1 << 3)
struct _FuFpcDevice {
FuUsbDevice parent_instance;
guint32 max_block_size;
};
G_DEFINE_TYPE(FuFpcDevice, fu_fpc_device, FU_TYPE_USB_DEVICE)
typedef struct __attribute__((packed)) {
guint8 bstatus;
guint8 bmax_payload_size;
guint8 reserved[2];
guint8 bstate;
guint8 reserved2;
} FuFpcDfuStatus;
static gboolean
fu_fpc_device_dfu_cmd(FuFpcDevice *self,
guint8 request,
guint16 value,
guint8 *data,
gsize length,
gboolean device2host,
gboolean type_vendor,
GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self));
gsize actual_len = 0;
if (data == NULL && length > 0) {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "Invalid input data");
return FALSE;
}
if (!g_usb_device_control_transfer(usb_device,
device2host ? G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST
: G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
type_vendor ? G_USB_DEVICE_REQUEST_TYPE_VENDOR
: G_USB_DEVICE_REQUEST_TYPE_CLASS,
G_USB_DEVICE_RECIPIENT_INTERFACE,
request,
value,
0x0000,
data,
length,
length ? &actual_len : NULL,
FPC_USB_TRANSFER_TIMEOUT,
NULL,
error))
return FALSE;
if (actual_len != length) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"only sent 0x%04x of 0x%04x",
(guint)actual_len,
(guint)length);
return FALSE;
}
return TRUE;
}
static gboolean
fu_fpc_device_fw_cmd(FuFpcDevice *self,
guint8 request,
guint8 *data,
gsize length,
gboolean device2host,
GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self));
gsize actual_len = 0;
if (data == NULL && length > 0) {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "Invalid input data");
return FALSE;
}
if (!g_usb_device_control_transfer(usb_device,
device2host ? G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST
: G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
G_USB_DEVICE_REQUEST_TYPE_VENDOR,
G_USB_DEVICE_RECIPIENT_DEVICE,
request,
0x0000,
0x0000,
data,
length,
length ? &actual_len : NULL,
FPC_USB_TRANSFER_TIMEOUT,
NULL,
error))
return FALSE;
if (actual_len != length) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"only sent 0x%04x of 0x%04x",
(guint)actual_len,
(guint)length);
return FALSE;
}
return TRUE;
}
static gboolean
fu_fpc_device_setup_mode(FuFpcDevice *self, GError **error)
{
#if G_USB_CHECK_VERSION(0, 3, 3)
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self));
g_autoptr(GPtrArray) intfs = NULL;
intfs = g_usb_device_get_interfaces(usb_device, error);
if (intfs == NULL)
return FALSE;
for (guint i = 0; i < intfs->len; i++) {
GUsbInterface *intf = g_ptr_array_index(intfs, i);
if (g_usb_interface_get_class(intf) == FPC_DEVICE_DFU_MODE_CLASS &&
g_usb_interface_get_protocol(intf) == FPC_DEVICE_DFU_MODE_PORT) {
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
return TRUE;
}
if (g_usb_interface_get_class(intf) == FPC_DEVICE_NORMAL_MODE_CLASS &&
g_usb_interface_get_protocol(intf) == FPC_DEVICE_NORMAL_MODE_PORT) {
fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
return TRUE;
}
}
g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no update interface found");
return FALSE;
#else
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"this version of GUsb is not supported");
return FALSE;
#endif
}
static gboolean
fu_fpc_device_setup_version(FuFpcDevice *self, GError **error)
{
guint32 version = 0;
gsize data_len = 0;
FuEndianType endian_type = G_LITTLE_ENDIAN;
g_autofree guint8 *data = NULL;
guint32 cmd_id = FPC_CMD_GET_STATE;
if (fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_RTS_DEVICE))
endian_type = G_BIG_ENDIAN;
if (!fu_device_has_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
if (fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_MOH_DEVICE)) {
data_len = FPC_DEVICE_MOH_STATE_LEN;
} else {
data_len = FPC_DEVICE_MOC_STATE_LEN;
}
data = g_malloc0(data_len);
if (fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_LENFY_DEVICE))
cmd_id = FPC_CMD_GET_STATE_LENFY;
if (!fu_fpc_device_fw_cmd(self, cmd_id, data, data_len, TRUE, error))
return FALSE;
if (!fu_memread_uint32_safe(data, data_len, 0, &version, endian_type, error))
return FALSE;
} else {
if (!fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_LEGACY_DFU)) {
if (!fu_fpc_device_dfu_cmd(self,
FPC_CMD_DFU_CLRSTATUS,
0x0000,
NULL,
0,
FALSE,
FALSE,
error))
return FALSE;
}
data = g_malloc0(FPC_DEVICE_DFU_FW_STATUS_LEN);
if (!fu_fpc_device_dfu_cmd(self,
FPC_CMD_DFU_GET_FW_STATUS,
0x0000,
data,
FPC_DEVICE_DFU_FW_STATUS_LEN,
TRUE,
TRUE,
error))
return FALSE;
if (!fu_memread_uint32_safe(data,
FPC_DEVICE_DFU_FW_STATUS_LEN,
4,
&version,
endian_type,
error))
return FALSE;
}
/* set display version */
fu_device_set_version_from_uint32(FU_DEVICE(self), version);
return TRUE;
}
static gboolean
fu_fpc_device_check_dfu_status(FuDevice *device, gpointer user_data, GError **error)
{
FuFpcDevice *self = FU_FPC_DEVICE(device);
FuFpcDfuStatus *dfu_status = (FuFpcDfuStatus *)user_data;
if (!fu_fpc_device_dfu_cmd(self,
FPC_CMD_DFU_GETSTATUS,
0x0000,
(guint8 *)dfu_status,
sizeof(FuFpcDfuStatus),
TRUE,
FALSE,
error)) {
g_prefix_error(error, "failed to get status: ");
return FALSE;
}
if (dfu_status->bstatus != 0 || dfu_status->bstate == FPC_DFU_DNBUSY) {
/* device is not in correct status/state */
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_WRITE,
"dfu status error [0x%x, 0x%x]",
dfu_status->bstatus,
dfu_status->bstate);
return FALSE;
}
return TRUE;
}
static gboolean
fu_fpc_device_update_init(FuFpcDevice *self, GError **error)
{
FuFpcDfuStatus dfu_status = {0};
if (!fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_LEGACY_DFU)) {
if (!fu_fpc_device_dfu_cmd(self,
FPC_CMD_DFU_CLRSTATUS,
0x0000,
NULL,
0,
FALSE,
FALSE,
error)) {
g_prefix_error(error, "failed to clear status: ");
return FALSE;
}
}
if (!fu_device_retry_full(FU_DEVICE(self),
fu_fpc_device_check_dfu_status,
FPC_DFU_MAX_ATTEMPTS,
20,
(gpointer)&dfu_status,
error))
return FALSE;
if (dfu_status.bmax_payload_size > 0 ||
fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_RTS_DEVICE))
self->max_block_size = FPC_FLASH_BLOCK_SIZE_4096;
else
self->max_block_size = FPC_FLASH_BLOCK_SIZE_DEFAULT;
return TRUE;
}
static void
fu_fpc_device_to_string(FuDevice *device, guint idt, GString *str)
{
FuFpcDevice *self = FU_FPC_DEVICE(device);
fu_string_append_kx(str, idt, "Max block size", self->max_block_size);
fu_string_append_kb(str,
idt,
"LegacyDfu",
fu_device_has_private_flag(device, FU_FPC_DEVICE_FLAG_LEGACY_DFU));
fu_string_append_kb(str,
idt,
"MocDevice",
!fu_device_has_private_flag(device, FU_FPC_DEVICE_FLAG_MOH_DEVICE));
if (fu_device_has_private_flag(device, FU_FPC_DEVICE_FLAG_MOH_DEVICE)) {
fu_string_append_kb(
str,
idt,
"RtsDevice",
fu_device_has_private_flag(device, FU_FPC_DEVICE_FLAG_RTS_DEVICE));
}
}
static gboolean
fu_fpc_device_attach(FuDevice *device, FuProgress *progress, GError **error)
{
FuFpcDevice *self = FU_FPC_DEVICE(device);
/* sanity check */
if (!fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
g_debug("already in runtime mode, skipping");
return TRUE;
}
if (!fu_fpc_device_dfu_cmd(self, FPC_CMD_DFU_DETACH, 0x0000, NULL, 0, FALSE, FALSE, error))
return FALSE;
fu_device_remove_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
return TRUE;
}
static gboolean
fu_fpc_device_detach(FuDevice *device, FuProgress *progress, GError **error)
{
FuFpcDevice *self = FU_FPC_DEVICE(device);
/* sanity check */
if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
g_debug("already in bootloader mode, skipping");
return TRUE;
}
if (!fu_fpc_device_fw_cmd(self, FPC_CMD_BOOT0, NULL, 0, FALSE, error))
return FALSE;
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
return TRUE;
}
static gboolean
fu_fpc_device_setup(FuDevice *device, GError **error)
{
FuFpcDevice *self = FU_FPC_DEVICE(device);
/* setup */
if (!FU_DEVICE_CLASS(fu_fpc_device_parent_class)->setup(device, error))
return FALSE;
if (!fu_fpc_device_setup_mode(self, error)) {
g_prefix_error(error, "failed to get device mode: ");
return FALSE;
}
/* ensure version */
if (!fu_fpc_device_setup_version(self, error)) {
g_prefix_error(error, "failed to get firmware version: ");
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_fpc_device_write_firmware(FuDevice *device,
FuFirmware *firmware,
FuProgress *progress,
FwupdInstallFlags flags,
GError **error)
{
FuFpcDevice *self = FU_FPC_DEVICE(device);
FuFpcDfuStatus dfu_status = {0};
g_autoptr(GBytes) fw = NULL;
g_autoptr(GError) error_local = NULL;
g_autoptr(GPtrArray) chunks = NULL;
/* progress */
fu_progress_set_id(progress, G_STRLOC);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1, "init");
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 95, NULL);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 5, "check");
/* get default image */
fw = fu_firmware_get_bytes(firmware, error);
if (fw == NULL)
return FALSE;
/* don't auto-boot firmware */
if (!fu_fpc_device_update_init(self, &error_local)) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_WRITE,
"failed to initial update: %s",
error_local->message);
return FALSE;
}
fu_progress_step_done(progress);
/* build packets */
chunks = fu_chunk_array_new_from_bytes(fw,
0x00,
0x00, /* page_sz */
self->max_block_size);
/* write each block */
for (guint i = 0; i < chunks->len; i++) {
FuChunk *chk = g_ptr_array_index(chunks, i);
g_autoptr(GByteArray) req = g_byte_array_new();
g_byte_array_append(req, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk));
if (!fu_fpc_device_dfu_cmd(self,
FPC_CMD_DFU_DNLOAD,
(guint16)i,
req->data,
(gsize)req->len,
FALSE,
FALSE,
&error_local)) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_WRITE,
"failed to write: %s",
error_local->message);
return FALSE;
}
if (!fu_device_retry_full(device,
fu_fpc_device_check_dfu_status,
FPC_DFU_MAX_ATTEMPTS,
20,
(gpointer)&dfu_status,
&error_local)) {
g_set_error(error,
FWUPD_ERROR,
FWUPD_ERROR_WRITE,
"failed to write: %s",
error_local->message);
return FALSE;
}
/* update progress */
fu_progress_set_percentage_full(fu_progress_get_child(progress),
(gsize)i + 1,
(gsize)chunks->len);
}
if (!fu_device_has_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_LEGACY_DFU)) {
/* exit fw download loop. send null package */
if (!fu_fpc_device_dfu_cmd(self,
FPC_CMD_DFU_DNLOAD,
0,
NULL,
0,
FALSE,
FALSE,
error)) {
g_prefix_error(error, "fail to exit dnload loop: ");
return FALSE;
}
}
fu_progress_step_done(progress);
if (!fu_device_retry_full(device,
fu_fpc_device_check_dfu_status,
FPC_DFU_MAX_ATTEMPTS,
20,
(gpointer)&dfu_status,
error))
return FALSE;
fu_progress_step_done(progress);
/* success! */
return TRUE;
}
static void
fu_fpc_device_set_progress(FuDevice *self, FuProgress *progress)
{
fu_progress_set_id(progress, G_STRLOC);
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach");
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write");
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach");
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload");
}
static void
fu_fpc_device_init(FuFpcDevice *self)
{
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_USE_RUNTIME_VERSION);
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD);
fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_QUAD);
fu_device_set_remove_delay(FU_DEVICE(self), 5000);
fu_device_add_protocol(FU_DEVICE(self), "com.fingerprints.dfupc");
fu_device_set_summary(FU_DEVICE(self), "FPC fingerprint sensor");
fu_device_set_install_duration(FU_DEVICE(self), 15);
fu_device_set_firmware_size_min(FU_DEVICE(self), 0x10000);
fu_device_set_firmware_size_max(FU_DEVICE(self), 0x64000);
fu_usb_device_add_interface(FU_USB_DEVICE(self), FPC_USB_INTERFACE);
fu_device_register_private_flag(FU_DEVICE(self),
FU_FPC_DEVICE_FLAG_MOH_DEVICE,
"moh-device");
fu_device_register_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_RTS_DEVICE, "rts");
fu_device_register_private_flag(FU_DEVICE(self),
FU_FPC_DEVICE_FLAG_LEGACY_DFU,
"legacy-dfu");
fu_device_register_private_flag(FU_DEVICE(self), FU_FPC_DEVICE_FLAG_LENFY_DEVICE, "lenfy");
}
static void
fu_fpc_device_class_init(FuFpcDeviceClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
klass_device->to_string = fu_fpc_device_to_string;
klass_device->write_firmware = fu_fpc_device_write_firmware;
klass_device->setup = fu_fpc_device_setup;
klass_device->reload = fu_fpc_device_setup;
klass_device->attach = fu_fpc_device_attach;
klass_device->detach = fu_fpc_device_detach;
klass_device->set_progress = fu_fpc_device_set_progress;
}