Add atrusctl

BUG=chrome-os-partner:62481
TEST=Initial commit, not used anywhere yet

Change-Id: I0ff5b0bcd17b3513bc546f8cbd74367d648489a2
Signed-off-by: Emil Lundmark <emil@limesaudio.com>
Reviewed-on: https://chromium-review.googlesource.com/444411
Reviewed-by: Kees Cook <keescook@chromium.org>
Commit-Queue: Gabe Black <gabeblack@chromium.org>
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..c55a705
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,18 @@
+---
+BasedOnStyle: Chromium
+BreakBeforeBinaryOperators: All
+CommentPragmas: ''
+ContinuationIndentWidth: 8
+IndentWidth: 4
+---
+Language: Cpp
+AlwaysBreakAfterReturnType: TopLevel
+BraceWrapping:
+  AfterFunction: true
+BreakBeforeBinaryOperators: NonAssignment
+BreakBeforeBraces: Custom
+IncludeCategories:
+- Regex: '^".*"'
+  Priority: 1
+PointerAlignment: Right
+SortIncludes: true
diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644
index 0000000..9e3649e
--- /dev/null
+++ b/.clang-tidy
@@ -0,0 +1,3 @@
+---
+Checks: 'misc-*,modernize-*,performance-*,readability-*'
+HeaderFilterRegex: .*
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a5309e6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+build*/
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..72b22c0
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,5 @@
+cmake_minimum_required(VERSION 2.8.12)
+
+project(ATRUSCTL C)
+
+add_subdirectory(src)
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..20a462a
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,26 @@
+Copyright 2017 Limes Audio AB. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  1. Redistributions of source code must retain the above copyright notice, this
+     list of conditions and the following disclaimer.
+
+  2. Redistributions in binary form must reproduce the above copyright notice,
+     this list of conditions and the following disclaimer in the documentation
+     and/or other materials provided with the distribution.
+
+  3. Neither the name of the copyright holder nor the names of its contributors
+     may be used to endorse or promote products derived from this software
+     without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/PRESUBMIT.cfg b/PRESUBMIT.cfg
new file mode 100644
index 0000000..f2d8f02
--- /dev/null
+++ b/PRESUBMIT.cfg
@@ -0,0 +1,2 @@
+[Hook Overrides]
+cros_license_check: false
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3d4cb0e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,34 @@
+# atrusctl
+This tool is used to interact with an Atrus device.
+
+## Requirements
+The GNU C Library, libusb 1.0 and libudev are required.
+GNU C is required because the program utilizes `argp`.
+
+## Building
+CMake is used for building the application.
+
+Here is an example of how to build:
+```
+$ mkdir build/
+$ cd build/
+$ cmake ..
+$ make
+```
+You can also install it on your machine by running:
+```
+$ make install
+```
+
+## Using
+Run the following to see the help section of the application.
+```
+$ atrusctl --help
+```
+
+Also, make sure you have read and write access to the USB device.
+This may be done by, e.g., setting appropriate [udev](https://www.kernel.org/pub/linux/utils/kernel/hotplug/udev/udev.html) rules.
+
+## License
+Individual files are tagged with SPDX-License-Identifier to indicate its license instead of including the full license text.
+See the [SPDX License List](https://spdx.org/licenses/) for more information.
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..f45b585
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,17 @@
+add_definitions(-D_GNU_SOURCE)
+
+add_executable(atrusctl
+               atrusctl.c
+               crc32.c
+               diagnose.c
+               list.c
+               upgrade.c
+               util.c)
+
+find_library(libudev_LIBRARIES NAMES udev)
+find_library(libusb_LIBRARIES NAMES usb-1.0)
+target_link_libraries(atrusctl
+                      ${libudev_LIBRARIES}
+                      ${libusb_LIBRARIES})
+
+install(TARGETS atrusctl DESTINATION bin)
diff --git a/src/atrusctl.c b/src/atrusctl.c
new file mode 100644
index 0000000..4316331
--- /dev/null
+++ b/src/atrusctl.c
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2017 Limes Audio AB. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include "diagnose.h"
+#include "list.h"
+#include "upgrade.h"
+
+#include <argp.h>
+#include <stdlib.h>
+#include <string.h>
+
+struct arguments {
+    int (*func)(int argc, char **argv);
+    int argc;
+    char **argv;
+};
+
+static void
+cmd_prepare(struct argp_state *state, struct arguments *arguments)
+{
+    if (state->next <= 0) {
+        argp_error(state, "unable to parse command line argument");
+        return;
+    }
+
+    int argc = state->argc - state->next + 1;
+    char **argv = &state->argv[state->next - 1];
+
+    char *cmd_name = argv[0];
+    argv[0] = malloc(strlen(state->name) + 1 + strlen(cmd_name) + 1);
+    if (!argv[0]) {
+        argp_failure(state, 1, ENOMEM, NULL);
+    }
+    sprintf(argv[0], "%s %s", state->name, cmd_name);
+
+    arguments->argc = argc;
+    arguments->argv = argv;
+}
+
+static error_t
+parser(int key, char *arg, struct argp_state *state)
+{
+    struct arguments *arguments = state->input;
+
+    switch (key) {
+        case ARGP_KEY_ARG:
+            if (state->arg_num == 0) {
+                if (strcmp(arg, "diagnose") == 0) {
+                    arguments->func = diagnose;
+                } else if (strcmp(arg, "list") == 0) {
+                    arguments->func = list;
+                } else if (strcmp(arg, "upgrade") == 0) {
+                    arguments->func = upgrade;
+                }
+
+                if (arguments->func) {
+                    cmd_prepare(state, arguments);
+                    return ARGP_KEY_SUCCESS;
+                }
+
+                argp_error(state, "%s is not a valid command", arg);
+            }
+            break;
+
+        case ARGP_KEY_NO_ARGS:
+            argp_state_help(state, stdout, ARGP_HELP_STD_HELP);
+            break;
+
+        default:
+            return ARGP_ERR_UNKNOWN;
+    }
+
+    return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+    char args_doc[] = "[COMMAND]";
+    char doc[] =
+            "\n"
+            "Options:"
+            "\v"
+            "Commands:\n"
+            "\n"
+            "  diagnose                   Show diagnostic information\n"
+            "  list                       List info about attached devices\n"
+            "  upgrade                    Firmware upgrade";
+    struct argp argp = {NULL, 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) {
+        return 1;
+    }
+
+    retval = arguments.func(arguments.argc, arguments.argv);
+    free(arguments.argv[0]);
+    if (retval < 0) {
+        return 1;
+    }
+
+    return 0;
+}
diff --git a/src/crc32.c b/src/crc32.c
new file mode 100644
index 0000000..29ff4b7
--- /dev/null
+++ b/src/crc32.c
@@ -0,0 +1,88 @@
+/*
+ * CRC polynomial 0xedb88320 – Contributed unknowingly by Gary S. Brown.
+ *
+ * "Copyright (C) 1986 Gary S. Brown. You may use this program, or code or
+ *  tables extracted from it, as desired without restriction."
+ *
+ * Paraphrased comments from the original:
+ *
+ * The 32 BIT ANSI X3.66 CRC checksum algorithm is used to compute the 32-bit
+ * frame check sequence in ADCCP. (ANSI X3.66, also known as FIPS PUB 71 and
+ * FED-STD-1003, the U.S. versions of CCITT's X.25 link-level protocol.)
+ *
+ * The polynomial is:
+ * X^32+X^26+X^23+X^22+X^16+X^12+X^11+X^10+X^8+X^7+X^5+X^4+X^2+X^1+X^0
+ *
+ * Put the highest-order term in the lowest-order bit. The X^32 term is
+ * implied, the LSB is the X^31 term, etc. The X^0 term usually shown as +1)
+ * results in the MSB being 1. Put the highest-order term in the lowest-order
+ * bit. The X^32 term is implied, the LSB is the X^31 term, etc. The X^0
+ * term (usually shown as +1) results in the MSB being 1.
+ *
+ * The feedback terms table consists of 256 32-bit entries. The feedback terms
+ * simply represent the results of eight shift/xor operations for all
+ * combinations of data and CRC register values. The values must be right-
+ * shifted by eight bits by the UPDCRC logic so the shift must be unsigned.
+ */
+
+#include "crc32.h"
+
+static const uint32_t lookup_table[] = {
+        0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F,
+        0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
+        0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2,
+        0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
+        0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9,
+        0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
+        0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C,
+        0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
+        0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423,
+        0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
+        0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106,
+        0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
+        0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D,
+        0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
+        0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950,
+        0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
+        0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7,
+        0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
+        0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA,
+        0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
+        0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81,
+        0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
+        0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84,
+        0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
+        0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB,
+        0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
+        0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E,
+        0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
+        0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55,
+        0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
+        0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28,
+        0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
+        0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F,
+        0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
+        0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242,
+        0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
+        0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69,
+        0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
+        0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC,
+        0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
+        0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693,
+        0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
+        0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D};
+
+/*
+ * The following function is derived from a macro called updcrc from an article
+ * Copyright 1986 by Stephen Satchell.
+ *
+ * "Programmers may incorporate any or all code into their programs, giving
+ *  proper credit within the source. Publication of the source routines is
+ *  permitted so long as proper credit is given to Steven Satchell, Satchell
+ *  Evaluations, and Chuck Forsberg, Omen technology."
+ */
+uint32_t
+crc32(uint32_t accum, uint8_t delta)
+{
+    return lookup_table[(accum ^ delta) & 0xFF] ^ (accum >> 8);
+}
diff --git a/src/crc32.h b/src/crc32.h
new file mode 100644
index 0000000..b9267d0
--- /dev/null
+++ b/src/crc32.h
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2017 Limes Audio AB. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef CRC32_H
+#define CRC32_H
+
+#include <stdint.h>
+
+uint32_t
+crc32(uint32_t accum, uint8_t delta);
+
+#endif
diff --git a/src/device.h b/src/device.h
new file mode 100644
index 0000000..0563b1d
--- /dev/null
+++ b/src/device.h
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2017 Limes Audio AB. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef DEVICE_H
+#define DEVICE_H
+
+#define USB_VID 0x18D1
+#define USB_PID 0x8001
+
+#endif
diff --git a/src/diagnose.c b/src/diagnose.c
new file mode 100644
index 0000000..f6f8258
--- /dev/null
+++ b/src/diagnose.c
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2017 Limes Audio AB. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include "diagnose.h"
+#include "device.h"
+
+#include <argp.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <libudev.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/select.h>
+#include <unistd.h>
+
+struct arguments {
+    uint8_t report_id;
+    uint16_t diag_id;
+};
+
+struct hidraw_dev {
+    char *path;
+    char *serial;
+    struct hidraw_dev *next;
+};
+
+struct diag_command {
+    uint16_t id;
+    const char *str;
+};
+
+static struct diag_command commands[] = {{0x1230, "SERIAL_MASTER"},
+                                         {0x1231, "SERIAL_SLAVE_UP0"},
+                                         {0x1232, "SERIAL_SLAVE_UP1"},
+                                         {0x1233, "SERIAL_SLAVE_UP2"},
+                                         {0x1234, "SERIAL_SLAVE_UP3"},
+                                         {0x1235, "SERIAL_SLAVE_DOWN0"},
+                                         {0x1236, "SERIAL_SLAVE_DOWN1"},
+                                         {0x1237, "SERIAL_SLAVE_DOWN2"},
+                                         {0x1238, "SERIAL_SLAVE_DOWN3"},
+                                         {0x1300, "VERSION_MASTER"},
+                                         {0x1310, "VERSION_SLAVE_UP0"},
+                                         {0x1311, "VERSION_SLAVE_UP1"},
+                                         {0x1312, "VERSION_SLAVE_UP2"},
+                                         {0x1313, "VERSION_SLAVE_UP3"},
+                                         {0x1314, "VERSION_SLAVE_DOWN0"},
+                                         {0x1315, "VERSION_SLAVE_DOWN1"},
+                                         {0x1316, "VERSION_SLAVE_DOWN2"},
+                                         {0x1317, "VERSION_SLAVE_DOWN3"},
+                                         {0x1350, "DAISY_MIN_VERSION"},
+                                         {0x1351, "DAISY_NUM_SLAVES"},
+                                         {0x1500, "DAISY_DFU_STATUS"},
+                                         {0x1600, "STATE_MUTED"},
+                                         {0x1601, "STATE_HOOK_OFF"},
+                                         {0x1602, "STATE_VOLUME"},
+                                         {0x1603, "STATE_LED"},
+                                         {0x1604, "STATE_UPTIME"},
+                                         {0x1605, "STATE_SHARC_ALIVE"},
+                                         {0x5001, "SPI_QUEUE_STATUS"},
+                                         {0}};
+
+struct report {
+    uint8_t report_id;
+    uint16_t diag_id;
+    char *message;
+};
+
+static const size_t report_header_length = 3;
+static const size_t report_length = 64;
+
+static void
+report_pack(struct report *report, char *buf)
+{
+    buf[0] = report->report_id;
+    buf[1] = report->diag_id;
+    buf[2] = report->diag_id >> 8;
+}
+
+static void
+report_unpack(struct report *report, char *buf)
+{
+    report->report_id = buf[0];
+    report->diag_id = (buf[2] << 8) | buf[1];
+    report->message = &buf[3];
+}
+
+static int
+get_report(int fd, struct report *report)
+{
+    char buf[report_length];
+
+    report_pack(report, buf);
+    ssize_t retval = write(fd, buf, report_header_length);
+    if (retval < 0) {
+        return -1;
+    }
+
+    struct report read_report;
+    while (true) {
+        fd_set readset;
+        struct timeval tv;
+
+        do {
+            FD_ZERO(&readset);
+            FD_SET(fd, &readset);
+            tv.tv_sec = 0;
+            tv.tv_usec = 100000;
+            retval = select(fd + 1, &readset, NULL, NULL, &tv);
+            if ((retval <= 0) && (errno != EINTR)) {
+                return -1;
+            }
+        } while ((retval < 0) && (errno == EINTR));
+
+        retval = read(fd, buf, report_length);
+        if (retval < 0) {
+            return -1;
+        }
+
+        report_unpack(&read_report, buf);
+        if (read_report.diag_id == report->diag_id) {
+            break;
+        }
+        if (read_report.diag_id == 0x7FF0) {
+            // Unknown command response
+            break;
+        }
+    }
+
+    strncpy(report->message, read_report.message,
+            report_length - report_header_length);
+
+    return 0;
+}
+
+static int
+str_to_uint16(const char *str, uint16_t *val)
+{
+    int retval = sscanf(str, "%" SCNx16, val);
+    return (retval == EOF || retval < 0) ? -1 : 0;
+}
+
+static int
+get_hidraw_devices(struct hidraw_dev **head)
+{
+    *head = NULL;
+
+    struct udev *udev = udev_new();
+    if (!udev) {
+        return -1;
+    }
+    struct udev_enumerate *enumerate = udev_enumerate_new(udev);
+    if (!enumerate) {
+        return -1;
+    }
+    udev_enumerate_add_match_subsystem(enumerate, "hidraw");
+    udev_enumerate_scan_devices(enumerate);
+
+    struct hidraw_dev *current = NULL;
+    struct udev_list_entry *devices = udev_enumerate_get_list_entry(enumerate);
+    struct udev_list_entry *dev_list_entry;
+    udev_list_entry_foreach(dev_list_entry, devices)
+    {
+        const char *path = udev_list_entry_get_name(dev_list_entry);
+        struct udev_device *dev = udev_device_new_from_syspath(udev, path);
+        const char *dev_node_path = udev_device_get_devnode(dev);
+
+        struct udev_device *parent;
+        parent = udev_device_get_parent_with_subsystem_devtype(dev, "usb",
+                                                               "usb_device");
+        if (!parent) {
+            continue;
+        }
+
+        uint16_t idVendor;
+        const char *attr = udev_device_get_sysattr_value(parent, "idVendor");
+        int retval = str_to_uint16(attr, &idVendor);
+        if (retval < 0) {
+            goto unref;
+        }
+
+        uint16_t idProduct;
+        attr = udev_device_get_sysattr_value(parent, "idProduct");
+        retval = str_to_uint16(attr, &idProduct);
+        if (retval < 0) {
+            goto unref;
+        }
+
+        if ((idVendor != USB_VID) || (idProduct != USB_PID)) {
+            goto unref;
+        }
+
+        struct hidraw_dev *prev = current;
+        current = calloc(1, sizeof(*current));
+        if (*head == NULL) {
+            *head = current;
+        }
+
+        size_t dev_node_length = strlen(dev_node_path) + 1;
+        current->path = malloc(sizeof(*current->path) * dev_node_length);
+        strncpy(current->path, dev_node_path, dev_node_length);
+
+        const char *serial = udev_device_get_sysattr_value(parent, "serial");
+        size_t serial_length = strlen(serial) + 1;
+        current->serial = malloc(sizeof(*current->serial) * serial_length);
+        strncpy(current->serial, serial, serial_length);
+
+        if (prev) {
+            prev->next = current;
+        }
+
+    unref:
+        udev_device_unref(dev);
+    }
+
+    udev_enumerate_unref(enumerate);
+    udev_unref(udev);
+
+    return 0;
+}
+
+static void
+get_and_print_cmd(int fd, struct diag_command *cmd, struct report *report)
+{
+    const char *message = "Error reading report";
+    int retval = get_report(fd, report);
+    if (retval == 0) {
+        message = report->message;
+    }
+    printf("0x%04X %s: %s\n", cmd->id, cmd->str, message);
+}
+
+static error_t
+parser(int key, char *arg, struct argp_state *state)
+{
+    struct arguments *arguments = state->input;
+
+    switch (key) {
+        case 'd':
+            arguments->diag_id = (uint16_t)strtol(arg, NULL, 0);
+            break;
+
+        case 'r':
+            arguments->report_id = (uint8_t)strtol(arg, NULL, 0);
+            break;
+
+        default:
+            return ARGP_ERR_UNKNOWN;
+    }
+
+    return 0;
+}
+
+int
+diagnose(int argc, char **argv)
+{
+    char doc[] =
+            "\n"
+            "Options:";
+    struct argp_option options[] = {
+            {"diag", 'd', "id", 0, "Diagnostic ID", 0},
+            {"report", 'r', "id", 0, "HID report ID", 0},
+            {0}};
+    struct argp argp = {options, parser, NULL, doc, NULL, NULL, NULL};
+    struct arguments arguments = {
+            .report_id = 0x07, .diag_id = 0,
+    };
+
+    int retval = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, NULL, &arguments);
+    if (retval < 0) {
+        return -1;
+    }
+
+    struct hidraw_dev *devs;
+    get_hidraw_devices(&devs);
+
+    struct hidraw_dev *current = devs;
+    while (current) {
+        int fd = open(current->path, O_RDWR);
+        if (fd < 0) {
+            perror(current->path);
+            current = current->next;
+            continue;
+        }
+
+        printf("Diagnostics from %s\n", current->serial);
+
+        struct report report;
+        report.report_id = arguments.report_id;
+        char buf[report_length - report_header_length];
+        report.message = buf;
+        struct diag_command *cmd;
+        if (arguments.diag_id != 0) {
+            struct diag_command custom_cmd = {.id = arguments.diag_id,
+                                              .str = "UNKNOWN"};
+            for (cmd = commands; (*cmd).id; ++cmd) {
+                if (cmd->id == custom_cmd.id) {
+                    custom_cmd.str = cmd->str;
+                }
+            }
+            report.diag_id = custom_cmd.id;
+            get_and_print_cmd(fd, &custom_cmd, &report);
+        } else {
+            for (cmd = commands; (*cmd).id; ++cmd) {
+                report.diag_id = cmd->id;
+                get_and_print_cmd(fd, cmd, &report);
+            };
+        }
+
+        close(fd);
+
+        current = current->next;
+    }
+
+    current = devs;
+    while (current) {
+        struct hidraw_dev *next = current->next;
+        free(current->path);
+        free(current->serial);
+        free(current);
+        current = next;
+    }
+
+    return 0;
+}
diff --git a/src/diagnose.h b/src/diagnose.h
new file mode 100644
index 0000000..ee29fd7
--- /dev/null
+++ b/src/diagnose.h
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2017 Limes Audio AB. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef DIAGNOSE_H
+#define DIAGNOSE_H
+
+int
+diagnose(int argc, char **argv);
+
+#endif
diff --git a/src/list.c b/src/list.c
new file mode 100644
index 0000000..61d35fd
--- /dev/null
+++ b/src/list.c
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2017 Limes Audio AB. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include "list.h"
+#include "device.h"
+#include "util.h"
+
+#include <argp.h>
+#include <libusb-1.0/libusb.h>
+#include <stdio.h>
+#include <string.h>
+
+static void
+print_info(libusb_context *context)
+{
+    libusb_device **devices;
+
+    int length = printf("%-20s %s\n", "Serial number", "Release number");
+    for (int i = 0; i < (length - 1); ++i) {
+        putchar('-');
+    }
+    putchar('\n');
+
+    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 != USB_VID)
+            || (device_desc.idProduct != USB_PID)) {
+            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 == LIBUSB_ERROR_INVALID_PARAM) {
+            strncpy(serial, "<unknown>", string_length);
+        } else if (retval < 0) {
+            continue;
+        }
+
+        libusb_close(handle);
+
+        int major, minor, patch;
+        bcd_to_version(device_desc.bcdDevice, &major, &minor, &patch);
+
+        printf("%-20s %d.%d.%d\n", serial, major, minor, patch);
+    }
+}
+
+int
+list(int argc, char **argv)
+{
+    int retval = argp_parse(NULL, argc, argv, 0, NULL, NULL);
+    if (retval < 0) {
+        goto exit;
+    }
+
+    libusb_context *context;
+    retval = libusb_init(&context);
+    if (retval < 0) {
+        goto exit;
+    }
+
+    libusb_set_debug(NULL, LIBUSB_LOG_LEVEL_WARNING);
+    libusb_set_debug(context, LIBUSB_LOG_LEVEL_WARNING);
+
+    print_info(context);
+
+    libusb_exit(context);
+
+exit:
+    return retval;
+}
diff --git a/src/list.h b/src/list.h
new file mode 100644
index 0000000..689720b
--- /dev/null
+++ b/src/list.h
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2017 Limes Audio AB. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef LIST_H
+#define LIST_H
+
+int
+list(int argc, char **argv);
+
+#endif
diff --git a/src/upgrade.c b/src/upgrade.c
new file mode 100644
index 0000000..4e0297d
--- /dev/null
+++ b/src/upgrade.c
@@ -0,0 +1,773 @@
+/*
+ * 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;
+}
diff --git a/src/upgrade.h b/src/upgrade.h
new file mode 100644
index 0000000..7f19808
--- /dev/null
+++ b/src/upgrade.h
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2017 Limes Audio AB. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef UPGRADE_H
+#define UPGRADE_H
+
+int
+upgrade(int argc, char **argv);
+
+#endif
diff --git a/src/util.c b/src/util.c
new file mode 100644
index 0000000..839a1d1
--- /dev/null
+++ b/src/util.c
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2017 Limes Audio AB. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include "util.h"
+
+void
+bcd_to_version(uint16_t bcd, int *major, int *minor, int *patch)
+{
+    *major = (bcd >> 8) & 0xFF;
+    *minor = (bcd >> 4) & 0xF;
+    *patch = bcd & 0xF;
+}
diff --git a/src/util.h b/src/util.h
new file mode 100644
index 0000000..247ab2b
--- /dev/null
+++ b/src/util.h
@@ -0,0 +1,15 @@
+/*
+ * Copyright 2017 Limes Audio AB. All rights reserved.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#ifndef UTIL_H
+#define UTIL_H
+
+#include <stdint.h>
+
+void
+bcd_to_version(uint16_t bcd, int *major, int *minor, int *patch);
+
+#endif