"use strict";
/*
 * This work is heavily based on https://github.com/raspberrypi/usbboot
 * Copyright 2016 Raspberry Pi Foundation
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.UsbbootScanner = exports.CM4 = exports.CM3 = exports.UsbbootDevice = exports.isUsbBootCapableUSBDevice = void 0;
// tslint:disable:no-bitwise
const usb_1 = require("usb");
const endpoint_1 = require("usb/dist/usb/endpoint");
const _debug = require("debug");
const events_1 = require("events");
const promises_1 = require("fs/promises");
const Path = require("path");
async function delay(ms) {
    await new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}
function fromCallback(fn) {
    return new Promise((resolve, reject) => {
        fn((error, result) => {
            if (error == null) {
                resolve(result);
            }
            else {
                reject(error);
            }
        });
    });
}
const debug = _debug('node-raspberrypi-usbboot');
const POLLING_INTERVAL_MS = 2000;
const TRANSFER_BLOCK_SIZE = 1024 ** 2;
// The equivalent of a NULL buffer, given that node-usb complains
// if the data argument is not an instance of Buffer
const NULL_BUFFER = Buffer.alloc(0);
/**
 * @summary The size of the boot message bootcode length section
 */
const BOOT_MESSAGE_BOOTCODE_LENGTH_SIZE = 4;
/**
 * @summary The offset of the boot message bootcode length section
 */
const BOOT_MESSAGE_BOOTCODE_LENGTH_OFFSET = 0;
/**
 * @summary The size of the boot message signature section
 */
const BOOT_MESSAGE_SIGNATURE_SIZE = 20;
/**
 * @summary The offset of the file message command section
 */
const FILE_MESSAGE_COMMAND_OFFSET = 0;
/**
 * @summary The size of the file message command section
 */
const FILE_MESSAGE_COMMAND_SIZE = 4;
/**
 * @summary The offset of the file message file name section
 */
const FILE_MESSAGE_FILE_NAME_OFFSET = FILE_MESSAGE_COMMAND_SIZE;
/**
 * @summary The size of the file message file name section
 */
const FILE_MESSAGE_FILE_NAME_SIZE = 256;
/**
 * @summary The GET_STATUS usb control transfer request code
 * @description
 * See http://www.jungo.com/st/support/documentation/windriver/811/wdusb_man_mhtml/node55.html#usb_standard_dev_req_codes
 */
const USB_REQUEST_CODE_GET_STATUS = 0;
/**
 * @summary The maximum buffer length of a usbboot message
 */
const USBBOOT_MESSAGE_MAX_BUFFER_LENGTH = 0xffff;
/**
 * @summary The amount of bits to shift to the right on a control transfer index
 */
const CONTROL_TRANSFER_INDEX_RIGHT_BIT_SHIFT = 16;
/**
 * @summary The size of the usbboot file message
 */
const FILE_MESSAGE_SIZE = FILE_MESSAGE_COMMAND_SIZE + FILE_MESSAGE_FILE_NAME_SIZE;
/**
 * @summary The usbboot return code that represents success
 */
const RETURN_CODE_SUCCESS = 0;
/**
 * @summary The buffer length of the return code message
 */
const RETURN_CODE_LENGTH = 4;
/**
 * @summary The timeout for USB control transfers, in milliseconds
 */
const USB_CONTROL_TRANSFER_TIMEOUT_MS = 10000;
/**
 * @summary The timeout for USB bulk transfers, in milliseconds
 */
const USB_BULK_TRANSFER_TIMEOUT_MS = 10000;
const USB_ENDPOINT_INTERFACES_SOC_BCM2835 = 1;
const USB_VENDOR_ID_BROADCOM_CORPORATION = 0x0a5c;
const USB_PRODUCT_ID_BCM2708_BOOT = 0x2763;
const USB_PRODUCT_ID_BCM2710_BOOT = 0x2764;
const USB_PRODUCT_ID_BCM2711_BOOT = 0x2711;
const USB_PRODUCT_ID_BCM2711_MASS_STORAGE = 1;
// When the pi reboots in mass storage mode, it has this product id
const USB_VENDOR_ID_NETCHIP_TECHNOLOGY = 0x0525;
const USB_PRODUCT_ID_POCKETBOOK_PRO_903 = 0xa4a5;
// Delay in ms after which we consider that the device was unplugged (not resetted)
const DEVICE_UNPLUG_TIMEOUT = 5000;
// Delay (in ms) to wait when epRead throws an error
const READ_ERROR_DELAY = 100;
var FileMessageCommand;
(function (FileMessageCommand) {
    FileMessageCommand[FileMessageCommand["GetFileSize"] = 0] = "GetFileSize";
    FileMessageCommand[FileMessageCommand["ReadFile"] = 1] = "ReadFile";
    FileMessageCommand[FileMessageCommand["Done"] = 2] = "Done";
})(FileMessageCommand || (FileMessageCommand = {}));
const getCommand = (fileMessageBuffer) => {
    const command = fileMessageBuffer.readInt32LE(FILE_MESSAGE_COMMAND_OFFSET);
    if (!(command in FileMessageCommand)) {
        throw new Error(`Invalid file message command code: ${command}`);
    }
    return command;
};
const getFilename = (fileMessageBuffer) => {
    // The parsed string will likely contain tons of trailing
    // null bytes that we should get rid of for convenience
    let end = fileMessageBuffer.indexOf(0, FILE_MESSAGE_FILE_NAME_OFFSET);
    if (end === -1) {
        end = fileMessageBuffer.length;
    }
    return fileMessageBuffer
        .slice(FILE_MESSAGE_FILE_NAME_OFFSET, end)
        .toString('ascii');
};
/**
 * @summary Parse a file message buffer from a device
 */
const parseFileMessageBuffer = (fileMessageBuffer) => {
    let command = getCommand(fileMessageBuffer);
    const filename = getFilename(fileMessageBuffer);
    // A blank file name can also mean "done"
    if (filename === '') {
        command = FileMessageCommand.Done;
    }
    return { command, filename };
};
/**
 * @summary Perform a USB control transfer
 * @description
 * See http://libusb.sourceforge.net/api-1.0/group__syncio.html
 */
const performControlTransfer = async (device, bmRequestType, bRequest, wValue, wIndex, dataOrLength) => {
    const previousTimeout = device.timeout;
    device.timeout = USB_CONTROL_TRANSFER_TIMEOUT_MS;
    const result = await fromCallback((callback) => {
        device.controlTransfer(bmRequestType, bRequest, wValue, wIndex, dataOrLength, callback);
    });
    device.timeout = previousTimeout;
    return result;
};
const isUsbBootCapableUSBDevice = (idVendor, idProduct) => {
    return (idVendor === USB_VENDOR_ID_BROADCOM_CORPORATION &&
        (idProduct === USB_PRODUCT_ID_BCM2708_BOOT ||
            idProduct === USB_PRODUCT_ID_BCM2710_BOOT ||
            idProduct === USB_PRODUCT_ID_BCM2711_BOOT));
};
exports.isUsbBootCapableUSBDevice = isUsbBootCapableUSBDevice;
const isUsbBootCapableUSBDevice$ = (device) => {
    return (0, exports.isUsbBootCapableUSBDevice)(device.deviceDescriptor.idVendor, device.deviceDescriptor.idProduct);
};
const isRaspberryPiInMassStorageMode = (device) => {
    return (device.deviceDescriptor.idVendor === USB_VENDOR_ID_NETCHIP_TECHNOLOGY &&
        device.deviceDescriptor.idProduct === USB_PRODUCT_ID_POCKETBOOK_PRO_903);
};
const isComputeModule4InMassStorageMode = (device) => {
    return (device.deviceDescriptor.idVendor === USB_VENDOR_ID_BROADCOM_CORPORATION &&
        device.deviceDescriptor.idProduct === USB_PRODUCT_ID_BCM2711_MASS_STORAGE);
};
const initializeDevice = (device) => {
    var _a;
    // interface is a reserved keyword in TypeScript so we use iface
    device.open();
    // Handle 2837 where it can start with two interfaces, the first is mass storage
    // the second is the vendor interface for programming
    let interfaceNumber;
    let endpointNumber;
    if (((_a = device.configDescriptor) === null || _a === void 0 ? void 0 : _a.bNumInterfaces) ===
        USB_ENDPOINT_INTERFACES_SOC_BCM2835) {
        interfaceNumber = 0;
        endpointNumber = 1;
    }
    else {
        interfaceNumber = 1;
        endpointNumber = 3;
    }
    const iface = device.interface(interfaceNumber);
    iface.claim();
    const endpoint = iface.endpoint(endpointNumber);
    if (!(endpoint instanceof endpoint_1.OutEndpoint)) {
        throw new Error('endpoint is not an usb.OutEndpoint');
    }
    debug('Initialized device correctly', devicePortId(device));
    return { iface, endpoint };
};
const sendSize = async (device, size) => {
    await performControlTransfer(device, usb_1.usb.LIBUSB_REQUEST_TYPE_VENDOR, USB_REQUEST_CODE_GET_STATUS, size & USBBOOT_MESSAGE_MAX_BUFFER_LENGTH, size >> CONTROL_TRANSFER_INDEX_RIGHT_BIT_SHIFT, NULL_BUFFER);
};
function* chunks(buffer, size) {
    for (let start = 0; start < buffer.length; start += size) {
        yield buffer.slice(start, start + size);
    }
}
const transfer = async (endpoint, chunk) => {
    endpoint.timeout = USB_BULK_TRANSFER_TIMEOUT_MS;
    for (let tries = 0; tries < 3; tries++) {
        if (tries > 0) {
            debug('Transfer stall, retrying');
        }
        try {
            await fromCallback((callback) => {
                endpoint.transfer(chunk, callback);
            });
            return;
        }
        catch (error) {
            if (error.errno === usb_1.usb.LIBUSB_TRANSFER_STALL) {
                continue;
            }
            throw error;
        }
    }
};
const epWrite = async (buffer, device, endpoint) => {
    debug('Sending buffer size', buffer.length);
    await sendSize(device, buffer.length);
    if (buffer.length > 0) {
        for (const chunk of chunks(buffer, TRANSFER_BLOCK_SIZE)) {
            debug('Sending chunk of size', chunk.length);
            await transfer(endpoint, chunk);
        }
    }
};
const epRead = async (device, bytesToRead) => {
    return await performControlTransfer(device, usb_1.usb.LIBUSB_REQUEST_TYPE_VENDOR | usb_1.usb.LIBUSB_ENDPOINT_IN, USB_REQUEST_CODE_GET_STATUS, bytesToRead & USBBOOT_MESSAGE_MAX_BUFFER_LENGTH, bytesToRead >> CONTROL_TRANSFER_INDEX_RIGHT_BIT_SHIFT, bytesToRead);
};
const getDeviceId = (device) => {
    return `${device.busNumber}:${device.deviceAddress}`;
};
const getFileBuffer = async (device, filename, extraFolder) => {
    try {
        if (extraFolder) {
            const extraBuffer = await (0, promises_1.readFile)(Path.join(extraFolder, filename));
            if (extraBuffer !== undefined) {
                debug(`Sending buffer from ${extraFolder}/${filename}`);
                return extraBuffer;
            }
        }
    }
    catch (e) {
        // no data
    }
    try {
        const folder = device.deviceDescriptor.idProduct === USB_PRODUCT_ID_BCM2711_BOOT
            ? 'cm4'
            : 'raspberrypi';
        const buffer = await (0, promises_1.readFile)(Path.join(__dirname, '..', 'blobs', folder, filename));
        if (buffer === undefined) {
            debug("Can't read file", filename);
        }
        return buffer;
    }
    catch (e) {
        // no data
    }
};
/**
 * @summary Create a boot message buffer
 *
 * @description
 * This is based on the following data structure:
 *
 * typedef struct MESSAGE_S {
 *   int length;
 *   unsigned char signature[20];
 * } boot_message_t;
 *
 * This needs to be sent to the out endpoint of the USB device
 * as a 24 bytes little-endian buffer where:
 *
 * - The first 4 bytes contain the size of the bootcode.bin buffer
 * - The remaining 20 bytes contain the boot signature, which
 *   we don't make use of in this implementation
 */
const createBootMessageBuffer = (bootCodeBufferLength) => {
    const bootMessageBufferSize = BOOT_MESSAGE_BOOTCODE_LENGTH_SIZE + BOOT_MESSAGE_SIGNATURE_SIZE;
    // Buffers are automatically filled with zero bytes
    const bootMessageBuffer = Buffer.alloc(bootMessageBufferSize);
    // The bootcode length should be stored in 4 little-endian bytes
    bootMessageBuffer.writeInt32LE(bootCodeBufferLength, BOOT_MESSAGE_BOOTCODE_LENGTH_OFFSET);
    return bootMessageBuffer;
};
const secondStageBoot = async (device, endpoint) => {
    const bootcodeBuffer = await getFileBuffer(device, 'bootcode.bin');
    if (bootcodeBuffer === undefined) {
        throw new Error("Can't find bootcode.bin");
    }
    const bootMessage = createBootMessageBuffer(bootcodeBuffer.length);
    await epWrite(bootMessage, device, endpoint);
    debug(`Writing ${bootMessage.length} bytes`, devicePortId(device));
    await epWrite(bootcodeBuffer, device, endpoint);
    // raspberrypi's sample code has a sleep(1) here, but it looks like it isn't required.
    const data = await epRead(device, RETURN_CODE_LENGTH);
    const returnCode = data.readInt32LE(0);
    if (returnCode !== RETURN_CODE_SUCCESS) {
        throw new Error(`Couldn't write the bootcode, got return code ${returnCode} from device`);
    }
};
class UsbbootDevice extends events_1.EventEmitter {
    constructor(portId) {
        super();
        this.portId = portId;
        this._step = 0;
        this.last_serial = -1;
    }
    get progress() {
        return Math.round((this._step / this.LAST_STEP) * 100);
    }
    get step() {
        return this._step;
    }
    set step(step) {
        this._step = step;
        this.emit('progress', this.progress);
    }
}
exports.UsbbootDevice = UsbbootDevice;
class CM3 extends UsbbootDevice {
    constructor() {
        super(...arguments);
        // LAST_STEP is hardcoded here as it is depends on the bootcode.bin file we send to the pi.
        // List of steps:
        // 0) device connects with iSerialNumber 0 and we write bootcode.bin to it
        // 1) the device detaches
        // 2 - 38) the device reattaches with iSerialNumber 1 and we upload the files it requires (the number of steps depends on the device)
        // 39) the device detaches
        // 40) the device reattaches as a mass storage device
        this.LAST_STEP = 40;
    }
}
exports.CM3 = CM3;
class CM4 extends UsbbootDevice {
    constructor() {
        super(...arguments);
        // LAST_STEP is hardcoded here as it is depends on the bootcode.bin file we send to the pi.
        // List of steps:
        // 0) device connects with iSerialNumber 0 and we write bootcode.bin to it
        // 1) the device detaches
        // 2 - 8) the device reattaches with iSerialNumber 1 and we upload the files it requires (the number of steps depends on the device)
        // 9) the device detaches
        // 10) the device reattaches as a mass storage device
        this.LAST_STEP = 10;
    }
}
exports.CM4 = CM4;
class UsbbootScanner extends events_1.EventEmitter {
    constructor(extraFolder) {
        super();
        this.usbbootDevices = new Map();
        // We use both events ('attach' and 'detach') and polling getDeviceList() on usb.
        // We don't know which one will trigger the this.attachDevice call.
        // So we keep track of attached devices ids in attachedDeviceIds to not run it twice.
        this.attachedDeviceIds = new Set();
        this.extraFolder = extraFolder;
        debug(`Extra folder: ${extraFolder}`);
        this.boundAttachDevice = this.attachDevice.bind(this);
        this.boundDetachDevice = this.detachDevice.bind(this);
    }
    start() {
        debug('Waiting for BCM2835/6/7/2711');
        // Prepare already connected devices
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
        usb_1.usb.getDeviceList().map(this.boundAttachDevice);
        // At this point all devices from `usg.getDeviceList()` above
        // have had an 'attach' event emitted if they were raspberry pis.
        this.emit('ready');
        // Watch for new devices being plugged in and prepare them
        usb_1.usb.on('attach', this.boundAttachDevice);
        // Watch for devices detaching
        usb_1.usb.on('detach', this.boundDetachDevice);
        // @ts-expect-error because of a confusion between NodeJS.Timer and number
        this.interval = setInterval(() => {
            // usb.getDeviceList().forEach(this.boundAttachDevice);
        }, POLLING_INTERVAL_MS);
    }
    stop() {
        usb_1.usb.removeListener('attach', this.boundAttachDevice);
        usb_1.usb.removeListener('detach', this.boundDetachDevice);
        clearInterval(this.interval);
        this.usbbootDevices.clear();
    }
    step(device, step) {
        const usbbootDevice = this.getOrCreate(device);
        usbbootDevice.step = step;
        usbbootDevice.last_serial = device.deviceDescriptor.iSerialNumber;
        if (step === usbbootDevice.LAST_STEP) {
            this.remove(device);
        }
    }
    get(device) {
        const key = devicePortId(device);
        return this.usbbootDevices.get(key);
    }
    getOrCreate(device) {
        const key = devicePortId(device);
        let usbbootDevice = this.usbbootDevices.get(key);
        if (usbbootDevice === undefined) {
            const Cls = device.deviceDescriptor.idProduct === USB_PRODUCT_ID_BCM2711_BOOT
                ? CM4
                : CM3;
            usbbootDevice = new Cls(key);
            this.usbbootDevices.set(key, usbbootDevice);
            this.emit('attach', usbbootDevice);
        }
        return usbbootDevice;
    }
    remove(device) {
        const key = devicePortId(device);
        const usbbootDevice = this.usbbootDevices.get(key);
        if (usbbootDevice !== undefined) {
            this.usbbootDevices.delete(key);
            this.emit('detach', usbbootDevice);
        }
    }
    async attachDevice(device) {
        if (this.attachedDeviceIds.has(getDeviceId(device))) {
            return;
        }
        this.attachedDeviceIds.add(getDeviceId(device));
        const usbbootDevice = this.get(device);
        let forceSecondstage = false;
        if (device.deviceDescriptor.iSerialNumber === (usbbootDevice === null || usbbootDevice === void 0 ? void 0 : usbbootDevice.last_serial)) {
            if (usbbootDevice.step > 0) {
                forceSecondstage = true;
            }
        }
        if ((isRaspberryPiInMassStorageMode(device) ||
            isComputeModule4InMassStorageMode(device)) &&
            usbbootDevice !== undefined) {
            this.step(device, usbbootDevice.LAST_STEP);
            return;
        }
        if (!isUsbBootCapableUSBDevice$(device)) {
            return;
        }
        debug('Found serial number', device.deviceDescriptor.iSerialNumber, `${forceSecondstage ? ' => Forced second stage' : ''}`);
        debug('port id', devicePortId(device));
        try {
            const { endpoint } = initializeDevice(device);
            // cm: 0; cm4: 3
            if ((device.deviceDescriptor.iSerialNumber === 0 ||
                device.deviceDescriptor.iSerialNumber === 3) &&
                !forceSecondstage) {
                debug('Sending bootcode.bin', devicePortId(device));
                this.step(device, 0);
                await secondStageBoot(device, endpoint);
                // The device will now detach and reattach with iSerialNumber 1.
                // This takes approximately 1.5 seconds
            }
            else {
                const extraFolder = this.extraFolder;
                debug('Second stage boot server', devicePortId(device));
                await this.fileServer(device, endpoint, 2, extraFolder);
            }
            device.close();
        }
        catch (error) {
            debug('error', error, devicePortId(device));
            this.remove(device);
        }
    }
    detachDevice(device) {
        this.attachedDeviceIds.delete(getDeviceId(device));
        if (!isUsbBootCapableUSBDevice$(device)) {
            return;
        }
        const usbbootDevice = this.getOrCreate(device);
        const step = device.deviceDescriptor.iSerialNumber === 1
            ? usbbootDevice.LAST_STEP - 1
            : 1;
        debug('detach', devicePortId(device), step);
        this.step(device, step);
        // This timeout is here to differentiate between the device resetting and the device being unplugged
        // If the step didn't changed in 5 seconds, we assume the device was unplugged.
        setTimeout(() => {
            const $usbbootDevice = this.get(device);
            if ($usbbootDevice !== undefined && $usbbootDevice.step === step) {
                debug('device', devicePortId(device), 'did not reattached after', DEVICE_UNPLUG_TIMEOUT, 'ms.');
                this.remove(device);
            }
        }, DEVICE_UNPLUG_TIMEOUT);
    }
    async fileServer(device, endpoint, step, extraFolder) {
        // eslint-disable-next-line no-constant-condition
        while (true) {
            let data;
            try {
                data = await epRead(device, FILE_MESSAGE_SIZE);
            }
            catch (error) {
                if (error.message === 'LIBUSB_ERROR_NO_DEVICE' ||
                    error.message === 'LIBUSB_ERROR_IO') {
                    // Drop out if the device goes away
                    break;
                }
                await delay(READ_ERROR_DELAY);
                continue;
            }
            this.step(device, step);
            step += 1;
            const message = parseFileMessageBuffer(data);
            debug('Received message', FileMessageCommand[message.command], message.filename, devicePortId(device));
            if (message.command === FileMessageCommand.GetFileSize ||
                message.command === FileMessageCommand.ReadFile) {
                const buffer = await getFileBuffer(device, message.filename, extraFolder);
                if (buffer === undefined) {
                    debug(`Couldn't find ${message.filename}`, devicePortId(device));
                    await sendSize(device, 0);
                }
                else {
                    if (message.command === FileMessageCommand.GetFileSize) {
                        await sendSize(device, buffer.length);
                    }
                    else {
                        await epWrite(buffer, device, endpoint);
                    }
                }
            }
            else if (message.command === FileMessageCommand.Done) {
                break;
            }
        }
        debug('File server done', devicePortId(device));
        // On some computers, the rpi won't detach at this point.
        // If you try communicating with it, it will error, detach and reattach as expected.
        await delay(2000);
        try {
            device.open();
        }
        catch (_a) {
            // We expect LIBUSB_ERROR_IO here
        }
    }
}
exports.UsbbootScanner = UsbbootScanner;
const devicePortId = (device) => {
    let result = `${device.busNumber}`;
    if (device.portNumbers !== undefined) {
        result += `-${device.portNumbers.join('.')}`;
    }
    return result;
};
//# sourceMappingURL=index.js.map