Skip to content

Commit 9245b68

Browse files
committed
add deviceId filter for requestDevice
1 parent e6bc3ae commit 9245b68

4 files changed

Lines changed: 82 additions & 3 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ button.addEventListener('click', async () => {
9595
});
9696
```
9797

98+
Pass an `id` to scope the picker to a single device. The picker lists that device in whichever mode it
99+
is currently in, and updates live as it enumerates, which is useful to grant access to a device that is
100+
about to enter DFU mode (the browser keeps a separate grant for the DFU mode of a device):
101+
102+
```js
103+
button.addEventListener('click', async () => {
104+
const device = await usb.requestDevice({ id: '0123456789abcdef01234567' });
105+
await device.open();
106+
});
107+
```
108+
98109
All three must be called from a user gesture, such as a click handler. Outside of one the browser
99110
refuses to show the picker, so you only get back devices that were already granted access.
100111

src/device-base.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -815,16 +815,17 @@ async function openNativeUsbDevice(nativeUsbDevice, options = null) {
815815
return dev;
816816
}
817817

818-
async function requestDevice({ types = [], includeDfu = true } = {}) {
818+
async function requestDevice({ types = [], includeDfu = true, id } = {}) {
819819
types = types.map(type => type.toLowerCase());
820820
const filters = [];
821+
const addFilter = (usbIds) => filters.push(id ? Object.assign({ serialNumber: id }, usbIds) : usbIds);
821822
PLATFORMS.forEach((platform) => {
822823
if (types.length === 0 || types.includes(platform.name)) {
823824
if (platform && platform.usb && platform.usb.vendorId) {
824-
filters.push(platform.usb);
825+
addFilter(platform.usb);
825826
}
826827
if (includeDfu && platform && platform.dfu && platform.dfu.vendorId) {
827-
filters.push(platform.dfu);
828+
addFilter(platform.dfu);
828829
}
829830
}
830831
});

src/device-base.test.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ const proxyquire = require('proxyquire');
55
const { getDevices, openDeviceById, openNativeUsbDevice, PollingPolicy } = proxyquire('../src/device-base', {
66
'./usb-device-node': fakeUsb
77
});
8+
// requestDevice() is browser-only, so the node implementation always throws. Stub it out to inspect
9+
// the filters the library passes down to WebUSB.
10+
const requestUsbDeviceStub = sinon.stub();
11+
const { requestDevice } = proxyquire('../src/device-base', {
12+
'./usb-device-node': Object.assign({}, fakeUsb, { requestUsbDevice: requestUsbDeviceStub })
13+
});
814
const usbImpl = require('./usb-device-node');
915
const proto = require('./usb-protocol');
1016
const error = require('./error');
@@ -195,6 +201,65 @@ describe('device-base', () => {
195201
});
196202
});
197203

204+
describe('requestDevice()', () => {
205+
beforeEach(() => {
206+
requestUsbDeviceStub.reset();
207+
});
208+
209+
it('does not filter by serial number by default', async () => {
210+
const photon = fakeUsb.addPhoton();
211+
requestUsbDeviceStub.resolves(photon);
212+
const dev = await requestDevice();
213+
expect(dev.usbDevice).to.equal(photon);
214+
const filters = requestUsbDeviceStub.firstCall.args[0];
215+
expect(filters).to.not.be.empty;
216+
expect(filters.every(f => f.serialNumber === undefined)).to.be.true;
217+
});
218+
219+
it('filters by serial number when an ID is specified', async () => {
220+
const photon = fakeUsb.addPhoton({ id: '111111111111111111111111' });
221+
requestUsbDeviceStub.resolves(photon);
222+
const dev = await requestDevice({ id: '111111111111111111111111' });
223+
expect(dev.usbDevice).to.equal(photon);
224+
const filters = requestUsbDeviceStub.firstCall.args[0];
225+
expect(filters).to.not.be.empty;
226+
expect(filters.every(f => f.serialNumber === '111111111111111111111111')).to.be.true;
227+
// The ID applies to both the regular and the DFU USB IDs of each platform
228+
expect(filters).to.containSubset([{
229+
serialNumber: '111111111111111111111111',
230+
vendorId: photon.vendorId,
231+
productId: photon.productId
232+
}]);
233+
});
234+
235+
it('filters by serial number for the requested device types only', async () => {
236+
const photon = fakeUsb.addPhoton({ id: '111111111111111111111111' });
237+
requestUsbDeviceStub.resolves(photon);
238+
await requestDevice({ types: ['photon'], includeDfu: false, id: '111111111111111111111111' });
239+
const filters = requestUsbDeviceStub.firstCall.args[0];
240+
expect(filters).to.have.lengthOf(1);
241+
expect(filters).to.containSubset([{
242+
serialNumber: '111111111111111111111111',
243+
vendorId: photon.vendorId,
244+
productId: photon.productId
245+
}]);
246+
});
247+
248+
it('does not modify the platform USB IDs', async () => {
249+
const photon = fakeUsb.addPhoton({ id: '111111111111111111111111' });
250+
requestUsbDeviceStub.resolves(photon);
251+
await requestDevice({ id: '111111111111111111111111' });
252+
await requestDevice();
253+
const filters = requestUsbDeviceStub.secondCall.args[0];
254+
expect(filters.every(f => f.serialNumber === undefined)).to.be.true;
255+
});
256+
257+
it('fails if no device type matches the requested types', async () => {
258+
await expect(requestDevice({ types: ['nonexistent'] })).to.be.rejectedWith(RangeError);
259+
expect(requestUsbDeviceStub).to.have.not.been.called;
260+
});
261+
});
262+
198263
describe('openNativeUsbDevice()', () => {
199264
it('opens a native usb device', async () => {
200265
const fakeNativeDevice = {

src/particle-usb.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ function openNativeUsbDevice(nativeUsbDevice, options) {
5454
* @param {Array<String>} [options.types] Device types (photon, boron, tracker, etc). By default,
5555
* the user can pick a device of any platform supported by the library.
5656
* @param {Boolean} [options.includeDfu=true] Whether to include devices in DFU mode.
57+
* @param {String} [options.id] Device ID. If specified, the picker only lists the device with that ID,
58+
* in whichever mode it is currently in. By default, the user can pick any matching device.
5759
* @return {Promise<Device>} The device the user has selected.
5860
* @throws {NotFoundError} The user dismissed the prompt without selecting a device.
5961
* @throws {NotAllowedError} Called outside of a browser environment.

0 commit comments

Comments
 (0)