Juan Cruz Viotti 02f7a5f55b refactor(SDK): make adapter scan functions event based (#1781)
* refactor(SDK): make adaptor scan functions event based

This change will allow us to start emitting progress events out of the
adaptors scan functions.

Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>

* refactor(sdk): Make adapters event emitters
2017-10-25 13:26:43 +02:00

460 lines
12 KiB
JavaScript

/*
* Copyright 2017 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* This work is heavily based on https://github.com/raspberrypi/usbboot
* Copyright 2016 Raspberry Pi Foundation
*/
'use strict'
const _ = require('lodash')
const EventEmitter = require('events')
const Bluebird = require('bluebird')
const debug = require('debug')('sdk:usbboot')
const usb = require('./usb')
const protocol = require('./protocol')
debug.enabled = true
/**
* @summary The radix used by USB ID numbers
* @type {Number}
* @constant
*/
const USB_ID_RADIX = 16
/**
* @summary The expected length of a USB ID number
* @type {Number}
* @constant
*/
const USB_ID_LENGTH = 4
/**
* @summary Vendor ID of "Broadcom Corporation"
* @type {Number}
* @constant
*/
const USB_VENDOR_ID_BROADCOM_CORPORATION = 0x0a5c
/**
* @summary Product ID of BCM2708
* @type {Number}
* @constant
*/
const USB_PRODUCT_ID_BCM2708_BOOT = 0x2763
/**
* @summary Product ID of BCM2710
* @type {Number}
* @constant
*/
const USB_PRODUCT_ID_BCM2710_BOOT = 0x2764
/**
* @summary The timeout for USB device operations
* @type {Number}
* @constant
*/
const USB_OPERATION_TIMEOUT_MS = 1000
/**
* @summary The number of USB endpoint interfaces in devices with a BCM2835 SoC
* @type {Number}
* @constant
*/
const USB_ENDPOINT_INTERFACES_SOC_BCM2835 = 1
/**
* @summary The USB device descriptor index of an empty property
* @type {Number}
* @constant
*/
const USB_DESCRIPTOR_NULL_INDEX = 0
/**
* @summary usbboot bootcode file name
* @type {String}
* @constant
*/
const USBBOOT_BOOTCODE_FILE_NAME = 'bootcode.bin'
/**
* @summary List of usbboot capable devices
* @type {Object[]}
* @constant
*/
const USBBOOT_CAPABLE_USB_DEVICES = [
// BCM2835
{
vendorID: USB_VENDOR_ID_BROADCOM_CORPORATION,
productID: USB_PRODUCT_ID_BCM2708_BOOT
},
// BCM2837
{
vendorID: USB_VENDOR_ID_BROADCOM_CORPORATION,
productID: USB_PRODUCT_ID_BCM2710_BOOT
}
]
/**
* @summary Convert a USB id (e.g. product/vendor) to a string
* @function
* @private
*
* @param {Number} id - USB id
* @returns {String} string id
*
* @example
* console.log(usbIdToString(2652))
* > '0x0a5c'
*/
const usbIdToString = (id) => {
return `0x${_.padStart(id.toString(USB_ID_RADIX), USB_ID_LENGTH, '0')}`
}
/**
* @summary Check if a USB device object is usbboot-capable
* @function
* @private
*
* @param {Object} device - device
* @returns {Boolean} whether the device is usbboot-capable
*
* @example
* if (isUsbBootCapableUSBDevice({ ... })) {
* console.log('We can use usbboot on this device')
* }
*/
const isUsbBootCapableUSBDevice = (device) => {
return _.some(USBBOOT_CAPABLE_USB_DEVICES, {
vendorID: device.deviceDescriptor.idVendor,
productID: device.deviceDescriptor.idProduct
})
}
/**
* @summary USBBootAdapter
* @class
*/
class USBBootAdapter extends EventEmitter {
/**
* @summary USBBootAdapter constructor
* @class
* @example
* const adapter = new USBBootAdapter()
*/
constructor () {
super()
/** @type {String} Adapter name */
this.id = this.constructor.id
/** @type {Object} Blob cache */
this.blobCache = {}
}
/**
* @summary Query a blob from the internal cache
* @private
*
* @param {String} name - blob name
* @param {Function} fallback - fallback function
* @fulfil {Buffer} - blob
* @returns {Promise}
*
* @example
* const Bluebird = require('bluebird')
* const fs = Bluebird.promisifyAll(require('fs'))
*
* const blob = adapter.queryBlobFromCache('start.elf', (name) => {
* return fs.readFileAsync(path.join('./blobs', name))
* })
*/
queryBlobFromCache (name, fallback) {
if (this.blobCache[name]) {
return Bluebird.resolve(this.blobCache[name])
}
return fallback(name).tap((buffer) => {
this.blobCache[name] = buffer
})
}
/**
* @summary Scan for usbboot capable USB devices
* @public
*
* @description
* You should at the very least pass a file named `bootcode.bin`.
*
* @param {Object} options - options
* @param {Object} options.files - files buffers
* @param {Function} callback - callback
* @returns {USBBootAdapter}
*
* @example
* adapter.scan({
* files: {
* 'bootcode.bin': fs.readFileSync('./msd/bootcode.bin'),
* 'start.elf': fs.readFileSync('./msd/start.elf')
* }
* }, (error, devices) => {
* // ...
* })
*/
scan (options = {}, callback) {
/* eslint-disable lodash/prefer-lodash-method */
usb.listDevices().filter(isUsbBootCapableUSBDevice).map((device) => {
/* eslint-enable lodash/prefer-lodash-method */
// This is the only way we can unique identify devices
device.device = `${device.busNumber}:${device.deviceAddress}`
device.displayName = 'Initializing device'
device.description = 'Compute Module'
device.raw = device.device
device.size = null
device.mountpoints = []
device.protected = false
device.system = false
device.disabled = true
device.icon = 'loading'
device.vendor = usbIdToString(device.deviceDescriptor.idVendor)
device.product = usbIdToString(device.deviceDescriptor.idProduct)
device.adaptor = exports.name
// We need to open the device in order to access _configDescriptor
debug(`Opening device: ${device.device} (${device.vendor}:${device.product})`)
device.open()
// Ensures we don't wait forever if an issue occurs
device.timeout = USB_OPERATION_TIMEOUT_MS
// Handle 2837 where it can start with two interfaces, the first
// is mass storage the second is the vendor interface for programming
const addresses = {}
/* eslint-disable no-underscore-dangle */
if (device._configDescriptor.bNumInterfaces === USB_ENDPOINT_INTERFACES_SOC_BCM2835) {
/* eslint-enable no-underscore-dangle */
addresses.interface = 0
addresses.endpoint = 1
} else {
addresses.interface = 1
addresses.endpoint = 3
}
const deviceInterface = device.interface(addresses.interface)
debug(`Claiming interface: ${addresses.interface}`)
try {
deviceInterface.claim()
} catch (error) {
if (error.message === 'LIBUSB_ERROR_NO_DEVICE') {
debug('Couldn\'t claim the interface. Assuming the device is gone')
return null
}
throw error
}
const endpoint = deviceInterface.endpoint(addresses.endpoint)
return Bluebird.try(() => {
const serialNumberIndex = device.deviceDescriptor.iSerialNumber
debug(`Serial number index: ${serialNumberIndex}`)
if (serialNumberIndex === USB_DESCRIPTOR_NULL_INDEX) {
return USBBootAdapter.writeBootCode(device, endpoint, _.get(options.files, [
USBBOOT_BOOTCODE_FILE_NAME
]))
}
debug('Starting file server')
return this.startFileServer(device, endpoint, options.files)
}).return(device).finally(() => {
device.close()
})
// See http://bluebirdjs.com/docs/api/promise.map.html
}, {
concurrency: 5
}).catch((error) => {
callback(error)
}).then((devices) => {
this.emit('devices', devices)
callback(null, devices)
})
return this
}
/**
* @summary Write bootcode to USB device (usbboot first stage)
* @private
*
* @description
* After this stage is run, the USB will be re-mounted as 0x0a5c:0x2764.
*
* @param {Object} device - node-usb device
* @param {Object} endpoint - node-usb endpoint
* @param {Buffer} bootCodeBuffer - bootcode buffer
* @returns {Promise}
*
* @example
* const usb = require('usb')
* const device = usb.findByIds(0x0a5c, 0x2763)
* const bootcode = fs.readFileSync('./bootcode.bin')
*
* adapter.writeBootCode(device, device.interfaces(0).endpoint(1), bootcode).then(() => {
* console.log('Done!')
* })
*/
static writeBootCode (device, endpoint, bootCodeBuffer) {
debug('Writing bootcode')
debug(`Bootcode buffer length: ${bootCodeBuffer.length}`)
const bootMessageBuffer = protocol.createBootMessageBuffer(bootCodeBuffer.length)
debug('Writing boot message buffer to out endpoint')
return protocol.write(device, endpoint, bootMessageBuffer).then(() => {
debug('Writing boot code buffer to out endpoint')
return protocol.write(device, endpoint, bootCodeBuffer)
}).then(() => {
debug('Reading return code from device')
return protocol.read(device, protocol.RETURN_CODE_LENGTH)
}).then((data) => {
const returnCode = data.readInt32LE()
debug(`Received return code: ${returnCode}`)
if (returnCode !== protocol.RETURN_CODE_SUCCESS) {
throw new Error(`Couldn't write the bootcode, got return code ${returnCode} from device`)
}
})
}
/**
* @summary Mount a USB device as a block device (usbboot second stage)
* @private
*
* @description
* The possible files you can pass here are:
*
* - autoboot.txt
* - config.txt
* - recovery.elf
* - start.elf
* - fixup.dat
*
* @param {Object} device - node-usb device
* @param {Object} endpoint - node-usb endpoint
* @param {Function} readFile - read file function
* @returns {Promise}
*
* @example
* const fs = Bluebird.promisifyAll(require('fs'))
* const usb = require('usb')
* const device = usb.findByIds(0x0a5c, 0x2763)
*
* adapter.startFileServer(device, device.interfaces(0).endpoint(1), (name) => {
* return fs.readFileAsync(name)
* }).then(() => {
* console.log('Done!')
* })
*/
startFileServer (device, endpoint, readFile) {
debug('Listening for file messages')
return protocol
.read(device, protocol.FILE_MESSAGE_SIZE)
.then(protocol.parseFileMessageBuffer)
// We get these error messages when reading a command
// from the device when the communication has ended
.catch({
message: 'LIBUSB_TRANSFER_STALL'
}, {
message: 'LIBUSB_TRANSFER_ERROR'
}, (error) => {
debug(`Got ${error.message} when reading a command, assuming everything is done`)
return {
command: protocol.FILE_MESSAGE_COMMANDS.DONE
}
})
.then((fileMessage) => {
debug(`Received message: ${fileMessage.command} -> ${fileMessage.fileName}`)
if (fileMessage.command === protocol.FILE_MESSAGE_COMMANDS.DONE) {
debug('Done')
return Bluebird.resolve()
}
return Bluebird.try(() => {
if (fileMessage.command === protocol.FILE_MESSAGE_COMMANDS.GET_FILE_SIZE) {
debug(`Getting the size of ${fileMessage.fileName}`)
return this.queryBlobFromCache(fileMessage.fileName, readFile).then((fileBuffer) => {
const fileSize = fileBuffer.length
debug(`Sending size: ${fileSize}`)
return protocol.sendBufferSize(device, fileSize)
}).catch({
code: 'ENOENT'
}, () => {
debug(`Couldn't find ${fileMessage.fileName}`)
debug('Sending error signal')
return protocol.sendErrorSignal(device)
})
}
if (fileMessage.command === protocol.FILE_MESSAGE_COMMANDS.READ_FILE) {
debug(`Reading ${fileMessage.fileName}`)
return this.queryBlobFromCache(fileMessage.fileName, readFile).then((fileBuffer) => {
return protocol.write(device, endpoint, fileBuffer)
}).catch({
code: 'ENOENT'
}, () => {
debug(`Couldn't find ${fileMessage.fileName}`)
debug('Sending error signal')
return protocol.sendErrorSignal(device)
})
}
return Bluebird.reject(new Error(`Unrecognized command: ${fileMessage.command}`))
}).then(() => {
debug('Starting again')
return this.startFileServer(device, endpoint, readFile)
})
})
}
}
/**
* @summary The name of this adapter
* @public
* @type {String}
* @constant
*/
USBBootAdapter.id = 'usbboot'
// Exports
module.exports = USBBootAdapter