fwupd/plugins/logitech-hidpp/fu-logitech-hidpp-bootloader.c
Richard Hughes b3f9841924 Support more than one protocol for a given device
Devices may want to support more than one protocol, and for some devices
(e.g. Unifying peripherals stuck in bootloader mode) you might not even be able
to query for the correct protocol anyway.
2021-03-01 16:14:36 +00:00

478 lines
15 KiB
C

/*
* Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <string.h>
#include "fu-firmware-common.h"
#include "fu-hid-device.h"
#include "fu-logitech-hidpp-common.h"
#include "fu-logitech-hidpp-bootloader.h"
#include "fu-logitech-hidpp-hidpp.h"
typedef struct
{
guint16 flash_addr_lo;
guint16 flash_addr_hi;
guint16 flash_blocksize;
} FuLogitechHidPpBootloaderPrivate;
#define FU_UNIFYING_DEVICE_EP1 0x81
#define FU_UNIFYING_DEVICE_EP3 0x83
G_DEFINE_TYPE_WITH_PRIVATE (FuLogitechHidPpBootloader, fu_logitech_hidpp_bootloader, FU_TYPE_HID_DEVICE)
#define GET_PRIVATE(o) (fu_logitech_hidpp_bootloader_get_instance_private (o))
static void
fu_logitech_hidpp_bootloader_to_string (FuDevice *device, guint idt, GString *str)
{
FuLogitechHidPpBootloader *self = FU_UNIFYING_BOOTLOADER (device);
FuLogitechHidPpBootloaderPrivate *priv = GET_PRIVATE (self);
fu_common_string_append_kx (str, idt, "FlashAddrHigh", priv->flash_addr_hi);
fu_common_string_append_kx (str, idt, "FlashAddrLow", priv->flash_addr_lo);
fu_common_string_append_kx (str, idt, "FlashBlockSize", priv->flash_blocksize);
}
FuLogitechHidPpBootloaderRequest *
fu_logitech_hidpp_bootloader_request_new (void)
{
FuLogitechHidPpBootloaderRequest *req = g_new0 (FuLogitechHidPpBootloaderRequest, 1);
return req;
}
GPtrArray *
fu_logitech_hidpp_bootloader_parse_requests (FuLogitechHidPpBootloader *self, GBytes *fw, GError **error)
{
const gchar *tmp;
g_auto(GStrv) lines = NULL;
g_autoptr(GPtrArray) reqs = NULL;
guint32 last_addr = 0;
reqs = g_ptr_array_new_with_free_func (g_free);
tmp = g_bytes_get_data (fw, NULL);
lines = g_strsplit_set (tmp, "\n\r", -1);
for (guint i = 0; lines[i] != NULL; i++) {
g_autoptr(FuLogitechHidPpBootloaderRequest) payload = NULL;
guint8 rec_type = 0x00;
guint16 offset = 0x0000;
gboolean exit = FALSE;
gsize linesz = strlen (lines[i]);
/* skip empty lines */
tmp = lines[i];
if (linesz < 5)
continue;
payload = fu_logitech_hidpp_bootloader_request_new ();
payload->len = fu_logitech_hidpp_buffer_read_uint8 (tmp + 0x01);
if (payload->len > 28) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"firmware data invalid: too large %u bytes",
payload->len);
return NULL;
}
if (!fu_firmware_strparse_uint16_safe (tmp, linesz, 0x03,
&payload->addr, error))
return NULL;
payload->cmd = FU_UNIFYING_BOOTLOADER_CMD_WRITE_RAM_BUFFER;
rec_type = fu_logitech_hidpp_buffer_read_uint8 (tmp + 0x07);
switch (rec_type) {
case 0x00: /* data */
break;
case 0x01: /* EOF */
exit = TRUE;
break;
case 0x03: /* start segment address */
/* this is used to specify the start address,
it is doesn't matter in this context so we can
safely ignore it */
continue;
case 0x04: /* extended linear address */
if (!fu_firmware_strparse_uint16_safe (tmp, linesz,
0x09, &offset,
error))
return NULL;
if (offset != 0x0000) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"extended linear addresses with offset different from 0 are not supported");
return NULL;
}
continue;
case 0x05: /* start linear address */
/* this is used to specify the start address,
it is doesn't matter in this context so we can
safely ignore it */
continue;
case 0xFD: /* custom - vendor */
/* record type of 0xFD indicates signature data */
payload->cmd = FU_UNIFYING_BOOTLOADER_CMD_WRITE_SIGNATURE;
break;
default:
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"intel hex file record type %02x not supported",
rec_type);
return NULL;
}
if (exit)
break;
/* read the data, but skip the checksum byte */
for (guint j = 0; j < payload->len; j++) {
const gchar *ptr = tmp + 0x09 + (j * 2);
if (ptr[0] == '\0') {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"firmware data invalid: expected %u bytes",
payload->len);
return NULL;
}
payload->data[j] = fu_logitech_hidpp_buffer_read_uint8 (ptr);
}
/* no need to bound check signature addresses */
if (payload->cmd == FU_UNIFYING_BOOTLOADER_CMD_WRITE_SIGNATURE) {
g_ptr_array_add (reqs, g_steal_pointer (&payload));
continue;
}
/* skip the bootloader */
if (payload->addr > fu_logitech_hidpp_bootloader_get_addr_hi (self)) {
g_debug ("skipping write @ %04x", payload->addr);
continue;
}
/* skip the header */
if (payload->addr < fu_logitech_hidpp_bootloader_get_addr_lo (self)) {
g_debug ("skipping write @ %04x", payload->addr);
continue;
}
/* make sure firmware addresses only go up */
if (payload->addr < last_addr) {
g_debug ("skipping write @ %04x", payload->addr);
continue;
}
last_addr = payload->addr;
/* pending */
g_ptr_array_add (reqs, g_steal_pointer (&payload));
}
if (reqs->len == 0) {
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"firmware data invalid: no payloads found");
return NULL;
}
return g_steal_pointer (&reqs);
}
guint16
fu_logitech_hidpp_bootloader_get_addr_lo (FuLogitechHidPpBootloader *self)
{
FuLogitechHidPpBootloaderPrivate *priv = GET_PRIVATE (self);
g_return_val_if_fail (FU_IS_UNIFYING_BOOTLOADER (self), 0x0000);
return priv->flash_addr_lo;
}
guint16
fu_logitech_hidpp_bootloader_get_addr_hi (FuLogitechHidPpBootloader *self)
{
FuLogitechHidPpBootloaderPrivate *priv = GET_PRIVATE (self);
g_return_val_if_fail (FU_IS_UNIFYING_BOOTLOADER (self), 0x0000);
return priv->flash_addr_hi;
}
guint16
fu_logitech_hidpp_bootloader_get_blocksize (FuLogitechHidPpBootloader *self)
{
FuLogitechHidPpBootloaderPrivate *priv = GET_PRIVATE (self);
g_return_val_if_fail (FU_IS_UNIFYING_BOOTLOADER (self), 0x0000);
return priv->flash_blocksize;
}
static gboolean
fu_logitech_hidpp_bootloader_attach (FuDevice *device, GError **error)
{
FuLogitechHidPpBootloader *self = FU_UNIFYING_BOOTLOADER (device);
g_autoptr(FuLogitechHidPpBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new ();
req->cmd = FU_UNIFYING_BOOTLOADER_CMD_REBOOT;
if (!fu_logitech_hidpp_bootloader_request (self, req, error)) {
g_prefix_error (error, "failed to attach back to runtime: ");
return FALSE;
}
fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
return TRUE;
}
static gboolean
fu_logitech_hidpp_bootloader_set_bl_version (FuLogitechHidPpBootloader *self, GError **error)
{
guint16 build;
guint8 major;
guint8 minor;
g_autofree gchar *version = NULL;
g_autoptr(FuLogitechHidPpBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new ();
/* call into hardware */
req->cmd = FU_UNIFYING_BOOTLOADER_CMD_GET_BL_VERSION;
if (!fu_logitech_hidpp_bootloader_request (self, req, error)) {
g_prefix_error (error, "failed to get firmware version: ");
return FALSE;
}
/* BOTxx.yy_Bzzzz
* 012345678901234 */
build = (guint16) fu_logitech_hidpp_buffer_read_uint8 ((const gchar *) req->data + 10) << 8;
build += fu_logitech_hidpp_buffer_read_uint8 ((const gchar *) req->data + 12);
major = fu_logitech_hidpp_buffer_read_uint8 ((const gchar *) req->data + 3);
minor = fu_logitech_hidpp_buffer_read_uint8 ((const gchar *) req->data + 6);
version = fu_logitech_hidpp_format_version ("BOT", major, minor, build);
if (version == NULL) {
g_prefix_error (error, "failed to format firmware version: ");
return FALSE;
}
fu_device_set_version_bootloader (FU_DEVICE (self), version);
if ((major == 0x01 && minor >= 0x04) ||
(major == 0x03 && minor >= 0x02))
fu_device_add_protocol (FU_DEVICE (self), "com.logitech.unifyingsigned");
else
fu_device_add_protocol (FU_DEVICE (self), "com.logitech.unifying");
return TRUE;
}
static gboolean
fu_logitech_hidpp_bootloader_open (FuDevice *device, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
const guint idx = 0x00;
/* FuUsbDevice->open */
if (!FU_DEVICE_CLASS (fu_logitech_hidpp_bootloader_parent_class)->open (device, error))
return FALSE;
/* claim the only interface */
if (!g_usb_device_claim_interface (usb_device, idx,
G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
error)) {
g_prefix_error (error, "Failed to claim 0x%02x: ", idx);
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_logitech_hidpp_bootloader_setup (FuDevice *device, GError **error)
{
FuLogitechHidPpBootloaderClass *klass = FU_UNIFYING_BOOTLOADER_GET_CLASS (device);
FuLogitechHidPpBootloader *self = FU_UNIFYING_BOOTLOADER (device);
FuLogitechHidPpBootloaderPrivate *priv = GET_PRIVATE (self);
g_autoptr(FuLogitechHidPpBootloaderRequest) req = fu_logitech_hidpp_bootloader_request_new ();
/* get memory map */
req->cmd = FU_UNIFYING_BOOTLOADER_CMD_GET_MEMINFO;
if (!fu_logitech_hidpp_bootloader_request (self, req, error)) {
g_prefix_error (error, "failed to get meminfo: ");
return FALSE;
}
if (req->len != 0x06) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"failed to get meminfo: invalid size %02x",
req->len);
return FALSE;
}
/* parse values */
priv->flash_addr_lo = fu_common_read_uint16 (req->data + 0, G_BIG_ENDIAN);
priv->flash_addr_hi = fu_common_read_uint16 (req->data + 2, G_BIG_ENDIAN);
priv->flash_blocksize = fu_common_read_uint16 (req->data + 4, G_BIG_ENDIAN);
/* get bootloader version */
if (!fu_logitech_hidpp_bootloader_set_bl_version (self, error))
return FALSE;
/* subclassed further */
if (klass->setup != NULL)
return klass->setup (self, error);
/* success */
return TRUE;
}
static gboolean
fu_logitech_hidpp_bootloader_close (FuDevice *device, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (device));
if (usb_device != NULL) {
if (!g_usb_device_release_interface (usb_device, 0x00,
G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
error)) {
return FALSE;
}
}
/* FuUsbDevice->close */
return FU_DEVICE_CLASS (fu_logitech_hidpp_bootloader_parent_class)->close (device, error);
}
gboolean
fu_logitech_hidpp_bootloader_request (FuLogitechHidPpBootloader *self,
FuLogitechHidPpBootloaderRequest *req,
GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self));
gsize actual_length = 0;
guint8 buf_request[32];
guint8 buf_response[32];
/* build packet */
memset (buf_request, 0x00, sizeof (buf_request));
buf_request[0x00] = req->cmd;
buf_request[0x01] = req->addr >> 8;
buf_request[0x02] = req->addr & 0xff;
buf_request[0x03] = req->len;
if (!fu_memcpy_safe (buf_request, sizeof(buf_request), 0x04, /* dst */
req->data, sizeof(req->data), 0x0, /* src */
sizeof(req->data), error))
return FALSE;
/* send request */
if (g_getenv ("FWUPD_LOGITECH_HIDPP") != NULL) {
fu_common_dump_raw (G_LOG_DOMAIN, "host->device",
buf_request, sizeof (buf_request));
}
if (usb_device != NULL) {
if (!fu_hid_device_set_report (FU_HID_DEVICE (self), 0x0,
buf_request, sizeof(buf_request),
FU_UNIFYING_DEVICE_TIMEOUT_MS,
FU_HID_DEVICE_FLAG_NONE,
error)) {
g_prefix_error (error, "failed to send data: ");
return FALSE;
}
}
/* no response required when rebooting */
if (usb_device != NULL &&
req->cmd == FU_UNIFYING_BOOTLOADER_CMD_REBOOT) {
g_autoptr(GError) error_ignore = NULL;
if (!g_usb_device_interrupt_transfer (usb_device,
FU_UNIFYING_DEVICE_EP1,
buf_response,
sizeof (buf_response),
&actual_length,
FU_UNIFYING_DEVICE_TIMEOUT_MS,
NULL,
&error_ignore)) {
g_debug ("ignoring: %s", error_ignore->message);
} else {
if (g_getenv ("FWUPD_LOGITECH_HIDPP") != NULL) {
fu_common_dump_raw (G_LOG_DOMAIN, "device->host",
buf_response, actual_length);
}
}
return TRUE;
}
/* get response */
memset (buf_response, 0x00, sizeof (buf_response));
if (usb_device != NULL) {
if (!g_usb_device_interrupt_transfer (usb_device,
FU_UNIFYING_DEVICE_EP1,
buf_response,
sizeof (buf_response),
&actual_length,
FU_UNIFYING_DEVICE_TIMEOUT_MS,
NULL,
error)) {
g_prefix_error (error, "failed to get data: ");
return FALSE;
}
} else {
/* emulated */
buf_response[0] = buf_request[0];
if (buf_response[0] == FU_UNIFYING_BOOTLOADER_CMD_GET_MEMINFO) {
buf_response[3] = 0x06; /* len */
buf_response[4] = 0x40; /* lo MSB */
buf_response[5] = 0x00; /* lo LSB */
buf_response[6] = 0x6b; /* hi MSB */
buf_response[7] = 0xff; /* hi LSB */
buf_response[8] = 0x00; /* bs MSB */
buf_response[9] = 0x80; /* bs LSB */
}
actual_length = sizeof (buf_response);
}
if (g_getenv ("FWUPD_LOGITECH_HIDPP") != NULL) {
fu_common_dump_raw (G_LOG_DOMAIN, "device->host",
buf_response, actual_length);
}
/* parse response */
if ((buf_response[0x00] & 0xf0) != req->cmd) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"invalid command response of %02x, expected %02x",
buf_response[0x00], req->cmd);
return FALSE;
}
req->cmd = buf_response[0x00];
req->addr = ((guint16) buf_response[0x01] << 8) + buf_response[0x02];
req->len = buf_response[0x03];
if (req->len > 28) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"invalid data size of %02x", req->len);
return FALSE;
}
memset (req->data, 0x00, 28);
if (req->len > 0)
memcpy (req->data, buf_response + 0x04, req->len);
return TRUE;
}
static void
fu_logitech_hidpp_bootloader_init (FuLogitechHidPpBootloader *self)
{
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_UPDATABLE);
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
fu_device_add_internal_flag (FU_DEVICE (self), FU_DEVICE_INTERNAL_FLAG_REPLUG_MATCH_GUID);
fu_device_add_icon (FU_DEVICE (self), "preferences-desktop-keyboard");
fu_device_set_version_format (FU_DEVICE (self), FWUPD_VERSION_FORMAT_PLAIN);
fu_device_set_name (FU_DEVICE (self), "Unifying Receiver");
fu_device_set_summary (FU_DEVICE (self), "A miniaturised USB wireless receiver (bootloader)");
fu_device_set_remove_delay (FU_DEVICE (self), FU_UNIFYING_DEVICE_TIMEOUT_MS);
}
static void
fu_logitech_hidpp_bootloader_class_init (FuLogitechHidPpBootloaderClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
klass_device->to_string = fu_logitech_hidpp_bootloader_to_string;
klass_device->attach = fu_logitech_hidpp_bootloader_attach;
klass_device->setup = fu_logitech_hidpp_bootloader_setup;
klass_device->open = fu_logitech_hidpp_bootloader_open;
klass_device->close = fu_logitech_hidpp_bootloader_close;
}