"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UsbBBbootDevice = exports.UsbBBbootScanner = exports.isSPLUSBDevice = exports.isROMUSBDevice = exports.isUsbBootCapableUSBDevice = void 0;
const usb_1 = require("usb");
const _debug = require("debug");
const events_1 = require("events");
const _os = require("os");
const timers_1 = require("timers");
const messages_1 = require("./messages");
const platform = _os.platform();
const debug = _debug('node-beaglebone-usbboot');
const POLLING_INTERVAL_MS = 2000;
// Delay in ms after which we consider that the device was unplugged (not resetted)
const DEVICE_UNPLUG_TIMEOUT = 5000;
const USB_VENDOR_ID_TEXAS_INSTRUMENTS = 0x0451;
const USB_PRODUCT_ID_ROM = 0x6141;
const USB_PRODUCT_ID_SPL = 0xd022;
const MAX_CLOSE_DEVICE_TRIES = 2;
const getDeviceId = (device) => {
    return `${device.busNumber}:${device.deviceAddress}`;
};
const isUsbBootCapableUSBDevice = (idVendor, idProduct) => {
    return (idVendor === USB_VENDOR_ID_TEXAS_INSTRUMENTS &&
        (idProduct === USB_PRODUCT_ID_ROM || idProduct === USB_PRODUCT_ID_SPL));
};
exports.isUsbBootCapableUSBDevice = isUsbBootCapableUSBDevice;
const isROMUSBDevice = (idVendor, idProduct) => {
    return (idVendor === USB_VENDOR_ID_TEXAS_INSTRUMENTS &&
        idProduct === USB_PRODUCT_ID_ROM);
};
exports.isROMUSBDevice = isROMUSBDevice;
const isSPLUSBDevice = (idVendor, idProduct) => {
    return (idVendor === USB_VENDOR_ID_TEXAS_INSTRUMENTS &&
        idProduct === USB_PRODUCT_ID_SPL);
};
exports.isSPLUSBDevice = isSPLUSBDevice;
const isUsbBootCapableUSBDevice$ = (device) => {
    return (0, exports.isUsbBootCapableUSBDevice)(device.deviceDescriptor.idVendor, device.deviceDescriptor.idProduct);
};
const isBeagleBoneInMassStorageMode = (device) => {
    return (device.deviceDescriptor.idVendor === USB_VENDOR_ID_TEXAS_INSTRUMENTS &&
        device.deviceDescriptor.idProduct === USB_PRODUCT_ID_SPL &&
        device.deviceDescriptor.bNumConfigurations === 1);
};
const initializeDevice = (device) => {
    var _a;
    debug('bInterface', (_a = device.configDescriptor) === null || _a === void 0 ? void 0 : _a.bNumInterfaces);
    const interfaceNumber = 1;
    const iface = device.interface(interfaceNumber);
    if (platform !== 'win32') {
        // Not supported in Windows
        // Detach Kernel Driver
        if (iface.isKernelDriverActive()) {
            iface.detachKernelDriver();
        }
    }
    iface.claim();
    const inEndpoint = iface.endpoints[0];
    const outEndpoint = iface.endpoints[1];
    if (!(inEndpoint instanceof usb_1.InEndpoint)) {
        throw new Error('endpoint is not an OutEndpoint');
    }
    if (!(outEndpoint instanceof usb_1.OutEndpoint)) {
        throw new Error('endpoint is not an OutEndpoint');
    }
    debug('Initialized device correctly', devicePortId(device));
    return { inEndpoint, outEndpoint };
};
const initializeRNDIS = (device) => {
    const interfaceNumber = 0;
    const iface0 = device.interface(interfaceNumber);
    iface0.claim();
    const iEndpoint = iface0.endpoints[0];
    if (!(iEndpoint instanceof usb_1.InEndpoint)) {
        throw new Error('endpoint is not an OutEndpoint');
    }
    else {
        iEndpoint.startPoll(1, 256);
    }
    const CONTROL_BUFFER_SIZE = 1025;
    const message = new messages_1.Message();
    const initMsg = message.getRNDISInit(); // RNDIS INIT Message
    // Windows Control Transfer
    // https://msdn.microsoft.com/en-us/library/aa447434.aspx
    // http://www.beyondlogic.org/usbnutshell/usb6.shtml
    const bmRequestTypeSend = 0x21; // USB_TYPE=CLASS | USB_RECIPIENT=INTERFACE
    const bmRequestTypeReceive = 0xa1; // USB_DATA=DeviceToHost | USB_TYPE=CLASS | USB_RECIPIENT=INTERFACE
    // Sending rndis_init_msg (SEND_ENCAPSULATED_COMMAND)
    device.controlTransfer(bmRequestTypeSend, 0, 0, 0, initMsg, (error) => {
        if (error) {
            throw new Error(`Control transfer error on SEND_ENCAPSULATED ${error}`);
        }
    });
    // Receive rndis_init_cmplt (GET_ENCAPSULATED_RESPONSE)
    device.controlTransfer(bmRequestTypeReceive, 0x01, 0, 0, CONTROL_BUFFER_SIZE, (error) => {
        if (error) {
            throw new Error(`Control transfer error on GET_ENCAPSULATED ${error}`);
        }
    });
    const setMsg = message.getRNDISSet(); // RNDIS SET Message
    // Send rndis_set_msg (SEND_ENCAPSULATED_COMMAND)
    device.controlTransfer(bmRequestTypeSend, 0, 0, 0, setMsg, (error) => {
        if (error) {
            throw new Error(`Control transfer error on SEND_ENCAPSULATED ${error}`);
        }
    });
    // Receive rndis_init_cmplt (GET_ENCAPSULATED_RESPONSE)
    device.controlTransfer(bmRequestTypeReceive, 0x01, 0, 0, CONTROL_BUFFER_SIZE, (error) => {
        if (error) {
            throw new Error(`Control transfer error on GET_ENCAPSULATED ${error}`);
        }
    });
    return iEndpoint;
};
const stopPoll = (inEndpoint) => __awaiter(void 0, void 0, void 0, function* () {
    return new Promise((res) => {
        inEndpoint.stopPoll(res);
    });
});
class UsbBBbootScanner extends events_1.EventEmitter {
    constructor() {
        super();
        this.usbBBbootDevices = 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.boundAttachDevice = this.attachDevice.bind(this);
        this.boundDetachDevice = this.detachDevice.bind(this);
    }
    start() {
        debug('Waiting for BeagleBone');
        // Prepare already connected devices
        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 beaglebone.
        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);
        this.interval = (0, timers_1.setInterval)(() => {
            usb_1.usb.getDeviceList().forEach(this.boundAttachDevice);
        }, POLLING_INTERVAL_MS);
    }
    stop() {
        usb_1.usb.removeListener('attach', this.boundAttachDevice);
        usb_1.usb.removeListener('detach', this.boundDetachDevice);
        if (this.interval !== undefined) {
            (0, timers_1.clearInterval)(this.interval);
        }
        this.usbBBbootDevices.clear();
    }
    step(device, step) {
        const usbBBbootDevice = this.getOrCreate(device);
        usbBBbootDevice.step = step;
        if (step === UsbBBbootDevice.LAST_STEP) {
            this.remove(device);
        }
    }
    incrementStep(device) {
        const usbBBbootDevice = this.getOrCreate(device);
        this.step(device, usbBBbootDevice.step + 1);
    }
    get(device) {
        const key = devicePortId(device);
        return this.usbBBbootDevices.get(key);
    }
    getOrCreate(device) {
        const key = devicePortId(device);
        let usbBBbootDevice = this.usbBBbootDevices.get(key);
        if (usbBBbootDevice === undefined) {
            usbBBbootDevice = new UsbBBbootDevice(key);
            this.usbBBbootDevices.set(key, usbBBbootDevice);
            this.emit('attach', usbBBbootDevice);
        }
        return usbBBbootDevice;
    }
    remove(device) {
        const key = devicePortId(device);
        const usbBBbootDevice = this.usbBBbootDevices.get(key);
        if (usbBBbootDevice !== undefined) {
            this.usbBBbootDevices.delete(key);
            this.emit('detach', usbBBbootDevice);
        }
    }
    attachDevice(device) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.attachedDeviceIds.has(getDeviceId(device))) {
                return;
            }
            this.attachedDeviceIds.add(getDeviceId(device));
            if (isBeagleBoneInMassStorageMode(device) &&
                this.usbBBbootDevices.has(devicePortId(device))) {
                this.step(device, UsbBBbootDevice.LAST_STEP);
                return;
            }
            if (!isUsbBootCapableUSBDevice$(device)) {
                return;
            }
            if (device.deviceDescriptor.iSerialNumber !== 0) {
                return;
            }
            if ((0, exports.isROMUSBDevice)(device.deviceDescriptor.idVendor, device.deviceDescriptor.idProduct)) {
                this.process(device, 'u-boot-spl.bin');
            }
            if ((0, exports.isSPLUSBDevice)(device.deviceDescriptor.idVendor, device.deviceDescriptor.idProduct)) {
                setTimeout(() => {
                    this.process(device, 'u-boot.img');
                }, 500);
            }
        });
    }
    process(device, fileName) {
        try {
            device.open();
            let rndisInEndpoint;
            if (platform === 'win32' || platform === 'darwin') {
                rndisInEndpoint = initializeRNDIS(device);
                rndisInEndpoint.on('error', (error) => {
                    debug('RNDIS InEndpoint Error', error);
                });
            }
            const { inEndpoint, outEndpoint } = initializeDevice(device);
            const serverConfig = {};
            serverConfig.bootpFile = fileName;
            inEndpoint.startPoll(1, 500); // MAXBUFF
            inEndpoint.on('error', (error) => {
                debug('InEndpoint Error', error);
            });
            const message = new messages_1.Message();
            inEndpoint.on('data', (data) => __awaiter(this, void 0, void 0, function* () {
                const request = message.identify(data);
                if (request === 'BOOTP') {
                    const bootPBuff = message.getBOOTPResponse(data, serverConfig);
                    yield this.transfer(device, outEndpoint, bootPBuff);
                }
                else if (request === 'ARP') {
                    const arpBuff = message.getARResponse(data, serverConfig);
                    yield this.transfer(device, outEndpoint, arpBuff);
                }
                else if (request === 'TFTP') {
                    message.getBootFile(data, serverConfig);
                    if (!serverConfig.tftp.fileError) {
                        const tftpBuff = message.getTFTPData(serverConfig);
                        if (tftpBuff !== undefined) {
                            yield this.transfer(device, outEndpoint, tftpBuff);
                        }
                    }
                    else {
                        yield this.transfer(device, outEndpoint, message.getTFTPError(serverConfig));
                    }
                }
                else if (request === 'TFTP_Data') {
                    const tftpBuff = message.getTFTPData(serverConfig);
                    if (serverConfig.tftp) {
                        if (tftpBuff !== undefined) {
                            yield this.transfer(device, outEndpoint, tftpBuff);
                        }
                        else {
                            if (platform === 'win32') {
                                yield stopPoll(rndisInEndpoint);
                            }
                            yield stopPoll(inEndpoint);
                            this.closeDevice(device, 0);
                        }
                    }
                }
                else {
                    debug('Request', request);
                }
            }));
        }
        catch (error) {
            debug('error', error, devicePortId(device));
            this.remove(device);
        }
    }
    transfer(device, outEndpoint, response) {
        return __awaiter(this, void 0, void 0, function* () {
            yield new Promise((resolve, reject) => {
                outEndpoint.transfer(response, (error) => {
                    if (!error) {
                        resolve();
                    }
                    else {
                        debug('Out transfer Error', error);
                        reject(error);
                    }
                });
            });
            this.incrementStep(device);
        });
    }
    closeDevice(device, tries) {
        try {
            debug('Closing device...');
            device.close();
            debug('Device closed.');
        }
        catch (error) {
            const errorMessage = error.message;
            if (tries < MAX_CLOSE_DEVICE_TRIES &&
                errorMessage === "Can't close device with a pending request") {
                debug('Retrying device.close()...');
                setTimeout(() => {
                    this.closeDevice(device, tries + 1);
                }, 150);
            }
            else {
                console.error(error);
            }
        }
    }
    detachDevice(device) {
        this.attachedDeviceIds.delete(getDeviceId(device));
        if (!isUsbBootCapableUSBDevice$(device)) {
            return;
        }
        setTimeout(() => {
            const usbBBbootDevice = this.get(device);
            if (usbBBbootDevice !== undefined &&
                usbBBbootDevice.step === UsbBBbootDevice.LAST_STEP) {
                debug('device', devicePortId(device), 'did not reattached after', DEVICE_UNPLUG_TIMEOUT, 'ms.');
                this.remove(device);
            }
        }, DEVICE_UNPLUG_TIMEOUT);
    }
}
exports.UsbBBbootScanner = UsbBBbootScanner;
// tslint:disable-next-line
class UsbBBbootDevice extends events_1.EventEmitter {
    constructor(portId) {
        super();
        this.portId = portId;
        this._step = 0;
    }
    get progress() {
        return Math.floor((this._step / UsbBBbootDevice.LAST_STEP) * 100);
    }
    get step() {
        return this._step;
    }
    set step(step) {
        this._step = step;
        this.emit('progress', this.progress);
    }
}
exports.UsbBBbootDevice = UsbBBbootDevice;
UsbBBbootDevice.LAST_STEP = 1124;
const devicePortId = (device) => {
    let result = `${device.busNumber}`;
    if (device.portNumbers !== undefined) {
        result += `-${device.portNumbers.join('.')}`;
    }
    return result;
};
//# sourceMappingURL=index.js.map