From e270270206175e1066bf3acefb2783b46e2826f9 Mon Sep 17 00:00:00 2001 From: rickkas7 Date: Fri, 23 Sep 2022 14:10:56 -0400 Subject: [PATCH 1/9] checkpoint - work in progress, not currently working - do not merge --- src/device-base.js | 21 +++++++++++++++++++-- src/device-base.test.js | 25 ++++++++++++++++++++++++- src/device.js | 24 +++++++++++++++--------- src/particle-usb.js | 14 +++++++++++++- src/particle-usb.test.js | 1 + test/integration/device-base.js | 25 ++++++++++++++++++++++++- 6 files changed, 96 insertions(+), 14 deletions(-) diff --git a/src/device-base.js b/src/device-base.js index 16b6f472..32e53410 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,26 @@ async function openDeviceById(id, options = null) { return dev; } +async function openNativeUsbDevice(nativeUsbDevice, options = null) { + console.log('openNativeUsbDevice nativeUsbDevice', nativeUsbDevice); + + const usbDevice = new UsbDevice(nativeUsbDevice); + console.log('openNativeUsbDevice usbDevice', usbDevice); + + const platform = platformForUsbIds(usbDevice.vendorId, usbDevice.productId); + assert(platform); + console.log('openNativeUsbDevice platform', platform); + const dev = new DeviceBase(usbDevice, platform); + console.log('openNativeUsbDevice dev', dev); + await dev.open(options); + console.log('open complete isOpen=' + dev.isOpen); + 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..88715f8b 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,29 @@ 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'); + }); + + }); + 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/integration/device-base.js b/test/integration/device-base.js index 27016aa4..99eb2a96 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,27 @@ 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]; + dev1.open(); + const id1 = dev1.id; + dev1.close(); + + const nativeUsbDevice = dev1._dev; + + const dev2 = openNativeUsbDevice(nativeUsbDevice); + dev2.open(); + const id2 = dev2.id; + dev2.close(); + + expect(id1).to.equal(id2); + }); + }); + }); From 79971a21354f9c9e966b8e180ab0b3339e22e71c Mon Sep 17 00:00:00 2001 From: rickkas7 Date: Mon, 17 Oct 2022 09:40:20 -0400 Subject: [PATCH 2/9] remove debugging statements --- src/device-base.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/device-base.js b/src/device-base.js index 32e53410..f4346f15 100644 --- a/src/device-base.js +++ b/src/device-base.js @@ -775,18 +775,15 @@ async function openDeviceById(id, options = null) { } async function openNativeUsbDevice(nativeUsbDevice, options = null) { - console.log('openNativeUsbDevice nativeUsbDevice', nativeUsbDevice); const usbDevice = new UsbDevice(nativeUsbDevice); - console.log('openNativeUsbDevice usbDevice', usbDevice); const platform = platformForUsbIds(usbDevice.vendorId, usbDevice.productId); assert(platform); - console.log('openNativeUsbDevice platform', platform); const dev = new DeviceBase(usbDevice, platform); - console.log('openNativeUsbDevice dev', dev); + await dev.open(options); - console.log('open complete isOpen=' + dev.isOpen); + return dev; } From 8faab457fdcbbf5280c7658ffce18a66619982df Mon Sep 17 00:00:00 2001 From: rickkas7 Date: Tue, 18 Oct 2022 06:03:28 -0400 Subject: [PATCH 3/9] add optional e2e test for openNativeUsbDevice --- test/e2e/__fixtures__/web-proj/index.html | 31 +++++++++++++++ test/e2e/browser.e2e.js | 46 +++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/test/e2e/__fixtures__/web-proj/index.html b/test/e2e/__fixtures__/web-proj/index.html index 1111b620..063feeea 100644 --- a/test/e2e/__fixtures__/web-proj/index.html +++ b/test/e2e/__fixtures__/web-proj/index.html @@ -13,6 +13,7 @@
+
    diff --git a/test/e2e/browser.e2e.js b/test/e2e/browser.e2e.js index 6deda01d..28f6ac51 100644 --- a/test/e2e/browser.e2e.js +++ b/test/e2e/browser.e2e.js @@ -18,6 +18,7 @@ describe('Browser Usage', () => { ok: '#test-device-ok', error: '#test-device-error', selectDevice: '#btn-selectdevice', + nativeListenButton: '#btn-native-listen', reset: '#btn-reset' }; @@ -126,6 +127,51 @@ describe('Browser Usage', () => { return { deviceId, device, devices }; } + // This is disabled by default because you need to manually select and authorize the device + // to use WebUSB + describe.skip('Changing Device Modes openNativeUsbDevice [@device]', () => { + let deviceId; + + before(async () => { + }); + + afterEach(async () => { + await page.click(selectors.reset); + + await page.evaluate(async (id) => { + const webDevice = await ParticleUsb.openDeviceById(id); + await webDevice.reset(); + }, deviceId); + + }); + + it('Enters listening mode', async () => { + await page.click(selectors.nativeListenButton); + + let result; + + for(let tries = 1; tries < 20; tries++) { + result = await page.evaluate(async () => { + return {deviceId: window.__PRTCL_DEVICE_ID__, deviceMode:window.__PRTCL_DEVICE_MODE__}; + }); + if (result.deviceId) { + break; + } + + await new Promise(function(resolve) { + setTimeout(function() { + resolve(); + }, 1000); + }); + } + deviceId = result.deviceId; + console.log('opened ' + deviceId); + + expect(result.deviceMode).to.equal('LISTENING'); + + }); + }); + function createServer(assets){ return http.createServer((req, res) => { let requestedAsset; From 2e9e6600499b7ce1c1aa67c99e6fb3785bfd2f1e Mon Sep 17 00:00:00 2001 From: rickkas7 Date: Tue, 18 Oct 2022 06:06:54 -0400 Subject: [PATCH 4/9] Fix ci errors in browser.e2e.js --- test/e2e/browser.e2e.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/e2e/browser.e2e.js b/test/e2e/browser.e2e.js index 28f6ac51..75efa3a7 100644 --- a/test/e2e/browser.e2e.js +++ b/test/e2e/browser.e2e.js @@ -137,38 +137,38 @@ describe('Browser Usage', () => { afterEach(async () => { await page.click(selectors.reset); - + await page.evaluate(async (id) => { const webDevice = await ParticleUsb.openDeviceById(id); await webDevice.reset(); }, deviceId); - + }); it('Enters listening mode', async () => { await page.click(selectors.nativeListenButton); - + let result; - for(let tries = 1; tries < 20; tries++) { + for (let tries = 1; tries < 20; tries++) { result = await page.evaluate(async () => { - return {deviceId: window.__PRTCL_DEVICE_ID__, deviceMode:window.__PRTCL_DEVICE_MODE__}; + return { deviceId: window.__PRTCL_DEVICE_ID__, deviceMode:window.__PRTCL_DEVICE_MODE__ }; }); if (result.deviceId) { break; } - + await new Promise(function(resolve) { - setTimeout(function() { - resolve(); - }, 1000); - }); + setTimeout(function() { + resolve(); + }, 1000); + }); } deviceId = result.deviceId; console.log('opened ' + deviceId); expect(result.deviceMode).to.equal('LISTENING'); - + }); }); From 0b8a8a6945fc064c1182c132a44fead3a89c3bc2 Mon Sep 17 00:00:00 2001 From: rickkas7 Date: Wed, 19 Oct 2022 05:20:42 -0400 Subject: [PATCH 5/9] Fix e2e test with much better implementation from review --- test/e2e/__fixtures__/web-proj/index.html | 31 ----------- test/e2e/browser.e2e.js | 66 +++++++---------------- 2 files changed, 20 insertions(+), 77 deletions(-) diff --git a/test/e2e/__fixtures__/web-proj/index.html b/test/e2e/__fixtures__/web-proj/index.html index 063feeea..1111b620 100644 --- a/test/e2e/__fixtures__/web-proj/index.html +++ b/test/e2e/__fixtures__/web-proj/index.html @@ -13,7 +13,6 @@
    -
      diff --git a/test/e2e/browser.e2e.js b/test/e2e/browser.e2e.js index 75efa3a7..263fc4b1 100644 --- a/test/e2e/browser.e2e.js +++ b/test/e2e/browser.e2e.js @@ -18,7 +18,6 @@ describe('Browser Usage', () => { ok: '#test-device-ok', error: '#test-device-error', selectDevice: '#btn-selectdevice', - nativeListenButton: '#btn-native-listen', reset: '#btn-reset' }; @@ -101,6 +100,7 @@ describe('Browser Usage', () => { await page.evaluate(async (id) => { const webDevice = await ParticleUsb.openDeviceById(id); await webDevice.reset(); + await webDevice.close(); }, deviceId); }); @@ -115,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){ @@ -127,51 +146,6 @@ describe('Browser Usage', () => { return { deviceId, device, devices }; } - // This is disabled by default because you need to manually select and authorize the device - // to use WebUSB - describe.skip('Changing Device Modes openNativeUsbDevice [@device]', () => { - let deviceId; - - before(async () => { - }); - - afterEach(async () => { - await page.click(selectors.reset); - - await page.evaluate(async (id) => { - const webDevice = await ParticleUsb.openDeviceById(id); - await webDevice.reset(); - }, deviceId); - - }); - - it('Enters listening mode', async () => { - await page.click(selectors.nativeListenButton); - - let result; - - for (let tries = 1; tries < 20; tries++) { - result = await page.evaluate(async () => { - return { deviceId: window.__PRTCL_DEVICE_ID__, deviceMode:window.__PRTCL_DEVICE_MODE__ }; - }); - if (result.deviceId) { - break; - } - - await new Promise(function(resolve) { - setTimeout(function() { - resolve(); - }, 1000); - }); - } - deviceId = result.deviceId; - console.log('opened ' + deviceId); - - expect(result.deviceMode).to.equal('LISTENING'); - - }); - }); - function createServer(assets){ return http.createServer((req, res) => { let requestedAsset; From 357b7c4e21b7cc3d8be4c1df77bea7f8257407be Mon Sep 17 00:00:00 2001 From: rickkas7 Date: Wed, 19 Oct 2022 05:35:38 -0400 Subject: [PATCH 6/9] Add node e2e test --- test/e2e/node.e2e.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/e2e/node.e2e.js b/test/e2e/node.e2e.js index 598f2e31..7c534608 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; // using `device._dev._dev` instead causes the test to pass + device = await openNativeUsbDevice(nativeUsbDevice); + expect(deviceId).to.equal(device.id); + }); }); }); From 3d5d442fa34e560a212f16a65cf3867c1d153d28 Mon Sep 17 00:00:00 2001 From: rickkas7 Date: Wed, 19 Oct 2022 05:36:11 -0400 Subject: [PATCH 7/9] remove unnecessary comment --- test/e2e/node.e2e.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/node.e2e.js b/test/e2e/node.e2e.js index 7c534608..69e2dccf 100644 --- a/test/e2e/node.e2e.js +++ b/test/e2e/node.e2e.js @@ -100,7 +100,7 @@ describe('Node.js Usage', () => { it('Opens device using a native usb device reference', async () => { const { openNativeUsbDevice } = require(PROJ_NODE_DIR); await device.close(); - const nativeUsbDevice = device._dev._dev; // using `device._dev._dev` instead causes the test to pass + const nativeUsbDevice = device._dev._dev; device = await openNativeUsbDevice(nativeUsbDevice); expect(deviceId).to.equal(device.id); }); From 5b88715b608b526560905986f548800988b0eecb Mon Sep 17 00:00:00 2001 From: rickkas7 Date: Wed, 26 Oct 2022 10:15:35 -0400 Subject: [PATCH 8/9] openNativeUsbDevice unknown device fix Instead of assert, throw an exception. Add a test. --- src/device-base.js | 4 +++- src/device-base.test.js | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/device-base.js b/src/device-base.js index f4346f15..87d74c04 100644 --- a/src/device-base.js +++ b/src/device-base.js @@ -779,7 +779,9 @@ async function openNativeUsbDevice(nativeUsbDevice, options = null) { const usbDevice = new UsbDevice(nativeUsbDevice); const platform = platformForUsbIds(usbDevice.vendorId, usbDevice.productId); - assert(platform); + if (!platform) { + throw new NotFoundError('Unsupported device type'); + } const dev = new DeviceBase(usbDevice, platform); await dev.open(options); diff --git a/src/device-base.test.js b/src/device-base.test.js index 88715f8b..b9d215c8 100644 --- a/src/device-base.test.js +++ b/src/device-base.test.js @@ -216,6 +216,24 @@ describe('device-base', () => { 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', () => { From 4f5491c64ab275d45073fcfc84d22f16728acfcf Mon Sep 17 00:00:00 2001 From: rickkas7 Date: Wed, 26 Oct 2022 11:46:14 -0400 Subject: [PATCH 9/9] Fix device-base e2e openNativeUsbDevice test --- test/integration/device-base.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/test/integration/device-base.js b/test/integration/device-base.js index 99eb2a96..e9e05639 100644 --- a/test/integration/device-base.js +++ b/test/integration/device-base.js @@ -87,17 +87,13 @@ describe('device-base', function desc() { throw new Error('This test requires a device'); } const dev1 = devs[0]; - dev1.open(); + await dev1.open(); const id1 = dev1.id; - dev1.close(); - - const nativeUsbDevice = dev1._dev; - - const dev2 = openNativeUsbDevice(nativeUsbDevice); - dev2.open(); + await dev1.close(); + const nativeUsbDevice = dev1.usbDevice.internalObject; + const dev2 = await openNativeUsbDevice(nativeUsbDevice); const id2 = dev2.id; - dev2.close(); - + await dev2.close(); expect(id1).to.equal(id2); }); });