node-beaglebone-usbboot
Version:
Transforms BeagleBone to mass storage device
344 lines • 14.9 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
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) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
// tslint:disable:no-bitwise
const usb = require("@balena.io/usb");
const _debug = require("debug");
const events_1 = require("events");
const _os = require("os");
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 getDeviceId = (device) => {
return `${device.busNumber}:${device.deviceAddress}`;
};
exports.isUsbBootCapableUSBDevice = (idVendor, idProduct) => {
return (idVendor === USB_VENDOR_ID_TEXAS_INSTRUMENTS &&
(idProduct === USB_PRODUCT_ID_ROM || idProduct === USB_PRODUCT_ID_SPL));
};
exports.isROMUSBDevice = (idVendor, idProduct) => {
return idVendor === USB_VENDOR_ID_TEXAS_INSTRUMENTS && idProduct === USB_PRODUCT_ID_ROM;
};
exports.isSPLUSBDevice = (idVendor, idProduct) => {
return idVendor === USB_VENDOR_ID_TEXAS_INSTRUMENTS && idProduct === USB_PRODUCT_ID_SPL;
};
const isUsbBootCapableUSBDevice$ = (device) => {
return 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) => {
debug('bInterface', device.configDescriptor.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.InEndpoint)) {
throw new Error('endpoint is not an usb.OutEndpoint');
}
if (!(outEndpoint instanceof usb.OutEndpoint)) {
throw new Error('endpoint is not an usb.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.InEndpoint)) {
throw new Error('endpoint is not an usb.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;
};
class UsbBBbootScanner extends events_1.EventEmitter {
constructor() {
super();
this.usbBBbootDevices = new Map();
this.stepCounter = 0;
// 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.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.on('attach', this.boundAttachDevice);
// Watch for devices detaching
usb.on('detach', this.boundDetachDevice);
// ts-ignore because of a confusion between NodeJS.Timer and number
// @ts-ignore
this.interval = setInterval(() => {
usb.getDeviceList().forEach(this.boundAttachDevice);
}, POLLING_INTERVAL_MS);
}
stop() {
usb.removeListener('attach', this.boundAttachDevice);
usb.removeListener('detach', this.boundDetachDevice);
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);
}
}
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.step(device, UsbBBbootDevice.LAST_STEP);
return;
}
if (!isUsbBootCapableUSBDevice$(device)) {
return;
}
if (device.deviceDescriptor.iSerialNumber !== 0) {
return;
}
if (exports.isROMUSBDevice(device.deviceDescriptor.idVendor, device.deviceDescriptor.idProduct)) {
this.stepCounter = 0;
this.process(device, 'u-boot-spl.bin');
}
if (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);
let serverConfig = {};
serverConfig.bootpFile = fileName;
inEndpoint.startPoll(1, 500); // MAXBUFF
inEndpoint.on('error', (error) => {
debug('InEndpoint Error', error);
});
inEndpoint.on('data', (data) => {
const message = new messages_1.Message();
const request = message.identify(data);
switch (request) {
case 'unidentified':
break;
case 'BOOTP':
const { bootPBuff, bootPServerConfig } = message.getBOOTPResponse(data, serverConfig);
serverConfig = bootPServerConfig;
this.transfer(device, outEndpoint, request, bootPBuff, this.stepCounter++);
break;
case 'ARP':
const { arpBuff, arpServerConfig } = message.getARResponse(data, serverConfig);
serverConfig = arpServerConfig;
this.transfer(device, outEndpoint, request, arpBuff, this.stepCounter++);
break;
case 'TFTP':
serverConfig = message.getBootFile(data, serverConfig);
if (!serverConfig.tftp.fileError) {
// tslint:disable-next-line
const { tftpBuff, tftpServerConfig } = message.getTFTPData(serverConfig);
serverConfig = tftpServerConfig;
this.transfer(device, outEndpoint, request, tftpBuff, this.stepCounter++);
}
else {
this.transfer(device, outEndpoint, request, message.getTFTPError(serverConfig), this.stepCounter);
}
break;
case 'TFTP_Data':
const { tftpBuff, tftpServerConfig } = message.getTFTPData(serverConfig);
serverConfig = tftpServerConfig;
if (serverConfig.tftp) {
if (serverConfig.tftp.blocks <= serverConfig.tftp.blocks) {
this.transfer(device, outEndpoint, request, tftpBuff, this.stepCounter++);
}
else {
if (platform === 'win32' || platform === 'darwin') {
rndisInEndpoint.stopPoll();
}
inEndpoint.stopPoll();
device.close();
}
}
break;
default:
debug('Request', request);
}
});
}
catch (error) {
debug('error', error, devicePortId(device));
this.remove(device);
}
}
transfer(device, outEndpoint, request, response, step) {
return new Promise((resolve, reject) => {
outEndpoint.transfer(response, (cb) => {
if (!cb) {
if (request === 'BOOTP') {
this.step(device, step);
}
if (request === 'ARP') {
this.step(device, step);
}
if (request === 'TFTP') {
this.step(device, step);
}
if (request === 'TFTP_Data') {
this.step(device, step);
}
}
else {
debug('Out transfer Error', cb);
reject(cb);
}
});
resolve(true);
});
}
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.round((this.STEP / UsbBBbootDevice.LAST_STEP) * 100);
}
get step() {
return this.STEP;
}
set step(step) {
this.STEP = step;
this.emit('progress', this.progress);
}
}
UsbBBbootDevice.LAST_STEP = 1124;
exports.UsbBBbootDevice = UsbBBbootDevice;
const devicePortId = (device) => {
let result = `${device.busNumber}`;
if (device.portNumbers !== undefined) {
result += `-${device.portNumbers.join('.')}`;
}
return result;
};
//# sourceMappingURL=index.js.map