fwupd/plugins/csr/fu-csr-device.c
Richard Hughes d9e6cca414 csr: Add a new plugin to add support for CSR "Driverless DFU"
CSR is short for Cambridge Silicon Radio, which is a the OEM that makes most
of the bluetooth audio chips in vendor hardware. The hardware vendor can enable
or disable features on the CSR microcontroller depending on licensing options.

The hardware vendor can also use a custom USB descriptor, or just set a custom
PID. In the latter case we need to set the vendor and model to reality using
quirks.

This commit allows the user to update the firmware in the AIAIAI H05 wireless
headphones.
2017-12-11 10:47:18 +00:00

611 lines
17 KiB
C

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
*
* Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
*
* Licensed under the GNU Lesser General Public License Version 2.1
*
* 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 "config.h"
#include <string.h>
#include "fu-csr-device.h"
#include "dfu-common.h"
#include "dfu-chunked.h"
#include "dfu-firmware.h"
struct _FuCsrDevice
{
FuUsbDevice parent_instance;
FuCsrDeviceQuirks quirks;
DfuState dfu_state;
guint32 dnload_timeout;
};
G_DEFINE_TYPE (FuCsrDevice, fu_csr_device, FU_TYPE_USB_DEVICE)
#define HID_REPORT_GET 0x01
#define HID_REPORT_SET 0x09
#define HID_REPORT_TYPE_INPUT 0x01
#define HID_REPORT_TYPE_OUTPUT 0x02
#define HID_REPORT_TYPE_FEATURE 0x03
#define HID_FEATURE 0x0300
#define FU_CSR_REPORT_ID_COMMAND 0x01
#define FU_CSR_REPORT_ID_STATUS 0x02
#define FU_CSR_REPORT_ID_CONTROL 0x03
#define FU_CSR_COMMAND_HEADER_SIZE 6 /* bytes */
#define FU_CSR_COMMAND_UPGRADE 0x01
#define FU_CSR_STATUS_HEADER_SIZE 7
#define FU_CSR_CONTROL_HEADER_SIZE 2 /* bytes */
#define FU_CSR_CONTROL_CLEAR_STATUS 0x04
#define FU_CSR_CONTROL_RESET 0xff
/* maximum firmware packet, including the command header */
#define FU_CSR_PACKET_DATA_SIZE 1023 /* bytes */
#define FU_CSR_DEVICE_TIMEOUT 5000 /* ms */
static void
fu_csr_device_to_string (FuDevice *device, GString *str)
{
FuCsrDevice *self = FU_CSR_DEVICE (device);
g_string_append (str, " DfuCsrDevice:\n");
g_string_append_printf (str, " state:\t\t%s\n", dfu_state_to_string (self->dfu_state));
g_string_append_printf (str, " timeout:\t\t%" G_GUINT32_FORMAT "\n", self->dnload_timeout);
}
static void
fu_csr_device_dump (const gchar *title, const guint8 *buf, gsize sz)
{
if (g_getenv ("FWUPD_CSR_VERBOSE") == NULL)
return;
g_print ("%s (%" G_GSIZE_FORMAT "):\n", title, sz);
for (gsize i = 0; i < sz; i++)
g_print ("%02x ", buf[i]);
g_print ("\n");
}
void
fu_csr_device_set_quirks (FuCsrDevice *self, FuCsrDeviceQuirks quirks)
{
self->quirks = quirks;
}
gboolean
fu_csr_device_attach (FuCsrDevice *self, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self));
gsize sz = 0;
guint8 buf[] = { FU_CSR_REPORT_ID_CONTROL, FU_CSR_CONTROL_RESET };
fu_csr_device_dump ("Reset", buf, sz);
if (!g_usb_device_control_transfer (usb_device,
G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
G_USB_DEVICE_REQUEST_TYPE_CLASS,
G_USB_DEVICE_RECIPIENT_INTERFACE,
HID_REPORT_SET, /* bRequest */
HID_FEATURE | FU_CSR_REPORT_ID_CONTROL, /* wValue */
0x0000, /* wIndex */
buf, sizeof(buf), &sz,
FU_CSR_DEVICE_TIMEOUT, /* timeout */
NULL, error)) {
g_prefix_error (error, "Failed to ClearStatus: ");
return FALSE;
}
/* check packet */
if (sz != FU_CSR_CONTROL_HEADER_SIZE) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"Reset packet was %" G_GSIZE_FORMAT " expected %i",
sz, FU_CSR_CONTROL_HEADER_SIZE);
return FALSE;
}
return TRUE;
}
static gboolean
fu_csr_device_get_status (FuCsrDevice *self, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self));
gsize sz = 0;
guint8 buf[64] = {0};
/* hit hardware */
if (!g_usb_device_control_transfer (usb_device,
G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST,
G_USB_DEVICE_REQUEST_TYPE_CLASS,
G_USB_DEVICE_RECIPIENT_INTERFACE,
HID_REPORT_GET, /* bRequest */
HID_FEATURE | FU_CSR_REPORT_ID_STATUS, /* wValue */
0x0000, /* wIndex */
buf, sizeof(buf), &sz,
FU_CSR_DEVICE_TIMEOUT,
NULL, error)) {
g_prefix_error (error, "Failed to GetStatus: ");
return FALSE;
}
fu_csr_device_dump ("GetStatus", buf, sz);
/* check packet */
if (sz != FU_CSR_STATUS_HEADER_SIZE) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"GetStatus packet was %" G_GSIZE_FORMAT " expected %i",
sz, FU_CSR_STATUS_HEADER_SIZE);
return FALSE;
}
if (buf[0] != FU_CSR_REPORT_ID_STATUS) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"GetStatus packet-id was %i expected %i",
buf[0], FU_CSR_REPORT_ID_STATUS);
return FALSE;
}
self->dfu_state = buf[5];
self->dnload_timeout = buf[2] + (((guint32) buf[3]) << 8) + (((guint32) buf[4]) << 16);
g_debug ("timeout=%" G_GUINT32_FORMAT, self->dnload_timeout);
g_debug ("state=%s", dfu_state_to_string (self->dfu_state));
g_debug ("status=%s", dfu_status_to_string (buf[6]));
return TRUE;
}
static gboolean
fu_csr_device_clear_status (FuCsrDevice *self, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self));
gsize sz = 0;
guint8 buf[] = { FU_CSR_REPORT_ID_CONTROL,
FU_CSR_CONTROL_CLEAR_STATUS };
/* only clear the status if the state is error */
if (!fu_csr_device_get_status (self, error))
return FALSE;
if (self->dfu_state != DFU_STATE_DFU_ERROR)
return TRUE;
/* hit hardware */
fu_csr_device_dump ("ClearStatus", buf, sz);
if (!g_usb_device_control_transfer (usb_device,
G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
G_USB_DEVICE_REQUEST_TYPE_CLASS,
G_USB_DEVICE_RECIPIENT_INTERFACE,
HID_REPORT_SET, /* bRequest */
HID_FEATURE | FU_CSR_REPORT_ID_CONTROL, /* wValue */
0x0000, /* wIndex */
buf, sizeof(buf), &sz,
FU_CSR_DEVICE_TIMEOUT,
NULL, error)) {
g_prefix_error (error, "Failed to ClearStatus: ");
return FALSE;
}
/* check packet */
if (sz != FU_CSR_CONTROL_HEADER_SIZE) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"ClearStatus packet was %" G_GSIZE_FORMAT " expected %i",
sz, FU_CSR_CONTROL_HEADER_SIZE);
return FALSE;
}
/* check the hardware again */
return fu_csr_device_get_status (self, error);
}
static GBytes *
fu_csr_device_upload_chunk (FuCsrDevice *self, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self));
gsize sz = 0;
guint16 data_sz;
guint8 buf[64] = {0};
/* hit hardware */
if (!g_usb_device_control_transfer (usb_device,
G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST,
G_USB_DEVICE_REQUEST_TYPE_CLASS,
G_USB_DEVICE_RECIPIENT_INTERFACE,
HID_REPORT_GET, /* bRequest */
HID_FEATURE | FU_CSR_REPORT_ID_COMMAND, /* wValue */
0x0000, /* wIndex */
buf, sizeof(buf), &sz,
FU_CSR_DEVICE_TIMEOUT,
NULL, error)) {
g_prefix_error (error, "Failed to ReadFirmware: ");
return NULL;
}
fu_csr_device_dump ("ReadFirmware", buf, sz);
/* too small to parse */
if (sz < FU_CSR_COMMAND_HEADER_SIZE) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"ReadFirmware packet too small, got %" G_GSIZE_FORMAT,
sz);
return NULL;
}
/* check command byte */
if (buf[0] != FU_CSR_REPORT_ID_COMMAND) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"wrong report ID %u", buf[0]);
return NULL;
}
/* check the length */
data_sz = fu_common_read_uint16 (&buf[1], G_LITTLE_ENDIAN);
if (data_sz + FU_CSR_COMMAND_HEADER_SIZE != (guint16) sz) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"wrong data length %" G_GUINT16_FORMAT,
data_sz);
return NULL;
}
/* return as bytes */
return g_bytes_new (buf + FU_CSR_COMMAND_HEADER_SIZE,
sz - FU_CSR_COMMAND_HEADER_SIZE);
}
GBytes *
fu_csr_device_upload (FuCsrDevice *self, GError **error)
{
g_autoptr(GPtrArray) chunks = NULL;
guint32 total_sz = 0;
gsize done_sz = 0;
/* notify UI */
fu_device_set_status (FU_DEVICE (self), FWUPD_STATUS_DEVICE_READ);
chunks = g_ptr_array_new_with_free_func ((GDestroyNotify) g_bytes_unref);
for (guint32 i = 0; i < 0x3ffffff; i++) {
g_autoptr(GBytes) chunk = NULL;
gsize chunk_sz;
/* hit hardware */
chunk = fu_csr_device_upload_chunk (self, error);
if (chunk == NULL)
return FALSE;
chunk_sz = g_bytes_get_size (chunk);
/* get the total size using the CSR header */
if (i == 0 && chunk_sz >= 10) {
const guint8 *buf = g_bytes_get_data (chunk, NULL);
if (memcmp (buf, "CSR-dfu", 7) == 0) {
guint16 hdr_ver;
guint16 hdr_len;
hdr_ver = fu_common_read_uint16 (buf + 8, G_LITTLE_ENDIAN);
if (hdr_ver != 0x03) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"CSR header version is "
"invalid %" G_GUINT16_FORMAT,
hdr_ver);
return NULL;
}
total_sz = fu_common_read_uint32 (buf + 10, G_LITTLE_ENDIAN);
if (total_sz == 0) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"CSR header data length "
"invalid %" G_GUINT32_FORMAT,
total_sz);
return NULL;
}
hdr_len = fu_common_read_uint16 (buf + 14, G_LITTLE_ENDIAN);
g_debug ("CSR header length: %" G_GUINT16_FORMAT, hdr_len);
}
}
/* add to chunk array */
done_sz += chunk_sz;
g_ptr_array_add (chunks, g_steal_pointer (&chunk));
fu_device_set_progress_full (FU_DEVICE (self), done_sz, (gsize) total_sz);
/* we're done */
if (chunk_sz < 64 - FU_CSR_COMMAND_HEADER_SIZE)
break;
}
/* notify UI */
fu_device_set_status (FU_DEVICE (self), FWUPD_STATUS_IDLE);
return dfu_utils_bytes_join_array (chunks);
}
static gboolean
fu_csr_device_download_chunk (FuCsrDevice *self, guint16 idx, GBytes *chunk, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (FU_USB_DEVICE (self));
const guint8 *chunk_data;
gsize chunk_sz = 0;
gsize write_sz = 0;
guint8 buf[FU_CSR_PACKET_DATA_SIZE] = {0};
/* too large? */
chunk_data = g_bytes_get_data (chunk, &chunk_sz);
if (chunk_sz + FU_CSR_COMMAND_HEADER_SIZE > FU_CSR_PACKET_DATA_SIZE) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"packet was too large: %" G_GSIZE_FORMAT, chunk_sz);
return FALSE;
}
g_debug ("writing %" G_GSIZE_FORMAT " bytes of data", chunk_sz);
/* create packet */
buf[0] = FU_CSR_REPORT_ID_COMMAND;
buf[1] = FU_CSR_COMMAND_UPGRADE;
fu_common_write_uint16 (&buf[2], idx, G_LITTLE_ENDIAN);
fu_common_write_uint16 (&buf[4], chunk_sz, G_LITTLE_ENDIAN);
memcpy (buf + FU_CSR_COMMAND_HEADER_SIZE, chunk_data, chunk_sz);
/* hit hardware */
fu_csr_device_dump ("Upgrade", buf, sizeof(buf));
if (!g_usb_device_control_transfer (usb_device,
G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
G_USB_DEVICE_REQUEST_TYPE_CLASS,
G_USB_DEVICE_RECIPIENT_INTERFACE,
HID_REPORT_SET, /* bRequest */
HID_FEATURE | FU_CSR_REPORT_ID_COMMAND, /* wValue */
0x0000, /* wIndex */
buf,
sizeof(buf),
&write_sz,
FU_CSR_DEVICE_TIMEOUT,
NULL, error)) {
g_prefix_error (error, "Failed to Upgrade: ");
return FALSE;
}
/* check packet */
if (write_sz != sizeof(buf)) {
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"Not all packet written for upgrade got "
"%" G_GSIZE_FORMAT " expected %" G_GSIZE_FORMAT,
write_sz, sizeof(buf));
return FALSE;
}
/* wait for hardware */
if (self->quirks & FU_CSR_DEVICE_QUIRK_REQUIRE_DELAY) {
g_debug ("sleeping for %ums", self->dnload_timeout);
g_usleep (self->dnload_timeout * 1000);
}
/* get status */
if (!fu_csr_device_get_status (self, error))
return FALSE;
/* is still busy */
if (self->dfu_state == DFU_STATE_DFU_DNBUSY) {
g_debug ("busy, so sleeping a bit longer");
g_usleep (G_USEC_PER_SEC);
if (!fu_csr_device_get_status (self, error))
return FALSE;
}
/* not correct */
if (self->dfu_state != DFU_STATE_DFU_DNLOAD_IDLE &&
self->dfu_state != DFU_STATE_DFU_IDLE) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INTERNAL,
"device did not return to IDLE");
return FALSE;
}
/* success */
return TRUE;
}
static GBytes *
_dfu_firmware_get_default_element_data (DfuFirmware *firmware)
{
DfuElement *element;
DfuImage *image;
image = dfu_firmware_get_image_default (firmware);
if (image == NULL)
return NULL;
element = dfu_image_get_element_default (image);
if (element == NULL)
return NULL;
return dfu_element_get_contents (element);
}
gboolean
fu_csr_device_download (FuCsrDevice *self, GBytes *blob, GError **error)
{
GBytes *blob_noftr;
const guint8 *data;
gsize sz = 0;
guint16 idx;
g_autoptr(DfuFirmware) dfu_firmware = dfu_firmware_new ();
g_autoptr(GBytes) blob_empty = NULL;
g_autoptr(GPtrArray) packets = NULL;
/* notify UI */
fu_device_set_status (FU_DEVICE (self), FWUPD_STATUS_DEVICE_WRITE);
/* parse the file */
if (!dfu_firmware_parse_data (dfu_firmware, blob,
DFU_FIRMWARE_PARSE_FLAG_NONE, error))
return FALSE;
if (g_getenv ("FWUPD_CSR_VERBOSE") != NULL) {
g_autofree gchar *fw_str = NULL;
fw_str = dfu_firmware_to_string (dfu_firmware);
g_debug ("%s", fw_str);
}
if (dfu_firmware_get_format (dfu_firmware) != DFU_FIRMWARE_FORMAT_DFU) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"expected DFU firmware");
return FALSE;
}
/* get the blob from the firmware file */
blob_noftr = _dfu_firmware_get_default_element_data (dfu_firmware);
if (blob_noftr == NULL) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_INVALID_FILE,
"firmware contained no data");
return FALSE;
}
/* create packets */
data = g_bytes_get_data (blob_noftr, &sz);
packets = dfu_chunked_new (data, (guint32) sz, 0x0, 0x0,
FU_CSR_PACKET_DATA_SIZE - FU_CSR_COMMAND_HEADER_SIZE);
/* send to hardware */
for (idx = 0; idx < packets->len; idx++) {
DfuChunkedPacket *pkt = g_ptr_array_index (packets, idx);
g_autoptr(GBytes) blob_tmp = g_bytes_new_static (pkt->data, pkt->data_sz);
/* send packet */
if (!fu_csr_device_download_chunk (self, idx, blob_tmp, error))
return FALSE;
/* update progress */
fu_device_set_progress_full (FU_DEVICE (self),
(gsize) idx, (gsize) packets->len);
}
/* all done */
blob_empty = g_bytes_new (NULL, 0);
if (!fu_csr_device_download_chunk (self, idx, blob_empty, error))
return FALSE;
/* notify UI */
fu_device_set_status (FU_DEVICE (self), FWUPD_STATUS_IDLE);
return TRUE;
}
static gboolean
fu_csr_device_probe (FuUsbDevice *device, GError **error)
{
const gchar *quirk_str;
/* devices have to be whitelisted */
quirk_str = fu_device_get_plugin_hints (FU_DEVICE (device));
if (quirk_str == NULL) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"not supported with this device");
return FALSE;
}
if (g_strcmp0 (quirk_str, "require-delay") == 0) {
fu_csr_device_set_quirks (FU_CSR_DEVICE (device),
FU_CSR_DEVICE_QUIRK_REQUIRE_DELAY);
}
/* hardcoded */
fu_device_add_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_UPDATABLE);
/* success */
return TRUE;
}
static gboolean
fu_csr_device_open (FuUsbDevice *device, GError **error)
{
FuCsrDevice *self = FU_CSR_DEVICE (device);
GUsbDevice *usb_device = fu_usb_device_get_dev (device);
/* open device and clear status */
if (!g_usb_device_claim_interface (usb_device, 0x00, /* HID */
G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
error)) {
g_prefix_error (error, "failed to claim HID interface: ");
return FALSE;
}
if (!fu_csr_device_clear_status (self, error))
return FALSE;
/* success */
return TRUE;
}
static gboolean
fu_csr_device_close (FuUsbDevice *device, GError **error)
{
GUsbDevice *usb_device = fu_usb_device_get_dev (device);
/* we're done here */
if (!g_usb_device_release_interface (usb_device, 0x00, /* HID */
G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER,
error)) {
g_prefix_error (error, "failed to release interface: ");
return FALSE;
}
/* success */
return TRUE;
}
static void
fu_csr_device_init (FuCsrDevice *device)
{
}
static void
fu_csr_device_class_init (FuCsrDeviceClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
FuUsbDeviceClass *klass_usb_device = FU_USB_DEVICE_CLASS (klass);
klass_device->to_string = fu_csr_device_to_string;
klass_usb_device->open = fu_csr_device_open;
klass_usb_device->close = fu_csr_device_close;
klass_usb_device->probe = fu_csr_device_probe;
}
FuCsrDevice *
fu_csr_device_new (GUsbDevice *usb_device)
{
FuCsrDevice *device = NULL;
device = g_object_new (FU_TYPE_CSR_DEVICE,
"usb-device", usb_device,
NULL);
return device;
}