blob: 4e0297db2ef44a7dd1909bf344449082315e41d3 [file] [log] [blame]
/*
* Copyright 2017 Limes Audio AB. All rights reserved.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "upgrade.h"
#include "crc32.h"
#include "device.h"
#include "util.h"
#include <argp.h>
#include <errno.h>
#include <libusb-1.0/libusb.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
enum dfu_state {
STATE_appIDLE = 0x0,
STATE_appDETACH = 0x1,
STATE_dfuIDLE = 0x2,
STATE_dfuDNLOAD_SYNC = 0x3,
STATE_dfuDNBUSY = 0x4,
STATE_dfuDNLOAD_IDLE = 0x5,
STATE_dfuMANIFEST_SYNC = 0x6,
STATE_dfuMANIFEST = 0x7,
STATE_dfuMANIFEST_WAIT_RESET = 0x8,
STATE_dfuUPLOAD_IDLE = 0x9,
STATE_dfuERROR = 0xA
};
enum dfu_status {
STATUS_OK = 0x00,
STATUS_errTARGET = 0x01,
STATUS_errFILE = 0x02,
STATUS_errWRITE = 0x03,
STATUS_errERASE = 0x04,
STATUS_errCHECK_ERASED = 0x05,
STATUS_errPROG = 0x06,
STATUS_errVERIFY = 0x07,
STATUS_errADDRESS = 0x08,
STATUS_errNOTDONE = 0x09,
STATUS_errFIRMWARE = 0x0A,
STATUS_errVENDOR = 0x0B,
STATUS_errUSBR = 0x0C,
STATUS_errPOR = 0x0D,
STATUS_errUNKNOWN = 0x0E,
STATUS_errSTALLEDPKT = 0x0F
};
struct dfu_suffix {
uint32_t dwCRC;
uint8_t bLength;
uint8_t ucDfuSignature[3];
uint16_t bcdDFU;
uint16_t idVendor;
uint16_t idProduct;
uint16_t bcdDevice;
};
struct dfu_descriptor_attributes {
uint8_t bitCanDnload : 1;
uint8_t bitCanUpload : 1;
uint8_t bitManifestationTolerant : 1;
uint8_t bitWillDetach : 1;
};
struct dfu_descriptor {
uint8_t bLength;
uint8_t bDescriptorType;
struct dfu_descriptor_attributes bmAttributes;
uint16_t wDetachTimeOut;
uint16_t wTransferSize;
uint16_t bcdDFUVersion;
};
struct dfu_status_request {
uint8_t bStatus;
uint32_t bwPollTimeout;
uint8_t bState;
uint8_t iString;
};
struct arguments {
FILE *file;
struct dfu_suffix dfu_suffix;
bool force;
bool retry;
bool debug;
};
static bool debug;
static const char *
status_to_string(enum dfu_status status)
{
switch (status) {
case STATUS_OK:
return "No error condition is present";
case STATUS_errTARGET:
return "File is not targeted for use by this device";
case STATUS_errFILE:
return "File is for this device but fails some vendor-specific "
"verification test";
case STATUS_errWRITE:
return "Device is unable to write memory";
case STATUS_errERASE:
return "Memory erase function failed";
case STATUS_errCHECK_ERASED:
return "Memory erase check failed";
case STATUS_errPROG:
return "Program memory function failed";
case STATUS_errVERIFY:
return "Programmed memory failed verification";
case STATUS_errADDRESS:
return "Cannot program memory due to received address that is out "
"of range";
case STATUS_errNOTDONE:
return "Received DFU_DNLOAD with wLength = 0, but device does not "
"think it has all of the data yet";
case STATUS_errFIRMWARE:
return "Device's firmware is corrupt. It cannot return to run-time "
"(non-DFU) operations";
case STATUS_errVENDOR:
return "errVENDOR";
case STATUS_errUSBR:
return "Device detected unexpected USB reset signaling";
case STATUS_errPOR:
return "Device detected unexpected power on reset";
case STATUS_errUNKNOWN:
return "Something went wrong, but the device does not know what it "
"was";
case STATUS_errSTALLEDPKT:
return "Device stalled an unexpected request";
}
return "";
}
static int
parse_dfu_suffix(FILE *file, struct dfu_suffix *suffix)
{
if (fseek(file, -16, SEEK_END) < 0) {
return -1;
}
fread(&suffix->bcdDevice, sizeof(suffix->bcdDevice), 1, file);
fread(&suffix->idProduct, sizeof(suffix->idProduct), 1, file);
fread(&suffix->idVendor, sizeof(suffix->idVendor), 1, file);
fread(&suffix->bcdDFU, sizeof(suffix->bcdDFU), 1, file);
fread(&suffix->ucDfuSignature[2], sizeof(*suffix->ucDfuSignature), 1, file);
fread(&suffix->ucDfuSignature[1], sizeof(*suffix->ucDfuSignature), 1, file);
fread(&suffix->ucDfuSignature[0], sizeof(*suffix->ucDfuSignature), 1, file);
fread(&suffix->bLength, sizeof(suffix->bLength), 1, file);
fread(&suffix->dwCRC, sizeof(suffix->dwCRC), 1, file);
long to_read = ftell(file) - 4;
rewind(file);
uint32_t crc = 0xFFFFFFFF;
for (long i = 0; i < to_read; ++i) {
crc = crc32(crc, (uint8_t)fgetc(file));
}
if (suffix->dwCRC != crc) {
return -1;
}
if (!((suffix->ucDfuSignature[0] == 'D')
&& (suffix->ucDfuSignature[1] == 'F')
&& (suffix->ucDfuSignature[2] == 'U'))) {
return -1;
}
return 0;
}
static bool
is_correct_usb_id(struct dfu_suffix *suffix)
{
return ((suffix->idVendor == USB_VID) && (suffix->idProduct == USB_PID));
}
static error_t
parser(int key, char *arg, struct argp_state *state)
{
struct arguments *arguments = state->input;
switch (key) {
case 'f':
arguments->force = true;
break;
case 'r':
arguments->retry = true;
break;
case 'd':
arguments->debug = true;
break;
case ARGP_KEY_ARG:
if (state->arg_num >= 1) {
argp_usage(state);
}
arguments->file = fopen(arg, "rb");
if (!arguments->file) {
argp_failure(state, 1, errno, "%s", arg);
}
if (parse_dfu_suffix(arguments->file, &arguments->dfu_suffix) < 0) {
argp_error(state, "%s: Not a valid firmware bundle", arg);
}
if (!is_correct_usb_id(&arguments->dfu_suffix)) {
argp_error(state,
"%s: The firmware bundle is not for an Atrus device",
arg);
}
break;
case ARGP_KEY_END:
if (state->arg_num < 1) {
argp_usage(state);
}
break;
default:
return ARGP_ERR_UNKNOWN;
}
return 0;
}
static int
dfu_detach(libusb_device_handle *device, uint16_t interface, uint16_t timeout)
{
uint8_t bmRequestType = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_CLASS
| LIBUSB_RECIPIENT_INTERFACE;
return libusb_control_transfer(device, bmRequestType, 0x0, timeout,
interface, NULL, 0, 10000);
}
static int
dfu_dnload(libusb_device_handle *device,
uint16_t interface,
uint16_t block,
uint8_t *data,
uint16_t length)
{
uint8_t bmRequestType = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_CLASS
| LIBUSB_RECIPIENT_INTERFACE;
return libusb_control_transfer(device, bmRequestType, 0x1, block, interface,
data, length, 10000);
}
static int
dfu_get_status(libusb_device_handle *device,
uint16_t interface,
struct dfu_status_request *status)
{
uint8_t buffer[6] = {0};
uint8_t bmRequestType = LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_CLASS
| LIBUSB_RECIPIENT_INTERFACE;
int retval = libusb_control_transfer(device, bmRequestType, 0x3, 0,
interface, buffer, 6, 10000);
status->bStatus = buffer[0];
status->bwPollTimeout =
(uint32_t)((buffer[3] << 16) | (buffer[2] << 8) | buffer[1]);
status->bState = buffer[4];
status->iString = buffer[5];
if (debug) {
printf("DFU retval=%d, state=0x%X, status=0x%02X\n", retval,
status->bState, status->bStatus);
}
return retval;
}
static int
dfu_clr_status(libusb_device_handle *device, uint16_t interface)
{
uint8_t bmRequestType = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_CLASS
| LIBUSB_RECIPIENT_INTERFACE;
return libusb_control_transfer(device, bmRequestType, 0x4, 0, interface,
NULL, 0, 10000);
}
static int
dfu_abort(libusb_device_handle *device, uint16_t interface)
{
uint8_t bmRequestType = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_CLASS
| LIBUSB_RECIPIENT_INTERFACE;
return libusb_control_transfer(device, bmRequestType, 0x6, 0, interface,
NULL, 0, 10000);
}
static int
version_compare(uint16_t a, uint16_t b)
{
int va[3];
int vb[3];
bcd_to_version(a, &va[0], &va[1], &va[2]);
bcd_to_version(b, &vb[0], &vb[1], &vb[2]);
for (int i = 0; i < 3; ++i) {
if (va[i] > vb[i]) {
return 1;
} else if (va[i] < vb[i]) {
return -1;
}
}
return 0;
}
static bool
get_dfu_descriptor(libusb_device *device,
struct libusb_device_descriptor *device_desc,
uint8_t *interface,
struct dfu_descriptor *dfu_desc)
{
for (uint8_t i = 0; i != device_desc->bNumConfigurations; ++i) {
struct libusb_config_descriptor *config_desc;
if (libusb_get_config_descriptor(device, i, &config_desc) < 0) {
continue;
}
for (uint8_t j = 0; j < config_desc->bNumInterfaces; ++j) {
const struct libusb_interface *iface = &config_desc->interface[j];
if (!iface) {
break;
}
for (uint8_t k = 0; k < iface->num_altsetting; ++k) {
const struct libusb_interface_descriptor *iface_desc;
iface_desc = &iface->altsetting[k];
if (iface_desc->bInterfaceClass == LIBUSB_CLASS_APPLICATION
&& iface_desc->bInterfaceSubClass == 1) {
if (sizeof(*dfu_desc) < (size_t)iface_desc->extra_length) {
return false;
}
*interface = j;
memcpy(dfu_desc, iface_desc->extra,
(size_t)iface_desc->extra_length);
return true;
}
}
}
libusb_free_config_descriptor(config_desc);
}
return false;
}
static void
set_devices_in_dfu_mode(libusb_context *context,
struct dfu_suffix *dfu_suffix,
bool only_lower_version)
{
libusb_device **devices;
ssize_t num_devices = libusb_get_device_list(context, &devices);
for (ssize_t i = 0; i < num_devices; ++i) {
struct libusb_device_descriptor device_desc;
libusb_device *device = devices[i];
if (libusb_get_device_descriptor(device, &device_desc) < 0) {
continue;
}
if ((device_desc.idVendor != dfu_suffix->idVendor)
|| (device_desc.idProduct != dfu_suffix->idProduct)) {
continue;
}
uint8_t interface;
struct dfu_descriptor dfu_desc;
if (!get_dfu_descriptor(device, &device_desc, &interface, &dfu_desc)) {
continue;
}
libusb_device_handle *handle;
int retval = libusb_open(device, &handle);
if (retval < 0) {
continue;
}
size_t string_length = 256;
char serial[string_length];
retval = libusb_get_string_descriptor_ascii(
handle, device_desc.iSerialNumber, (unsigned char *)serial,
(int)string_length);
if (retval < 0) {
strncpy(serial, "<unknown>", string_length);
}
if (only_lower_version
&& (version_compare(device_desc.bcdDevice, dfu_suffix->bcdDevice)
>= 0)) {
int major, minor, patch;
bcd_to_version(device_desc.bcdDevice, &major, &minor, &patch);
printf("Skipping %s with version %d.%d.%d\n", serial, major, minor,
patch);
goto release_handle;
}
int config;
if (libusb_get_configuration(handle, &config) < 0) {
goto release_handle;
}
if (config == 0) {
retval = libusb_set_configuration(handle, 1);
if (retval < 0) {
goto release_handle;
}
}
retval = libusb_claim_interface(handle, interface);
if (retval < 0) {
goto release_handle;
}
printf("Setting %s in DFU mode\n", serial);
dfu_detach(handle, interface, 16);
libusb_reset_device(handle);
libusb_release_interface(handle, interface);
release_handle:
libusb_close(handle);
}
}
static void
on_dfu_error(libusb_device_handle *handle, uint16_t interface)
{
struct dfu_status_request status_req;
int retval = dfu_get_status(handle, interface, &status_req);
if (retval < 0) {
fprintf(stderr, "Could not get status of the device\n");
return;
}
enum dfu_state state = (enum dfu_state)status_req.bState;
if (state != STATE_dfuERROR) {
return;
}
enum dfu_status status = (enum dfu_status)status_req.bStatus;
if (status == STATUS_OK) {
return;
}
size_t string_length = 256;
char status_string[string_length];
if (status == STATUS_errVENDOR) {
retval = libusb_get_string_descriptor_ascii(
handle, status_req.iString, (unsigned char *)status_string,
(int)string_length);
if (retval < 0) {
strncpy(status_string, "Could not get vendor defined status string",
string_length);
}
} else {
strncpy(status_string, status_to_string(status), string_length);
}
fprintf(stderr, "Error: %s\n", status_string);
dfu_clr_status(handle, interface);
}
static void
timeout(long ms)
{
int retval;
struct timespec sleep_req;
struct timespec sleep_rem;
sleep_req.tv_sec = ms / 1000;
sleep_req.tv_nsec = (ms % 1000) * 1000000;
do {
retval = nanosleep(&sleep_req, &sleep_rem);
if (retval < 0) {
if (errno != EINTR) {
return;
}
sleep_req.tv_sec = sleep_rem.tv_sec;
sleep_req.tv_nsec = sleep_rem.tv_nsec;
}
} while (retval != 0);
}
static int
get_status_until_state(libusb_device_handle *handle,
uint16_t interface,
struct dfu_status_request *status_req,
enum dfu_state state)
{
int retries = 0;
const int retry_limit = 100;
while (true) {
int retval = dfu_get_status(handle, interface, status_req);
if (retval < 0) {
return -1;
}
if (status_req->bState == STATE_dfuERROR) {
on_dfu_error(handle, interface);
return -1;
}
if (status_req->bState == state) {
break;
}
retries += 1;
if (retries >= retry_limit) {
return -1;
}
timeout(status_req->bwPollTimeout);
};
return 0;
}
static int
download_file(libusb_device_handle *handle, uint16_t interface, FILE *file)
{
struct dfu_status_request status_req;
int retval = dfu_get_status(handle, interface, &status_req);
if ((retval < 0) || (status_req.bState != STATE_dfuIDLE)) {
return -1;
}
uint16_t block = 0;
size_t buffer_length = 64;
enum dfu_state expected_state = STATE_dfuDNLOAD_IDLE;
size_t bytes_read;
rewind(file);
do {
uint8_t buffer[buffer_length];
bytes_read = fread(buffer, sizeof(*buffer), buffer_length, file);
if (bytes_read < buffer_length) {
if (ferror(file) < 0) {
return -1;
}
if (feof(file)) {
expected_state = STATE_dfuIDLE;
}
}
retval = dfu_get_status(handle, interface, &status_req);
if (retval < 0) {
return -1;
}
if ((enum dfu_state)status_req.bState == STATE_dfuERROR) {
on_dfu_error(handle, interface);
return -1;
}
retval = dfu_dnload(handle, interface, block++, buffer,
(uint16_t)bytes_read);
if (retval < 0) {
return -1;
}
timeout(status_req.bwPollTimeout);
} while (bytes_read != 0);
retval = get_status_until_state(handle, interface, &status_req,
expected_state);
return retval;
}
static int
perform_dfu(libusb_context *context,
struct dfu_suffix *dfu_suffix,
FILE *file,
int *num_successful)
{
int num_found = 0;
*num_successful = 0;
libusb_device **devices;
ssize_t num_devices = libusb_get_device_list(context, &devices);
for (ssize_t i = 0; i < num_devices; ++i) {
struct libusb_device_descriptor device_desc;
libusb_device *device = devices[i];
if (libusb_get_device_descriptor(device, &device_desc) < 0) {
continue;
}
if ((device_desc.idVendor != dfu_suffix->idVendor)
|| (device_desc.idProduct != (dfu_suffix->idProduct + 1))) {
continue;
}
uint8_t interface;
struct dfu_descriptor dfu_desc;
if (!get_dfu_descriptor(device, &device_desc, &interface, &dfu_desc)) {
continue;
}
++num_found;
libusb_device_handle *handle;
int retval = libusb_open(device, &handle);
if (retval < 0) {
continue;
}
size_t string_length = 256;
char serial[string_length];
retval = libusb_get_string_descriptor_ascii(
handle, device_desc.iSerialNumber, (unsigned char *)serial,
(int)string_length);
if (retval < 0) {
strncpy(serial, "<unknown>", string_length);
}
if (!dfu_desc.bmAttributes.bitCanDnload
|| !dfu_desc.bmAttributes.bitManifestationTolerant
|| dfu_desc.bmAttributes.bitWillDetach) {
fprintf(stderr,
"Found %s with unsupported USB DFU interface descriptor\n",
serial);
goto release_handle;
}
retval = libusb_set_configuration(handle, 1);
if (retval < 0) {
goto release_handle;
}
retval = libusb_claim_interface(handle, interface);
if (retval < 0) {
goto release_handle;
}
int major, minor, patch;
bcd_to_version(device_desc.bcdDevice, &major, &minor, &patch);
printf("Upgrading %s from version %d.%d.%d\n", serial, major, minor,
patch);
const int max_retries = 3;
int retries = 0;
do {
// Check that the device is in a proper state for beginning download
struct dfu_status_request status_req;
dfu_get_status(handle, interface, &status_req);
switch (status_req.bState) {
case STATE_dfuIDLE:
case STATE_dfuDNLOAD_SYNC:
case STATE_dfuDNLOAD_IDLE:
case STATE_dfuMANIFEST_SYNC:
case STATE_dfuUPLOAD_IDLE:
dfu_abort(handle, interface);
break;
default:
break;
}
dfu_get_status(handle, interface, &status_req);
if (status_req.bState == STATE_dfuERROR) {
// Silently ignore any previous error state and reset to dfuIDLE
dfu_clr_status(handle, interface);
dfu_get_status(handle, interface, &status_req);
}
if (status_req.bState != STATE_dfuIDLE) {
fprintf(stderr,
"Device %s is not in a proper state for upgrading\n",
serial);
goto usb_reset;
}
retval = download_file(handle, interface, file);
if (retval < 0) {
printf("Upgrade failed on %s\n", serial);
++retries;
if (retries <= max_retries) {
printf("Retrying upgrade on %s, attempt %d/%d\n", serial,
retries, max_retries);
}
} else {
printf("Upgrade successful on %s\n", serial);
++(*num_successful);
}
} while ((retval < 0) && (retries <= max_retries));
usb_reset:
libusb_reset_device(handle);
libusb_release_interface(handle, interface);
release_handle:
libusb_close(handle);
}
return num_found;
}
int
upgrade(int argc, char **argv)
{
char args_doc[] = "FILE";
char doc[] =
"\n"
"Options:";
struct argp_option options[] = {
{"force", 'f', NULL, 0,
"Try to upgrade devices with newer version than FILE", 0},
{"retry", 'r', NULL, 0, "Retry if no devices are found", 0},
{"debug", 'd', NULL, 0, "Debug output", 1},
{0}};
struct argp argp = {options, parser, args_doc, doc, NULL, NULL, NULL};
struct arguments arguments = {0};
int retval = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, NULL, &arguments);
if (retval < 0) {
goto close_file;
}
debug = arguments.debug;
libusb_context *context;
retval = libusb_init(&context);
if (retval < 0) {
goto close_file;
}
libusb_set_debug(NULL, LIBUSB_LOG_LEVEL_WARNING);
libusb_set_debug(context, LIBUSB_LOG_LEVEL_WARNING);
int major, minor, patch;
bcd_to_version(arguments.dfu_suffix.bcdDevice, &major, &minor, &patch);
printf("Starting upgrade to version %d.%d.%d\n", major, minor, patch);
struct dfu_suffix suffix = arguments.dfu_suffix;
set_devices_in_dfu_mode(context, &suffix, !arguments.force);
// Make sure devices have time to re-enumerate in DFU mode
timeout(1000);
unsigned int backoff = 1;
const unsigned int limit = 64;
int num_successful = 0;
do {
retval = perform_dfu(context, &suffix, arguments.file, &num_successful);
if (retval > 0) {
break;
}
if (!arguments.retry) {
break;
}
printf("No devices in DFU mode found, retrying in %u s\n", backoff);
sleep(backoff);
backoff *= 2;
} while ((retval <= 0) && (backoff < limit));
if (retval > 0) {
retval = 0;
}
printf("Upgrade succeeded on %d device(s)\n", num_successful);
libusb_exit(context);
close_file:
if (arguments.file) {
fclose(arguments.file);
}
return retval;
}