usb
Version:
Library to access USB devices
295 lines • 11.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebUSB = exports.getWebUsb = void 0;
const usb = require("../usb");
const events_1 = require("events");
const webusb_device_1 = require("./webusb-device");
/**
* Convenience method to get the WebUSB interface available
*/
const getWebUsb = () => {
if (navigator && navigator.usb) {
return navigator.usb;
}
return new WebUSB();
};
exports.getWebUsb = getWebUsb;
class NamedError extends Error {
constructor(message, name) {
super(message);
this.name = name;
}
}
class WebUSB {
constructor(options = {}) {
this.options = options;
this.emitter = new events_1.EventEmitter();
this.knownDevices = new Map();
this.authorisedDevices = new Set();
const deviceConnectCallback = async (device) => {
const webDevice = await this.getWebDevice(device);
// When connected, emit an event if it is an allowed device
if (webDevice && this.isAuthorisedDevice(webDevice)) {
const event = {
type: 'connect',
device: webDevice
};
this.emitter.emit('connect', event);
}
};
const deviceDisconnectCallback = async (device) => {
// When disconnected, emit an event if the device was a known allowed device
if (this.knownDevices.has(device)) {
const webDevice = this.knownDevices.get(device);
if (webDevice && this.isAuthorisedDevice(webDevice)) {
const event = {
type: 'disconnect',
device: webDevice
};
this.emitter.emit('disconnect', event);
}
}
};
this.emitter.on('newListener', event => {
const listenerCount = this.emitter.listenerCount(event);
if (listenerCount !== 0) {
return;
}
if (event === 'connect') {
usb.addListener('attach', deviceConnectCallback);
}
else if (event === 'disconnect') {
usb.addListener('detach', deviceDisconnectCallback);
}
});
this.emitter.on('removeListener', event => {
const listenerCount = this.emitter.listenerCount(event);
if (listenerCount !== 0) {
return;
}
if (event === 'connect') {
usb.removeListener('attach', deviceConnectCallback);
}
else if (event === 'disconnect') {
usb.removeListener('detach', deviceDisconnectCallback);
}
});
}
set onconnect(fn) {
if (this._onconnect) {
this.removeEventListener('connect', this._onconnect);
this._onconnect = undefined;
}
if (fn) {
this._onconnect = fn;
this.addEventListener('connect', this._onconnect);
}
}
set ondisconnect(fn) {
if (this._ondisconnect) {
this.removeEventListener('disconnect', this._ondisconnect);
this._ondisconnect = undefined;
}
if (fn) {
this._ondisconnect = fn;
this.addEventListener('disconnect', this._ondisconnect);
}
}
addEventListener(type, listener) {
this.emitter.addListener(type, listener);
}
removeEventListener(type, callback) {
this.emitter.removeListener(type, callback);
}
dispatchEvent(_event) {
// Don't dispatch from here
return false;
}
/**
* Requests a single Web USB device
* @param options The options to use when scanning
* @returns Promise containing the selected device
*/
async requestDevice(options) {
// Must have options
if (!options) {
throw new TypeError('requestDevice error: 1 argument required, but only 0 present');
}
// Options must be an object
if (options.constructor !== {}.constructor) {
throw new TypeError('requestDevice error: parameter 1 (options) is not an object');
}
// Must have a filter
if (!options.filters) {
throw new TypeError('requestDevice error: required member filters is undefined');
}
// Filter must be an array
if (options.filters.constructor !== [].constructor) {
throw new TypeError('requestDevice error: the provided value cannot be converted to a sequence');
}
// Check filters
options.filters.forEach(filter => {
// Protocol & Subclass
if (filter.protocolCode && !filter.subclassCode) {
throw new TypeError('requestDevice error: subclass code is required');
}
// Subclass & Class
if (filter.subclassCode && !filter.classCode) {
throw new TypeError('requestDevice error: class code is required');
}
});
let devices = await this.loadDevices(options.filters);
devices = devices.filter(device => this.filterDevice(device, options.filters));
if (devices.length === 0) {
throw new NamedError('Failed to execute \'requestDevice\' on \'USB\': No device selected.', 'NotFoundError');
}
try {
// If no devicesFound function, select the first device found
const device = this.options.devicesFound ? await this.options.devicesFound(devices) : devices[0];
if (!device) {
throw new NamedError('Failed to execute \'requestDevice\' on \'USB\': No device selected.', 'NotFoundError');
}
this.authorisedDevices.add({
vendorId: device.vendorId,
productId: device.productId,
classCode: device.deviceClass,
subclassCode: device.deviceSubclass,
protocolCode: device.deviceProtocol,
serialNumber: device.serialNumber
});
return device;
}
catch (error) {
throw new NamedError('Failed to execute \'requestDevice\' on \'USB\': No device selected.', 'NotFoundError');
}
}
/**
* Gets all allowed Web USB devices which are connected
* @returns Promise containing an array of devices
*/
async getDevices() {
const preFilters = this.options.allowAllDevices ? undefined : this.options.allowedDevices;
// Refresh devices and filter for allowed ones
const devices = await this.loadDevices(preFilters);
return devices.filter(device => this.isAuthorisedDevice(device));
}
async loadDevices(preFilters) {
let devices = usb.getDeviceList();
// Pre-filter devices
devices = this.quickFilter(devices, preFilters);
const refreshedKnownDevices = new Map();
for (const device of devices) {
const webDevice = await this.getWebDevice(device);
if (webDevice) {
refreshedKnownDevices.set(device, webDevice);
}
}
// Refresh knownDevices to remove old devices from the map
this.knownDevices = refreshedKnownDevices;
return [...this.knownDevices.values()];
}
// Get a WebUSBDevice corresponding to underlying device.
// Returns undefined the device was not found and could not be created.
async getWebDevice(device) {
if (!this.knownDevices.has(device)) {
if (this.options.deviceTimeout) {
device.timeout = this.options.deviceTimeout;
}
try {
const webDevice = await webusb_device_1.WebUSBDevice.createInstance(device, this.options.autoDetachKernelDriver);
this.knownDevices.set(device, webDevice);
}
catch {
// Ignore creation issues as this may be a system device
}
}
return this.knownDevices.get(device);
}
// Undertake quick filter on devices before creating WebUSB devices if possible
quickFilter(devices, preFilters) {
if (!preFilters || !preFilters.length) {
return devices;
}
// Just pre-filter on vid/pid
return devices.filter(device => preFilters.some(filter => {
// Vendor
if (filter.vendorId && filter.vendorId !== device.deviceDescriptor.idVendor)
return false;
// Product
if (filter.productId && filter.productId !== device.deviceDescriptor.idProduct)
return false;
// Ignore Class, Subclass and Protocol as these need to check interfaces, too
// Ignore serial number for node-usb as it requires device connection
return true;
}));
}
// Filter WebUSB devices
filterDevice(device, filters) {
if (!filters || !filters.length) {
return true;
}
return filters.some(filter => {
// Vendor
if (filter.vendorId && filter.vendorId !== device.vendorId)
return false;
// Product
if (filter.productId && filter.productId !== device.productId)
return false;
// Class
if (filter.classCode) {
if (!device.configuration) {
return false;
}
// Interface Descriptors
const match = device.configuration.interfaces.some(iface => {
// Class
if (filter.classCode && filter.classCode !== iface.alternate.interfaceClass)
return false;
// Subclass
if (filter.subclassCode && filter.subclassCode !== iface.alternate.interfaceSubclass)
return false;
// Protocol
if (filter.protocolCode && filter.protocolCode !== iface.alternate.interfaceProtocol)
return false;
return true;
});
if (match) {
return true;
}
}
// Class
if (filter.classCode && filter.classCode !== device.deviceClass)
return false;
// Subclass
if (filter.subclassCode && filter.subclassCode !== device.deviceSubclass)
return false;
// Protocol
if (filter.protocolCode && filter.protocolCode !== device.deviceProtocol)
return false;
// Serial
if (filter.serialNumber && filter.serialNumber !== device.serialNumber)
return false;
return true;
});
}
// Check whether a device is authorised
isAuthorisedDevice(device) {
// All devices are authorised
if (this.options.allowAllDevices) {
return true;
}
// Check any allowed device filters
if (this.options.allowedDevices && this.filterDevice(device, this.options.allowedDevices)) {
return true;
}
// Check authorised devices
return [...this.authorisedDevices.values()].some(authorised => authorised.vendorId === device.vendorId
&& authorised.productId === device.productId
&& authorised.classCode === device.deviceClass
&& authorised.subclassCode === device.deviceSubclass
&& authorised.protocolCode === device.deviceProtocol
&& authorised.serialNumber === device.serialNumber);
}
}
exports.WebUSB = WebUSB;
//# sourceMappingURL=index.js.map