diff --git a/src/device-base.js b/src/device-base.js index 16b6f472..87d74c04 100644 --- a/src/device-base.js +++ b/src/device-base.js @@ -1,4 +1,4 @@ -const { getUsbDevices, MAX_CONTROL_TRANSFER_DATA_SIZE } = require('./usb-device-node'); +const { getUsbDevices, UsbDevice, MAX_CONTROL_TRANSFER_DATA_SIZE } = require('./usb-device-node'); const proto = require('./usb-protocol'); const { PLATFORMS } = require('./platforms'); const { DeviceError, NotFoundError, StateError, TimeoutError, MemoryError, ProtocolError, assert } = require('./error'); @@ -774,9 +774,25 @@ async function openDeviceById(id, options = null) { return dev; } +async function openNativeUsbDevice(nativeUsbDevice, options = null) { + + const usbDevice = new UsbDevice(nativeUsbDevice); + + const platform = platformForUsbIds(usbDevice.vendorId, usbDevice.productId); + if (!platform) { + throw new NotFoundError('Unsupported device type'); + } + const dev = new DeviceBase(usbDevice, platform); + + await dev.open(options); + + return dev; +} + module.exports = { PollingPolicy, DeviceBase, getDevices, - openDeviceById + openDeviceById, + openNativeUsbDevice }; diff --git a/src/device-base.test.js b/src/device-base.test.js index 3812645b..b9d215c8 100644 --- a/src/device-base.test.js +++ b/src/device-base.test.js @@ -1,7 +1,7 @@ const { fakeUsb, sinon, expect, assert, nextTick } = require('../test/support'); const proxyquire = require('proxyquire'); -const { getDevices, openDeviceById, PollingPolicy } = proxyquire('../src/device-base', { +const { getDevices, openDeviceById, openNativeUsbDevice, PollingPolicy } = proxyquire('../src/device-base', { './usb-device-node': fakeUsb }); const usbImpl = require('./usb-device-node'); @@ -195,6 +195,47 @@ describe('device-base', () => { }); }); + describe('openNativeUsbDevice()', () => { + it('opens a native usb device', async () => { + const fakeNativeDevice = { + open: function() { + }, + close: function() { + }, + deviceDescriptor: { + iSerialNumber: '111111111111111111111111', + idVendor: 0x2b04, + idProduct: 0xc00a, + }, + getStringDescriptor: function(s, fn) { + fn(null, s); + } + }; + const dev = await openNativeUsbDevice(fakeNativeDevice); + expect(dev.isOpen).to.be.true; + expect(dev.id).to.equal('111111111111111111111111'); + }); + + it('opens throws an exception on invalid device', async () => { + const fakeNativeDevice = { + open: function() { + }, + close: function() { + }, + deviceDescriptor: { + iSerialNumber: '111111111111111111111111', + idVendor: 0x2b04, + idProduct: 0xcfff, + }, + getStringDescriptor: function(s, fn) { + fn(null, s); + } + }; + const dev = openNativeUsbDevice(fakeNativeDevice); + await expect(dev).to.be.rejectedWith(error.NotFoundError); + }); + }); + describe('DeviceBase', () => { let dev = null; let usbDev = null; diff --git a/src/device.js b/src/device.js index 21a1fb27..35349ab0 100644 --- a/src/device.js +++ b/src/device.js @@ -189,10 +189,13 @@ class Device extends DeviceBase { * - Gen 2 (since Device OS 0.8.0) * * @param {Object} [options] Options. + * @param {Boolean} [options.noReconnectWait] After entering DFU mode, do not attempt to connect to the device to make sure it's in DFU mode. + * This can be useful in a web browser because connecting to the device in DFU mode may prompt the user to authorize + * access to the device. * @param {Number} [options.timeout] Timeout (milliseconds). * @return {Promise} */ - enterDfuMode({ timeout = globalOptions.requestTimeout } = {}) { + enterDfuMode({ noReconnectWait = false, timeout = globalOptions.requestTimeout } = {}) { if (this.isInDfuMode) { return; } @@ -201,19 +204,22 @@ class Device extends DeviceBase { await s.close(); let isInDfuMode; - while (!isInDfuMode) { - try { - await s.open({ includeDfu: true }); - isInDfuMode = s.device.isInDfuMode; - } catch (error) { - // device is reconnecting, ignore + if (!noReconnectWait) { + while (!isInDfuMode) { + try { + await s.open({ includeDfu: true }); + isInDfuMode = s.device.isInDfuMode; + } catch (error) { + // device is reconnecting, ignore + } + await s.close(); + await s.delay(500); } - await s.close(); - await s.delay(500); } }); } + /** * Reset and enter the safe mode. * diff --git a/src/particle-usb.js b/src/particle-usb.js index 45c9b2d0..602451ac 100644 --- a/src/particle-usb.js +++ b/src/particle-usb.js @@ -1,4 +1,4 @@ -const { getDevices: getUsbDevices, openDeviceById: openUsbDeviceById } = require('./device-base'); +const { getDevices: getUsbDevices, openDeviceById: openUsbDeviceById, openNativeUsbDevice: openUsbNativeUsbDevice } = require('./device-base'); const { PollingPolicy } = require('./device-base'); const { FirmwareModule } = require('./device'); const { NetworkStatus } = require('./network-device'); @@ -33,6 +33,17 @@ function openDeviceById(id, options) { return openUsbDeviceById(id, options).then(dev => setDevicePrototype(dev)); } +/** + * Open a Particle USB device from a native browser or node USB device handle + * + * @param {Object} nativeUsbDevice A WebUSB (browser) or node-usb USB device + * @param {Object} [options] Options (see {@link DeviceBase#open}). + * @return {Promise} + */ +function openNativeUsbDevice(nativeUsbDevice, options) { + return openUsbNativeUsbDevice(nativeUsbDevice, options).then(dev => setDevicePrototype(dev)); +} + module.exports = { PollingPolicy, FirmwareModule, @@ -56,5 +67,6 @@ module.exports = { RequestError, getDevices, openDeviceById, + openNativeUsbDevice, config }; diff --git a/src/particle-usb.test.js b/src/particle-usb.test.js index 02170845..8846cf36 100644 --- a/src/particle-usb.test.js +++ b/src/particle-usb.test.js @@ -4,6 +4,7 @@ describe('Public interface of npm module', () => { it('exports expected objects and functions', () => { expect(particleUSB.getDevices).to.be.a('Function'); expect(particleUSB.openDeviceById).to.be.a('Function'); + expect(particleUSB.openNativeUsbDevice).to.be.a('Function'); expect(particleUSB.PollingPolicy).to.be.an('object'); expect(particleUSB.PollingPolicy.DEFAULT).to.be.a('Function'); diff --git a/test/e2e/browser.e2e.js b/test/e2e/browser.e2e.js index 6deda01d..263fc4b1 100644 --- a/test/e2e/browser.e2e.js +++ b/test/e2e/browser.e2e.js @@ -100,6 +100,7 @@ describe('Browser Usage', () => { await page.evaluate(async (id) => { const webDevice = await ParticleUsb.openDeviceById(id); await webDevice.reset(); + await webDevice.close(); }, deviceId); }); @@ -114,6 +115,25 @@ describe('Browser Usage', () => { expect(mode).to.equal('LISTENING'); }); + + it('Enters listening mode using a native webusb device reference', async () => { + const mode = await page.evaluate(async () => { + const filters = [ + { vendorId: 0x2b04 } + ]; + + const nativeUsbDevice = await navigator.usb.requestDevice({ + filters + }); + + const webDevice = await ParticleUsb.openNativeUsbDevice(nativeUsbDevice); + + await webDevice.enterListeningMode(); + return await webDevice.getDeviceMode(); + }); + + expect(mode).to.equal('LISTENING'); + }); }); async function setupDevices(page){ diff --git a/test/e2e/node.e2e.js b/test/e2e/node.e2e.js index 598f2e31..69e2dccf 100644 --- a/test/e2e/node.e2e.js +++ b/test/e2e/node.e2e.js @@ -71,13 +71,14 @@ describe('Node.js Usage', () => { }); describe('Basic Device Interactions [@device]', () => { - let device, devices; + let device, devices, deviceId; beforeEach(async () => { const usb = require(PROJ_NODE_DIR); devices = await usb.getDevices(); device = devices[0]; await device.open(); + deviceId = device.id; }); afterEach(async () => { @@ -95,6 +96,14 @@ describe('Node.js Usage', () => { 'CONNECTED' ]); }); + + it('Opens device using a native usb device reference', async () => { + const { openNativeUsbDevice } = require(PROJ_NODE_DIR); + await device.close(); + const nativeUsbDevice = device._dev._dev; + device = await openNativeUsbDevice(nativeUsbDevice); + expect(deviceId).to.equal(device.id); + }); }); }); diff --git a/test/integration/device-base.js b/test/integration/device-base.js index 27016aa4..e9e05639 100644 --- a/test/integration/device-base.js +++ b/test/integration/device-base.js @@ -1,4 +1,4 @@ -const { getDevices, openDeviceById } = require('../../src/particle-usb'); +const { getDevices, openDeviceById, openNativeUsbDevice } = require('../../src/particle-usb'); const { MAX_CONTROL_TRANSFER_DATA_SIZE } = require('../../src/usb-device-node'); const { expect, randomString, integrationTest } = require('../support'); @@ -79,4 +79,23 @@ describe('device-base', function desc() { }); }); }); + + + describe('openNativeUsbDevice()', () => { + it('it can open a native USB device handle', async () => { + if (devs.length < 1) { + throw new Error('This test requires a device'); + } + const dev1 = devs[0]; + await dev1.open(); + const id1 = dev1.id; + await dev1.close(); + const nativeUsbDevice = dev1.usbDevice.internalObject; + const dev2 = await openNativeUsbDevice(nativeUsbDevice); + const id2 = dev2.id; + await dev2.close(); + expect(id1).to.equal(id2); + }); + }); + });