//
// Copyright 2012-2013 Ettus Research LLC
// Copyright 2018-2019 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: GPL-3.0-or-later
//

#include "b200_iface.hpp"
#include <uhd/config.hpp>
#include <uhd/exception.hpp>
#include <uhd/utils/log.hpp>
#include <uhdlib/utils/ihex.hpp>
#include <libusb.h>
#include <stdint.h>
#include <boost/format.hpp>
#include <boost/functional/hash.hpp>

#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iomanip>
#include <string>
#include <thread>
#include <vector>

//! libusb_error_name is only in newer API
#ifndef HAVE_LIBUSB_ERROR_NAME
#    define libusb_error_name(code) str(boost::format("LIBUSB_ERROR_CODE %d") % code)
#endif

using namespace uhd;
using namespace uhd::transport;

static const bool load_img_msg = true;

const static uint8_t FX3_FIRMWARE_LOAD = 0xA0;

// 32 KB - 256 bytes for EEPROM storage
constexpr size_t BOOTLOADER_MAX_SIZE = 32512;

const static uint8_t VRT_VENDOR_OUT = static_cast<uint8_t>(LIBUSB_REQUEST_TYPE_VENDOR)
                                      | static_cast<uint8_t>(LIBUSB_ENDPOINT_OUT);
const static uint8_t VRT_VENDOR_IN = static_cast<uint8_t>(LIBUSB_REQUEST_TYPE_VENDOR)
                                     | static_cast<uint8_t>(LIBUSB_ENDPOINT_IN);
const static uint8_t B200_VREQ_FPGA_START    = 0x02;
const static uint8_t B200_VREQ_FPGA_DATA     = 0x12;
const static uint8_t B200_VREQ_GET_COMPAT    = 0x15;
const static uint8_t B200_VREQ_SET_FPGA_HASH = 0x1C;
const static uint8_t B200_VREQ_GET_FPGA_HASH = 0x1D;
const static uint8_t B200_VREQ_SET_FW_HASH   = 0x1E;
const static uint8_t B200_VREQ_GET_FW_HASH   = 0x1F;
const static uint8_t B200_VREQ_LOOP          = 0x22;
const static uint8_t B200_VREQ_FPGA_CONFIG   = 0x55;
// const static uint8_t B200_VREQ_FPGA_RESET = 0x62;
const static uint8_t B200_VREQ_GPIF_RESET   = 0x72;
const static uint8_t B200_VREQ_GET_USB      = 0x80;
const static uint8_t B200_VREQ_GET_STATUS   = 0x83;
const static uint8_t B200_VREQ_FX3_RESET    = 0x99;
const static uint8_t B200_VREQ_EEPROM_WRITE = 0xBA;
const static uint8_t B200_VREQ_EEPROM_READ  = 0xBB;

const static uint8_t FX3_STATE_UNDEFINED        = 0x00;
const static uint8_t FX3_STATE_FPGA_READY       = 0x01;
const static uint8_t FX3_STATE_CONFIGURING_FPGA = 0x02;
const static uint8_t FX3_STATE_BUSY             = 0x03;
const static uint8_t FX3_STATE_RUNNING          = 0x04;
const static uint8_t FX3_STATE_UNCONFIGURED     = 0x05;
const static uint8_t FX3_STATE_ERROR            = 0x06;

const static int VREQ_MAX_SIZE_USB2 = 64;
const static int VREQ_MAX_SIZE_USB3 = 512;
const static int VREQ_DEFAULT_SIZE  = VREQ_MAX_SIZE_USB2;
const static int VREQ_MAX_SIZE      = VREQ_MAX_SIZE_USB3;

typedef uint32_t hash_type;


/***********************************************************************
 * Helper Functions
 **********************************************************************/
/*!
 * Create a file hash
 * The hash will be used to identify the loaded fpga image
 * \param filename file used to generate hash value
 * \return hash value in a uint32_t type
 */
static hash_type generate_hash(const char* filename)
{
    if (filename == NULL)
        return hash_type(0);

    std::ifstream file(filename);
    if (not file) {
        throw uhd::io_error(std::string("cannot open input file ") + filename);
    }

    hash_type hash = 0;

    char ch;
    long long count = 0;
    while (file.get(ch)) {
        count++;
        // hash algorithm derived from boost hash_combine
        // http://www.boost.org/doc/libs/1_35_0/doc/html/boost/hash_combine_id241013.html
        hash ^= ch + 0x9e3779b9 + (hash << 6) + (hash >> 2);
    }

    if (count == 0) {
        throw uhd::io_error(std::string("empty input file ") + filename);
    }

    if (not file.eof()) {
        throw uhd::io_error(std::string("file error ") + filename);
    }

    file.close();
    return hash_type(hash);
}


/***********************************************************************
 * The implementation class
 **********************************************************************/
class b200_iface_impl : public b200_iface
{
public:
    b200_iface_impl(usb_control::sptr usb_ctrl) : _usb_ctrl(usb_ctrl)
    {
        // NOP
    }

    int fx3_control_write(uint8_t request,
        uint16_t value,
        uint16_t index,
        unsigned char* buff,
        uint16_t length,
        uint32_t timeout = 0)
    {
        return _usb_ctrl->submit(VRT_VENDOR_OUT, // bmReqeustType
            request, // bRequest
            value, // wValue
            index, // wIndex
            buff, // data
            length, // wLength
            timeout); // timeout
    }

    int fx3_control_read(uint8_t request,
        uint16_t value,
        uint16_t index,
        unsigned char* buff,
        uint16_t length,
        uint32_t timeout = 0)
    {
        return _usb_ctrl->submit(VRT_VENDOR_IN, // bmReqeustType
            request, // bRequest
            value, // wValue
            index, // wIndex
            buff, // data
            length, // wLength
            timeout); // timeout
    }

    void write_i2c(
        UHD_UNUSED(uint16_t addr), UHD_UNUSED(const byte_vector_t& bytes)) override
    {
        throw uhd::not_implemented_error("b200 write i2c");
    }


    byte_vector_t read_i2c(
        UHD_UNUSED(uint16_t addr), UHD_UNUSED(size_t num_bytes)) override
    {
        throw uhd::not_implemented_error("b200 read i2c");
    }

    void write_eeprom(uint16_t addr, uint16_t offset, const byte_vector_t& bytes) override
    {
        int ret = fx3_control_write(B200_VREQ_EEPROM_WRITE,
            0,
            offset | (uint16_t(addr) << 8),
            (unsigned char*)&bytes[0],
            bytes.size());

        if (ret < 0)
            throw uhd::io_error((boost::format("Failed to write EEPROM (%d: %s)") % ret
                                 % libusb_error_name(ret))
                                    .str());
        else if ((size_t)ret != bytes.size())
            throw uhd::io_error((
                boost::format("Short write on write EEPROM (expecting: %d, returned: %d)")
                % bytes.size() % ret)
                                    .str());
    }

    byte_vector_t read_eeprom(uint16_t addr, uint16_t offset, size_t num_bytes) override
    {
        byte_vector_t recv_bytes(num_bytes);
        int bytes_read = fx3_control_read(B200_VREQ_EEPROM_READ,
            0,
            offset | (uint16_t(addr) << 8),
            (unsigned char*)&recv_bytes[0],
            num_bytes);

        if (bytes_read < 0)
            throw uhd::io_error((boost::format("Failed to read EEPROM (%d: %s)")
                                 % bytes_read % libusb_error_name(bytes_read))
                                    .str());
        else if ((size_t)bytes_read != num_bytes)
            throw uhd::io_error(
                (boost::format("Short read on read EEPROM (expecting: %d, returned: %d)")
                    % num_bytes % bytes_read)
                    .str());

        return recv_bytes;
    }

    void load_firmware(
        const std::string filestring, UHD_UNUSED(bool force) = false) override
    {
        if (load_img_msg) {
            UHD_LOGGER_INFO("B200") << "Loading firmware image: " << filestring << "...";
        }

        ihex_reader file_reader(filestring);
        try {
            file_reader.read(std::bind(&b200_iface_impl::fx3_control_write,
                this,
                FX3_FIRMWARE_LOAD,
                std::placeholders::_1,
                std::placeholders::_2,
                std::placeholders::_3,
                std::placeholders::_4,
                0));
        } catch (const uhd::io_error& e) {
            throw uhd::io_error(
                str(boost::format("Could not load firmware: \n%s") % e.what()));
        }


        // TODO
        // usrp_set_firmware_hash(hash); //set hash before reset

        /* Success! Let the system settle. */
        // TODO: Replace this with a polling loop in the FX3, or find out
        // what the actual, correct timeout value is.
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    }

    void reset_fx3(void) override
    {
        unsigned char data[4];
        memset(data, 0x00, sizeof(data));
        const int bytes_to_send = sizeof(data);

        int ret = fx3_control_write(B200_VREQ_FX3_RESET, 0x00, 0x00, data, bytes_to_send);
        if (ret < 0)
            throw uhd::io_error((boost::format("Failed to reset FX3 (%d: %s)") % ret
                                 % libusb_error_name(ret))
                                    .str());
        else if (ret != bytes_to_send)
            throw uhd::io_error(
                (boost::format("Short write on reset FX3 (expecting: %d, returned: %d)")
                    % bytes_to_send % ret)
                    .str());
    }

    void reset_gpif(void) override
    {
        unsigned char data[4];
        memset(data, 0x00, sizeof(data));
        const int bytes_to_send = sizeof(data);

        int ret =
            fx3_control_write(B200_VREQ_GPIF_RESET, 0x00, 0x00, data, bytes_to_send);
        if (ret < 0)
            throw uhd::io_error((boost::format("Failed to reset GPIF (%d: %s)") % ret
                                 % libusb_error_name(ret))
                                    .str());
        else if (ret != bytes_to_send)
            throw uhd::io_error(
                (boost::format("Short write on reset GPIF (expecting: %d, returned: %d)")
                    % bytes_to_send % ret)
                    .str());
    }

    void set_fpga_reset_pin(const bool reset) override
    {
        unsigned char data[4];
        memset(data, (reset) ? 0xFF : 0x00, sizeof(data));

        UHD_THROW_INVALID_CODE_PATH();

        // Below is dead code as long as UHD_THROW_INVALID_CODE_PATH(); is declared above.
        // It is preserved here in a comment in case it is needed later:
        /*
        const int bytes_to_send = sizeof(data);

        int ret = fx3_control_write(B200_VREQ_FPGA_RESET, 0x00, 0x00, data,
        bytes_to_send); if (ret < 0) throw uhd::io_error((boost::format("Failed to reset
        FPGA (%d: %s)") % ret % libusb_error_name(ret)).str()); else if (ret !=
        bytes_to_send) throw uhd::io_error((boost::format("Short write on reset FPGA
        (expecting: %d, returned: %d)") % bytes_to_send % ret).str());
        */
    }

    uint8_t get_usb_speed(void) override
    {
        unsigned char rx_data[1];
        memset(rx_data, 0x00, sizeof(rx_data));
        const int bytes_to_recv = sizeof(rx_data);

        int ret = fx3_control_read(B200_VREQ_GET_USB, 0x00, 0x00, rx_data, bytes_to_recv);
        if (ret < 0)
            throw uhd::io_error((boost::format("Failed to get USB speed (%d: %s)") % ret
                                 % libusb_error_name(ret))
                                    .str());
        else if (ret != bytes_to_recv)
            throw uhd::io_error((
                boost::format("Short read on get USB speed (expecting: %d, returned: %d)")
                % bytes_to_recv % ret)
                                    .str());

        return static_cast<uint8_t>(rx_data[0]);
    }

    uint8_t get_fx3_status(void) override
    {
        unsigned char rx_data[1];
        memset(rx_data, 0x00, sizeof(rx_data));
        const int bytes_to_recv = sizeof(rx_data);

        int ret =
            fx3_control_read(B200_VREQ_GET_STATUS, 0x00, 0x00, rx_data, bytes_to_recv);
        if (ret < 0)
            throw uhd::io_error((boost::format("Failed to get FX3 status (%d: %s)") % ret
                                 % libusb_error_name(ret))
                                    .str());
        else if (ret != bytes_to_recv)
            throw uhd::io_error(
                (boost::format(
                     "Short read on get FX3 status (expecting: %d, returned: %d)")
                    % bytes_to_recv % ret)
                    .str());

        return static_cast<uint8_t>(rx_data[0]);
    }

    uint16_t get_compat_num(void) override
    {
        unsigned char rx_data[2];
        memset(rx_data, 0x00, sizeof(rx_data));
        const int bytes_to_recv = sizeof(rx_data);

        int ret =
            fx3_control_read(B200_VREQ_GET_COMPAT, 0x00, 0x00, rx_data, bytes_to_recv);
        if (ret < 0)
            throw uhd::io_error((boost::format("Failed to get compat num (%d: %s)") % ret
                                 % libusb_error_name(ret))
                                    .str());
        else if (ret != bytes_to_recv)
            throw uhd::io_error(
                (boost::format(
                     "Short read on get compat num (expecting: %d, returned: %d)")
                    % bytes_to_recv % ret)
                    .str());

        return (((uint16_t)rx_data[0]) << 8) | rx_data[1];
    }

    void usrp_get_firmware_hash(hash_type& hash)
    {
        const int bytes_to_recv = 4;
        if (sizeof(hash_type) != bytes_to_recv)
            throw uhd::type_error(
                (boost::format("hash_type is %d bytes but transfer length is %d bytes")
                    % sizeof(hash_type) % bytes_to_recv)
                    .str());

        int ret = fx3_control_read(
            B200_VREQ_GET_FW_HASH, 0x00, 0x00, (unsigned char*)&hash, bytes_to_recv, 500);
        if (ret < 0)
            throw uhd::io_error((boost::format("Failed to get firmware hash (%d: %s)")
                                 % ret % libusb_error_name(ret))
                                    .str());
        else if (ret != bytes_to_recv)
            throw uhd::io_error(
                (boost::format(
                     "Short read on get firmware hash (expecting: %d, returned: %d)")
                    % bytes_to_recv % ret)
                    .str());
    }

    void usrp_set_firmware_hash(hash_type hash)
    {
        const int bytes_to_send = 4;
        if (sizeof(hash_type) != bytes_to_send)
            throw uhd::type_error(
                (boost::format("hash_type is %d bytes but transfer length is %d bytes")
                    % sizeof(hash_type) % bytes_to_send)
                    .str());

        int ret = fx3_control_write(
            B200_VREQ_SET_FW_HASH, 0x00, 0x00, (unsigned char*)&hash, bytes_to_send);
        if (ret < 0)
            throw uhd::io_error((boost::format("Failed to set firmware hash (%d: %s)")
                                 % ret % libusb_error_name(ret))
                                    .str());
        else if (ret != bytes_to_send)
            throw uhd::io_error(
                (boost::format(
                     "Short write on set firmware hash (expecting: %d, returned: %d)")
                    % bytes_to_send % ret)
                    .str());
    }

    void usrp_get_fpga_hash(hash_type& hash)
    {
        const int bytes_to_recv = 4;
        if (sizeof(hash_type) != bytes_to_recv)
            throw uhd::type_error(
                (boost::format("hash_type is %d bytes but transfer length is %d bytes")
                    % sizeof(hash_type) % bytes_to_recv)
                    .str());

        int ret = fx3_control_read(B200_VREQ_GET_FPGA_HASH,
            0x00,
            0x00,
            (unsigned char*)&hash,
            bytes_to_recv,
            500);
        if (ret < 0)
            throw uhd::io_error((boost::format("Failed to get FPGA hash (%d: %s)") % ret
                                 % libusb_error_name(ret))
                                    .str());
        else if (ret != bytes_to_recv)
            throw uhd::io_error((
                boost::format("Short read on get FPGA hash (expecting: %d, returned: %d)")
                % bytes_to_recv % ret)
                                    .str());
    }

    void usrp_set_fpga_hash(hash_type hash)
    {
        const int bytes_to_send = 4;
        if (sizeof(hash_type) != bytes_to_send)
            throw uhd::type_error(
                (boost::format("hash_type is %d bytes but transfer length is %d bytes")
                    % sizeof(hash_type) % bytes_to_send)
                    .str());

        int ret = fx3_control_write(
            B200_VREQ_SET_FPGA_HASH, 0x00, 0x00, (unsigned char*)&hash, bytes_to_send);
        if (ret < 0)
            throw uhd::io_error((boost::format("Failed to set FPGA hash (%d: %s)") % ret
                                 % libusb_error_name(ret))
                                    .str());
        else if (ret != bytes_to_send)
            throw uhd::io_error(
                (boost::format(
                     "Short write on set FPGA hash (expecting: %d, returned: %d)")
                    % bytes_to_send % ret)
                    .str());
    }

    // Establish default largest possible control request transfer size based on operating
    // USB speed
    int _get_transfer_size()
    {
        switch (get_usb_speed()) {
            case 2:
                return VREQ_DEFAULT_SIZE;
            case 3:
                return VREQ_MAX_SIZE_USB3;
            default:
                throw uhd::io_error(
                    "load_fpga: get_usb_speed returned invalid USB speed (not 2 or 3).");
        }
    }

    size_t _get_file_size(const char* filename)
    {
        std::filesystem::path path(filename);
        auto filesize = std::filesystem::file_size(path);
        return static_cast<size_t>(filesize);
    }

    uint32_t load_fpga(const std::string filestring, bool force) override
    {
        uint8_t fx3_state = 0;
        uint32_t wait_count;
        int ret           = 0;
        int bytes_to_xfer = 0;

        const char* filename = filestring.c_str();

        hash_type hash = generate_hash(filename);
        hash_type loaded_hash;
        usrp_get_fpga_hash(loaded_hash);
        if (hash == loaded_hash and !force)
            return 0;

        const int transfer_size = _get_transfer_size();

        UHD_ASSERT_THROW(transfer_size <= VREQ_MAX_SIZE);

        unsigned char out_buff[VREQ_MAX_SIZE];

        // Request loopback read, which will indicate the firmware's current control
        // request buffer size
        int nread = fx3_control_read(B200_VREQ_LOOP, 0, 0, out_buff, transfer_size, 1000);
        if (nread < 0)
            throw uhd::io_error(
                (boost::format(
                     "load_fpga: unable to complete firmware loopback request (%d: %s)")
                    % nread % libusb_error_name(nread))
                    .str());
        else if (nread != transfer_size)
            throw uhd::io_error(
                (boost::format("load_fpga: short read on firmware loopback request "
                               "(expecting: %d, returned: %d)")
                    % transfer_size % nread)
                    .str());

        const size_t file_size = _get_file_size(filename);

        std::ifstream file;
        file.open(filename, std::ios::in | std::ios::binary);

        if (!file.good()) {
            throw uhd::io_error("load_fpga: cannot open FPGA input file.");
        }

        // Zero the hash, in case we abort programming another image and revert to the
        // previously programmed image
        usrp_set_fpga_hash(0);

        memset(out_buff, 0x00, sizeof(out_buff));
        bytes_to_xfer = 1;
        ret =
            fx3_control_write(B200_VREQ_FPGA_CONFIG, 0, 0, out_buff, bytes_to_xfer, 1000);
        if (ret < 0)
            throw uhd::io_error((boost::format("Failed to start FPGA config (%d: %s)")
                                 % ret % libusb_error_name(ret))
                                    .str());
        else if (ret != bytes_to_xfer)
            throw uhd::io_error(
                (boost::format(
                     "Short write on start FPGA config (expecting: %d, returned: %d)")
                    % bytes_to_xfer % ret)
                    .str());

        wait_count = 0;
        do {
            fx3_state = get_fx3_status();

            if ((wait_count >= 500) || (fx3_state == FX3_STATE_ERROR)
                || (fx3_state == FX3_STATE_UNDEFINED)) {
                return fx3_state;
            }

            std::this_thread::sleep_for(std::chrono::milliseconds(10));

            wait_count++;
        } while (fx3_state != FX3_STATE_FPGA_READY);

        if (load_img_msg) {
            UHD_LOGGER_INFO("B200") << "Loading FPGA image: " << filestring << "...";
        }

        bytes_to_xfer = 1;
        ret =
            fx3_control_write(B200_VREQ_FPGA_START, 0, 0, out_buff, bytes_to_xfer, 1000);
        if (ret < 0)
            throw uhd::io_error((boost::format("Failed to start FPGA bitstream (%d: %s)")
                                 % ret % libusb_error_name(ret))
                                    .str());
        else if (ret != bytes_to_xfer)
            throw uhd::io_error(
                (boost::format(
                     "Short write on start FPGA bitstream (expecting: %d, returned: %d)")
                    % bytes_to_xfer % ret)
                    .str());

        wait_count = 0;
        do {
            fx3_state = get_fx3_status();

            if ((wait_count >= 1000) || (fx3_state == FX3_STATE_ERROR)
                || (fx3_state == FX3_STATE_UNDEFINED)) {
                return fx3_state;
            }

            std::this_thread::sleep_for(std::chrono::milliseconds(10));

            wait_count++;
        } while (fx3_state != FX3_STATE_CONFIGURING_FPGA);

        size_t bytes_sent = 0;
        while (!file.eof()) {
            file.read((char*)out_buff, transfer_size);
            const std::streamsize n = file.gcount();
            if (n == 0)
                continue;

            uint16_t transfer_count = uint16_t(n);

            /* Send the data to the device. */
            int nwritten = fx3_control_write(
                B200_VREQ_FPGA_DATA, 0, 0, out_buff, transfer_count, 5000);
            if (nwritten < 0)
                throw uhd::io_error(
                    (boost::format("load_fpga: cannot write bitstream to FX3 (%d: %s)")
                        % nwritten % libusb_error_name(nwritten))
                        .str());
            else if (nwritten != transfer_count)
                throw uhd::io_error(
                    (boost::format("load_fpga: short write while transferring bitstream "
                                   "to FX3  (expecting: %d, returned: %d)")
                        % transfer_count % nwritten)
                        .str());

            const size_t LOG_GRANULARITY = 10; // %. Keep this an integer divisor of 100.
            if (load_img_msg) {
                if (bytes_sent == 0)
                    UHD_LOGGER_DEBUG("B200") << "FPGA load:   0%" << std::flush;
                const size_t percent_before =
                    size_t((bytes_sent * 100) / file_size)
                    - (size_t((bytes_sent * 100) / file_size) % LOG_GRANULARITY);
                bytes_sent += transfer_count;
                const size_t percent_after =
                    size_t((bytes_sent * 100) / file_size)
                    - (size_t((bytes_sent * 100) / file_size) % LOG_GRANULARITY);
                if (percent_before != percent_after) {
                    UHD_LOGGER_DEBUG("B200")
                        << "FPGA load: " << std::setw(3) << percent_after << "%";
                }
            }
        }

        file.close();

        wait_count = 0;
        do {
            fx3_state = get_fx3_status();

            if ((wait_count >= 500) || (fx3_state == FX3_STATE_ERROR)
                || (fx3_state == FX3_STATE_UNDEFINED)) {
                return fx3_state;
            }

            std::this_thread::sleep_for(std::chrono::milliseconds(10));

            wait_count++;
        } while (fx3_state != FX3_STATE_RUNNING);

        usrp_set_fpga_hash(hash);

        if (load_img_msg) {
            UHD_LOGGER_DEBUG("B200") << "FPGA image loaded!";
        }

        return 0;
    }

    uint32_t load_bootloader(const std::string filestring) override
    {
        // open bootloader file
        const char* filename = filestring.c_str();

        const size_t file_size = _get_file_size(filename);

        if (file_size > BOOTLOADER_MAX_SIZE) {
            throw uhd::runtime_error(
                (boost::format("Bootloader img file is too large for EEPROM! (expecting: "
                               "less than %d actual: %d")
                    % BOOTLOADER_MAX_SIZE % file_size)
                    .str());
        }
        std::ifstream file;
        file.open(filename, std::ios::in | std::ios::binary);

        if (!file.good()) {
            throw uhd::io_error("load_bootloader: cannot open bootloader input file.");
        }

        // allocate buffer
        const int transfer_size = _get_transfer_size();
        UHD_ASSERT_THROW(transfer_size <= VREQ_MAX_SIZE);
        std::vector<uint8_t> out_buff(transfer_size);

        // Request loopback read, which will indicate the firmware's current control
        // request buffer size
        int nread =
            fx3_control_read(B200_VREQ_LOOP, 0, 0, out_buff.data(), transfer_size, 1000);
        if (nread < 0) {
            throw uhd::io_error((boost::format("load_bootloader: unable to complete "
                                               "firmware loopback request (%d: %s)")
                                 % nread % libusb_error_name(nread))
                                    .str());
        } else if (nread != transfer_size) {
            throw uhd::io_error(
                (boost::format("load_bootloader: short read on firmware loopback request "
                               "(expecting: %d, returned: %d)")
                    % transfer_size % nread)
                    .str());
        }
        // ensure FX3 is in non-error state
        {
            uint8_t fx3_state = get_fx3_status();

            if (fx3_state == FX3_STATE_ERROR or fx3_state == FX3_STATE_UNDEFINED) {
                return fx3_state;
            }
        }

        UHD_LOGGER_INFO("B200") << "Loading bootloader image: " << filestring << "...";

        size_t bytes_sent = 0;
        while (!file.eof()) {
            file.read((char*)&out_buff[0], transfer_size);
            const std::streamsize n = file.gcount();
            if (n == 0)
                continue;

            uint16_t transfer_count = uint16_t(n);

            // Send the data to the device
            int nwritten = fx3_control_write(B200_VREQ_EEPROM_WRITE,
                0,
                bytes_sent,
                out_buff.data(),
                transfer_count,
                5000);
            if (nwritten < 0) {
                throw uhd::io_error(
                    (boost::format(
                         "load_bootloader: cannot write bitstream to FX3 (%d: %s)")
                        % nwritten % libusb_error_name(nwritten))
                        .str());
            } else if (nwritten != transfer_count) {
                throw uhd::io_error(
                    (boost::format(
                         "load_bootloader: short write while transferring bitstream "
                         "to FX3  (expecting: %d, returned: %d)")
                        % transfer_count % nwritten)
                        .str());
            }

            const size_t LOG_GRANULARITY = 10; // %. Keep this an integer divisor of 100.

            if (bytes_sent == 0)
                UHD_LOGGER_DEBUG("B200") << "Bootloader load:   0%" << std::flush;
            const size_t percent_before =
                size_t((bytes_sent * 100) / file_size)
                - (size_t((bytes_sent * 100) / file_size) % LOG_GRANULARITY);
            bytes_sent += transfer_count;
            const size_t percent_after =
                size_t((bytes_sent * 100) / file_size)
                - (size_t((bytes_sent * 100) / file_size) % LOG_GRANULARITY);
            if (percent_before != percent_after) {
                UHD_LOGGER_DEBUG("B200")
                    << "Bootloader load: " << std::setw(3) << percent_after << "%";
            }
        }

        file.close();

        // ensure FX3 is in non-error state
        {
            uint8_t fx3_state = get_fx3_status();

            if (fx3_state == FX3_STATE_ERROR or fx3_state == FX3_STATE_UNDEFINED) {
                return fx3_state;
            }
        }

        UHD_LOGGER_DEBUG("B200") << "Bootloader image loaded!";

        return 0;
    }

private:
    usb_control::sptr _usb_ctrl;
};


std::string b200_iface::fx3_state_string(uint8_t state)
{
    switch (state) {
        case FX3_STATE_FPGA_READY:
            return std::string("Ready");
        case FX3_STATE_CONFIGURING_FPGA:
            return std::string("Configuring FPGA");
        case FX3_STATE_BUSY:
            return std::string("Busy");
        case FX3_STATE_RUNNING:
            return std::string("Running");
        case FX3_STATE_UNCONFIGURED:
            return std::string("Unconfigured");
        case FX3_STATE_ERROR:
            return std::string("Error");
        default:
            break;
    }
    return std::string("Unknown");
}

/***********************************************************************
 * Make an instance of the implementation
 **********************************************************************/
b200_iface::sptr b200_iface::make(usb_control::sptr usb_ctrl)
{
    return sptr(new b200_iface_impl(usb_ctrl));
}
