Skip to content
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,47 @@ const device = await usb.openDeviceById('0123456789abcdef01234567');
await device.reset();
```

#### In the browser

Browsers only expose devices the user has explicitly granted access to, which the browser's device
picker asks for.

`getDevices()` returns the devices already granted, and shows the picker so the user can add one.
The devices come back unopened:

```js
button.addEventListener('click', async () => {
const devices = await usb.getDevices();
if (devices.length === 0) {
throw new Error('No devices found');
}
const device = devices[0];
await device.open();
});
```

`openDeviceById()` shows the picker only if that device hasn't been granted access yet, and returns
the device already open:

```js
button.addEventListener('click', async () => {
const device = await usb.openDeviceById('0123456789abcdef01234567');
await device.reset();
});
```

`requestDevice()` always shows the picker and resolves to the device the user selected, unopened:

```js
button.addEventListener('click', async () => {
const device = await usb.requestDevice(); // Throws a NotFoundError if the user cancels
await device.open();
});
```

All three must be called from a user gesture, such as a click handler. Outside of one the browser
refuses to show the picker, so you only get back devices that were already granted access.

The device should be closed when it is no longer needed:

```js
Expand Down
29 changes: 27 additions & 2 deletions src/device-base.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict';
const { getUsbDevices, UsbDevice, MAX_CONTROL_TRANSFER_DATA_SIZE } = require('./usb-device-node');
const { getUsbDevices, requestUsbDevice, 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, NotAllowedError, assert } = require('./error');
Expand Down Expand Up @@ -815,11 +815,36 @@ async function openNativeUsbDevice(nativeUsbDevice, options = null) {
return dev;
}

async function requestDevice({ types = [], includeDfu = true } = {}) {
types = types.map(type => type.toLowerCase());
const filters = [];
PLATFORMS.forEach((platform) => {
if (types.length === 0 || types.includes(platform.name)) {
if (platform && platform.usb && platform.usb.vendorId) {
filters.push(platform.usb);
}
if (includeDfu && platform && platform.dfu && platform.dfu.vendorId) {
filters.push(platform.dfu);
}
}
});

if (filters.length === 0) {
// Requesting with no filters would let the user pick any USB device attached to the host
throw new RangeError('No supported device types matched the requested types');
}
const dev = await requestUsbDevice(filters);
const platform = platformForUsbIds(dev.vendorId, dev.productId);
assert(platform);
return new DeviceBase(dev, platform);
}

module.exports = {
PollingPolicy,
DeviceBase,
getDevices,
openDeviceById,
openNativeUsbDevice,
platformForUsbIds // For testing
platformForUsbIds, // For testing
requestDevice
};
18 changes: 17 additions & 1 deletion src/particle-usb.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict';
const { getDevices: getUsbDevices, openDeviceById: openUsbDeviceById, openNativeUsbDevice: openUsbNativeUsbDevice } = require('./device-base');
const { getDevices: getUsbDevices, openDeviceById: openUsbDeviceById, openNativeUsbDevice: openUsbNativeUsbDevice, requestDevice: requestUsbDevice } = require('./device-base');
const { PollingPolicy } = require('./device-base');
const { FirmwareModule, FirmwareModuleDisplayNames } = require('./device');
const { NetworkStatus } = require('./network-device');
Expand Down Expand Up @@ -47,6 +47,21 @@ function openNativeUsbDevice(nativeUsbDevice, options) {
return openUsbNativeUsbDevice(nativeUsbDevice, options).then(dev => setDevicePrototype(dev));
}

/**
* Prompt the user to grant access to a Particle USB device. (Web Browser only)
* NOTE: This method must be called from a user gesture (click) in other case the browser will reject the call
* @param {Object} [options] Options.
* @param {Array<String>} [options.types] Device types (photon, boron, tracker, etc). By default,
* the user can pick a device of any platform supported by the library.
* @param {Boolean} [options.includeDfu=true] Whether to include devices in DFU mode.
* @return {Promise<Device>} The device the user has selected.
* @throws {NotFoundError} The user dismissed the prompt without selecting a device.
* @throws {NotAllowedError} Called outside of a browser environment.
*/
function requestDevice(options) {
return requestUsbDevice(options).then(dev => setDevicePrototype(dev));
}

/**
* Get devices in Qualcomm EDL mode.
*
Expand Down Expand Up @@ -85,5 +100,6 @@ module.exports = {
openDeviceById,
openNativeUsbDevice,
getEdlDevices,
requestDevice,
config
};
1 change: 1 addition & 0 deletions src/particle-usb.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe('Public interface of npm module', () => {
expect(particleUSB.getDevices).to.be.a('Function');
expect(particleUSB.openDeviceById).to.be.a('Function');
expect(particleUSB.openNativeUsbDevice).to.be.a('Function');
expect(particleUSB.requestDevice).to.be.a('Function');
expect(particleUSB.PollingPolicy).to.be.an('object');
expect(particleUSB.PollingPolicy.DEFAULT).to.be.a('Function');

Expand Down
8 changes: 7 additions & 1 deletion src/usb-device-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,14 @@ async function getUsbDevices(filters) {
return devs;
}

async function requestUsbDevice(/* filters */) {
// Requesting a permission is a browser-only concept, use getUsbDevices instead
throw new NotAllowedError('requestDevice() is only supported in the browser');
}

module.exports = {
MAX_CONTROL_TRANSFER_DATA_SIZE,
UsbDevice,
getUsbDevices
getUsbDevices,
requestUsbDevice
};
47 changes: 32 additions & 15 deletions src/usb-device-webusb.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict';
const { UsbError, UsbStallError } = require('./error');
const { UsbError, UsbStallError, NotFoundError } = require('./error');

// Maximum size of a control transfer's data stage
const MAX_CONTROL_TRANSFER_DATA_SIZE = 4096;
Expand Down Expand Up @@ -179,26 +179,29 @@ async function getUsbDevices(filters) {
}
let devs = [];
try {
// Fow now, always ask the user to grant access to the device, even if we already have a
// permission to access it. The permissions API for USB is not yet implemented in Chrome,
// and calling requestDevice() after getDevices() causes a SecurityError.
// For now it will always prompt the user unless we pass a serialNumber filter
// if we pass a serialNumber filter and the devices.legth is 0 then we will prompt it
// TODO: Implement a separate API to request a permission from the user
let newDev = null;
try {
newDev = await navigator.usb.requestDevice({ filters });
Comment thread
hugomontero marked this conversation as resolved.
} catch (e) {
// Ignore NotFoundError which means that the user has cancelled the request
if (e.name !== 'NotFoundError') {
throw e;
}
}
// Get the list of known devices and filter them according to the provided options
devs = await navigator.usb.getDevices();
let newDev = null;
if (filters.length > 0) {
devs = devs.filter(dev => filters.some(f => ((!f.vendorId || dev.vendorId === f.vendorId) &&
(!f.productId || dev.productId === f.productId) &&
(!f.serialNumber || dev.serialNumber === f.serialNumber))));
}

const filteredById = filters.some(f => f.serialNumber);
const alreadyPermitted = (filteredById && devs.length > 0);
if (!alreadyPermitted) {
try {
newDev = await navigator.usb.requestDevice({ filters });
} catch (e) {
// Ignore NotFoundError which means that the user has cancelled the request
if (e.name !== 'NotFoundError') {
throw e;
}
}
}
if (newDev) {
// Avoid listing the same device twice
const hasNewDev = devs.some(dev => dev.vendorId === newDev.vendorId && dev.productId === newDev.productId &&
Expand All @@ -214,8 +217,22 @@ async function getUsbDevices(filters) {
return devs;
}

async function requestUsbDevice(filters){
try {
const dev = await navigator.usb.requestDevice({ filters });
return new UsbDevice(dev);
} catch (err) {
if (err.name === 'NotFoundError') {
throw new NotFoundError('No device selected', { cause: err });
}
throw new UsbError('Unable to request a USB device', { cause: err });
}
}


module.exports = {
MAX_CONTROL_TRANSFER_DATA_SIZE,
UsbDevice,
getUsbDevices
getUsbDevices,
requestUsbDevice
};
Loading