mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 07:17:18 +00:00
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
This commit is contained in:
parent
2556807166
commit
02f7a5f55b
@ -18,25 +18,36 @@
|
||||
|
||||
const Bluebird = require('bluebird')
|
||||
const _ = require('lodash')
|
||||
const sdk = module.exports
|
||||
|
||||
/**
|
||||
* @summary The list of loaded adaptors
|
||||
* @summary The list of loaded adapters
|
||||
* @type {Object[]}
|
||||
* @constant
|
||||
*/
|
||||
const ADAPTORS = [
|
||||
const ADAPTERS = [
|
||||
require('./standard'),
|
||||
require('./usbboot')
|
||||
]
|
||||
|
||||
/**
|
||||
* @summary Scan for drives using all registered adaptors
|
||||
* @summary Initialised adapters
|
||||
* @type {Object<String,Adapter>}
|
||||
* @constant
|
||||
*/
|
||||
sdk.adapters = _.reduce(ADAPTERS, (adapters, Adapter) => {
|
||||
adapters[Adapter.name] = new Adapter()
|
||||
return adapters
|
||||
}, {})
|
||||
|
||||
/**
|
||||
* @summary Scan for drives using all registered adapters
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* The options object contains options for all the registered
|
||||
* adaptors. For the `standard` adaptor, for example, place
|
||||
* adapters. For the `standard` adapter, for example, place
|
||||
* options in `options.standard`.
|
||||
*
|
||||
* @param {Object} options - options
|
||||
@ -52,8 +63,16 @@ const ADAPTORS = [
|
||||
* console.log(drives)
|
||||
* })
|
||||
*/
|
||||
exports.scan = (options) => {
|
||||
return Bluebird.all(_.map(ADAPTORS, (adaptor) => {
|
||||
return adaptor.scan(_.get(options, [ adaptor.name ], {}))
|
||||
sdk.scan = (options) => {
|
||||
return Bluebird.all(_.map(sdk.adapters, (adapter) => {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
adapter.scan(_.get(options, [ adapter.id ], {}), (error, devices) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(devices)
|
||||
}
|
||||
})
|
||||
})
|
||||
})).then(_.flatten)
|
||||
}
|
||||
|
@ -18,49 +18,78 @@
|
||||
|
||||
const _ = require('lodash')
|
||||
const Bluebird = require('bluebird')
|
||||
const EventEmitter = require('events')
|
||||
const drivelist = Bluebird.promisifyAll(require('drivelist'))
|
||||
|
||||
/**
|
||||
* @summary The name of this adaptor
|
||||
* @summary StandardAdapter
|
||||
* @class
|
||||
*/
|
||||
class StandardAdapter extends EventEmitter {
|
||||
/**
|
||||
* @summary StandardAdapter constructor
|
||||
* @class
|
||||
* @example
|
||||
* const adapter = new StandardAdapter()
|
||||
*/
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
/** @type {String} Adapter name */
|
||||
this.id = this.constructor.id
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Scan for block devices
|
||||
* @public
|
||||
*
|
||||
* @param {Object} [options] - options
|
||||
* @param {Object} [options.includeSystemDrives=false] - include system drives
|
||||
* @param {Function} callback - callback
|
||||
* @returns {StandardAdapter}
|
||||
*
|
||||
* @example
|
||||
* adapter.scan({
|
||||
* includeSystemDrives: true
|
||||
* }, (error, devices) => {
|
||||
* // ...
|
||||
* })
|
||||
*/
|
||||
scan (options = {}, callback) {
|
||||
// eslint-disable-next-line lodash/prefer-lodash-method
|
||||
drivelist.listAsync().map((drive) => {
|
||||
drive.adapter = this.id
|
||||
|
||||
// TODO: Find a better way to detect that a certain
|
||||
// block device is a compute module initialized
|
||||
// through usbboot.
|
||||
if (_.includes([ '0001', 'RPi-MSD- 0001', 'File-Stor Gadget', 'Linux File-Stor Gadget USB Device' ], drive.description)) {
|
||||
drive.description = 'Compute Module'
|
||||
drive.icon = 'raspberrypi'
|
||||
drive.system = false
|
||||
}
|
||||
|
||||
return drive
|
||||
}).catch((error) => {
|
||||
callback(error)
|
||||
}).filter((drive) => {
|
||||
return options.includeSystemDrives || !drive.system
|
||||
}).then((drives) => {
|
||||
this.emit('devices', drives)
|
||||
callback(null, drives)
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary The name of this adapter
|
||||
* @public
|
||||
* @type {String}
|
||||
* @constant
|
||||
*/
|
||||
exports.name = 'standard'
|
||||
StandardAdapter.id = 'standard'
|
||||
|
||||
/**
|
||||
* @summary Scan for block devices
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} [options] - options
|
||||
* @param {Object} [options.includeSystemDrives=false] - include system drives
|
||||
* @fulfil {Object[]} - block devices
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* standard.scan({
|
||||
* includeSystemDrives: true
|
||||
* }).each((device) => {
|
||||
* console.log(device)
|
||||
* })
|
||||
*/
|
||||
exports.scan = (options = {}) => {
|
||||
// eslint-disable-next-line lodash/prefer-lodash-method
|
||||
return drivelist.listAsync().map((drive) => {
|
||||
drive.adaptor = exports.name
|
||||
|
||||
// TODO: Find a better way to detect that a certain
|
||||
// block device is a compute module initialized
|
||||
// through usbboot.
|
||||
if (_.includes([ '0001', 'RPi-MSD- 0001', 'File-Stor Gadget', 'Linux File-Stor Gadget USB Device' ], drive.description)) {
|
||||
drive.description = 'Compute Module'
|
||||
drive.icon = 'raspberrypi'
|
||||
drive.system = false
|
||||
}
|
||||
|
||||
return drive
|
||||
}).filter((drive) => {
|
||||
return options.includeSystemDrives || !drive.system
|
||||
})
|
||||
}
|
||||
// Exports
|
||||
module.exports = StandardAdapter
|
||||
|
@ -22,18 +22,27 @@
|
||||
'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 name of this adaptor
|
||||
* @public
|
||||
* @type {String}
|
||||
* @summary The radix used by USB ID numbers
|
||||
* @type {Number}
|
||||
* @constant
|
||||
*/
|
||||
exports.name = 'usbboot'
|
||||
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"
|
||||
@ -56,29 +65,6 @@ const USB_PRODUCT_ID_BCM2708_BOOT = 0x2763
|
||||
*/
|
||||
const USB_PRODUCT_ID_BCM2710_BOOT = 0x2764
|
||||
|
||||
/**
|
||||
* @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 The timeout for USB device operations
|
||||
* @type {Number}
|
||||
@ -107,6 +93,45 @@ const USB_DESCRIPTOR_NULL_INDEX = 0
|
||||
*/
|
||||
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
|
||||
@ -128,315 +153,307 @@ const isUsbBootCapableUSBDevice = (device) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary The radix used by USB ID numbers
|
||||
* @type {Number}
|
||||
* @constant
|
||||
* @summary USBBootAdapter
|
||||
* @class
|
||||
*/
|
||||
const USB_ID_RADIX = 16
|
||||
class USBBootAdapter extends EventEmitter {
|
||||
/**
|
||||
* @summary USBBootAdapter constructor
|
||||
* @class
|
||||
* @example
|
||||
* const adapter = new USBBootAdapter()
|
||||
*/
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
/**
|
||||
* @summary The expected length of a USB ID number
|
||||
* @type {Number}
|
||||
* @constant
|
||||
*/
|
||||
const USB_ID_LENGTH = 4
|
||||
/** @type {String} Adapter name */
|
||||
this.id = this.constructor.id
|
||||
|
||||
/**
|
||||
* @summary The cache of blobs
|
||||
* @type {Object}
|
||||
* @constant
|
||||
*/
|
||||
const BLOBS_CACHE = {}
|
||||
|
||||
/**
|
||||
* @summary Query a blob from the internal cache
|
||||
* @function
|
||||
* @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 = queryBlobFromCache('start.elf', (name) => {
|
||||
* return fs.readFileAsync(path.join('./blobs', name))
|
||||
* })
|
||||
*/
|
||||
const queryBlobFromCache = (name, fallback) => {
|
||||
if (BLOBS_CACHE[name]) {
|
||||
return Bluebird.resolve(BLOBS_CACHE[name])
|
||||
/** @type {Object} Blob cache */
|
||||
this.blobCache = {}
|
||||
}
|
||||
|
||||
return fallback(name).tap((buffer) => {
|
||||
BLOBS_CACHE[name] = buffer
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 Write bootcode to USB device (usbboot first stage)
|
||||
* @function
|
||||
* @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')
|
||||
*
|
||||
* writeBootCode(device, device.interfaces(0).endpoint(1), bootcode).then(() => {
|
||||
* console.log('Done!')
|
||||
* })
|
||||
*/
|
||||
const 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 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])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Mount a USB device as a block device (usbboot second stage)
|
||||
* @function
|
||||
* @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)
|
||||
*
|
||||
* startFileServer(device, device.interfaces(0).endpoint(1), (name) => {
|
||||
* return fs.readFileAsync(name)
|
||||
* }).then(() => {
|
||||
* console.log('Done!')
|
||||
* })
|
||||
*/
|
||||
const 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
|
||||
}
|
||||
return fallback(name).tap((buffer) => {
|
||||
this.blobCache[name] = buffer
|
||||
})
|
||||
}
|
||||
|
||||
.then((fileMessage) => {
|
||||
debug(`Received message: ${fileMessage.command} -> ${fileMessage.fileName}`)
|
||||
/**
|
||||
* @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 */
|
||||
|
||||
if (fileMessage.command === protocol.FILE_MESSAGE_COMMANDS.DONE) {
|
||||
debug('Done')
|
||||
return Bluebird.resolve()
|
||||
// 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(() => {
|
||||
if (fileMessage.command === protocol.FILE_MESSAGE_COMMANDS.GET_FILE_SIZE) {
|
||||
debug(`Getting the size of ${fileMessage.fileName}`)
|
||||
const serialNumberIndex = device.deviceDescriptor.iSerialNumber
|
||||
debug(`Serial number index: ${serialNumberIndex}`)
|
||||
|
||||
return 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 (serialNumberIndex === USB_DESCRIPTOR_NULL_INDEX) {
|
||||
return USBBootAdapter.writeBootCode(device, endpoint, _.get(options.files, [
|
||||
USBBOOT_BOOTCODE_FILE_NAME
|
||||
]))
|
||||
}
|
||||
|
||||
if (fileMessage.command === protocol.FILE_MESSAGE_COMMANDS.READ_FILE) {
|
||||
debug(`Reading ${fileMessage.fileName}`)
|
||||
|
||||
return 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 startFileServer(device, endpoint, readFile)
|
||||
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 Scan for usbboot capable USB devices
|
||||
* @function
|
||||
* @summary The name of this adapter
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* You should at the very least pass a file named `bootcode.bin`.
|
||||
*
|
||||
* @param {Object} options - options
|
||||
* @param {Function} options.readFile - file reading function
|
||||
* @fulfil {Object[]} - USB devices
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* const fs = Bluebird.promisifyAll(require('fs'))
|
||||
*
|
||||
* usbboot.scan({
|
||||
* readFile: (name) => {
|
||||
* return fs.readFileAsync(name)
|
||||
* }
|
||||
* }).each((device) => {
|
||||
* console.log(device)
|
||||
* })
|
||||
* @type {String}
|
||||
* @constant
|
||||
*/
|
||||
exports.scan = (options) => {
|
||||
/* eslint-disable lodash/prefer-lodash-method */
|
||||
return usb.listDevices().filter(isUsbBootCapableUSBDevice).map((device) => {
|
||||
/* eslint-enable lodash/prefer-lodash-method */
|
||||
USBBootAdapter.id = 'usbboot'
|
||||
|
||||
// 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 queryBlobFromCache(USBBOOT_BOOTCODE_FILE_NAME, options.readFile).then((bootcode) => {
|
||||
return writeBootCode(device, endpoint, bootcode)
|
||||
})
|
||||
}
|
||||
|
||||
debug('Starting file server')
|
||||
return startFileServer(device, endpoint, options.readFile)
|
||||
}).return(device).catch({
|
||||
message: 'LIBUSB_TRANSFER_CANCELLED'
|
||||
}, {
|
||||
message: 'LIBUSB_ERROR_NO_DEVICE'
|
||||
}, _.constant(null)).tap((result) => {
|
||||
if (result) {
|
||||
result.close()
|
||||
}
|
||||
})
|
||||
|
||||
// See http://bluebirdjs.com/docs/api/promise.map.html
|
||||
}, {
|
||||
concurrency: 5
|
||||
}).then(_.compact)
|
||||
}
|
||||
// Exports
|
||||
module.exports = USBBootAdapter
|
||||
|
@ -160,7 +160,7 @@ describe('Browser: driveScanner', function () {
|
||||
path: '/mnt/foo'
|
||||
}
|
||||
],
|
||||
adaptor: 'standard',
|
||||
adapter: 'standard',
|
||||
system: false
|
||||
},
|
||||
{
|
||||
@ -173,7 +173,7 @@ describe('Browser: driveScanner', function () {
|
||||
path: '/mnt/bar'
|
||||
}
|
||||
],
|
||||
adaptor: 'standard',
|
||||
adapter: 'standard',
|
||||
system: false
|
||||
}
|
||||
])
|
||||
@ -254,7 +254,7 @@ describe('Browser: driveScanner', function () {
|
||||
description: 'Foo',
|
||||
size: '14G',
|
||||
mountpoints: [],
|
||||
adaptor: 'standard',
|
||||
adapter: 'standard',
|
||||
system: false
|
||||
},
|
||||
{
|
||||
@ -267,7 +267,7 @@ describe('Browser: driveScanner', function () {
|
||||
path: 'F:'
|
||||
}
|
||||
],
|
||||
adaptor: 'standard',
|
||||
adapter: 'standard',
|
||||
system: false
|
||||
}
|
||||
])
|
||||
|
Loading…
x
Reference in New Issue
Block a user