Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open Native USB Feature #75

Merged
merged 9 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/device-base.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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
};
43 changes: 42 additions & 1 deletion src/device-base.test.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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;
Expand Down
24 changes: 15 additions & 9 deletions src/device.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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.
*
Expand Down
14 changes: 13 additions & 1 deletion src/particle-usb.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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<Device>}
*/
function openNativeUsbDevice(nativeUsbDevice, options) {
return openUsbNativeUsbDevice(nativeUsbDevice, options).then(dev => setDevicePrototype(dev));
}

module.exports = {
PollingPolicy,
FirmwareModule,
Expand All @@ -56,5 +67,6 @@ module.exports = {
RequestError,
getDevices,
openDeviceById,
openNativeUsbDevice,
config
};
1 change: 1 addition & 0 deletions src/particle-usb.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
20 changes: 20 additions & 0 deletions test/e2e/browser.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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){
Expand Down
11 changes: 10 additions & 1 deletion test/e2e/node.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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);
});
});
});

21 changes: 20 additions & 1 deletion test/integration/device-base.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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);
});
});
busticated marked this conversation as resolved.
Show resolved Hide resolved

});