usb 1.9.2 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.eslintignore +4 -0
  2. package/.eslintrc.json +87 -0
  3. package/.gitattributes +1 -0
  4. package/.vscode/launch.json +15 -0
  5. package/.vscode/tasks.json +23 -0
  6. package/CHANGELOG.md +13 -0
  7. package/README.md +400 -175
  8. package/dist/index.d.ts +17 -0
  9. package/dist/index.js +138 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/usb/bindings.d.ts +253 -0
  12. package/dist/usb/bindings.js +10 -0
  13. package/dist/usb/bindings.js.map +1 -0
  14. package/dist/usb/capability.d.ts +14 -0
  15. package/dist/usb/capability.js +18 -0
  16. package/dist/usb/capability.js.map +1 -0
  17. package/dist/usb/descriptors.d.ts +129 -0
  18. package/dist/usb/descriptors.js +3 -0
  19. package/dist/usb/descriptors.js.map +1 -0
  20. package/dist/usb/device.d.ts +94 -0
  21. package/dist/usb/device.js +300 -0
  22. package/dist/usb/device.js.map +1 -0
  23. package/dist/usb/endpoint.d.ts +91 -0
  24. package/dist/usb/endpoint.js +236 -0
  25. package/dist/usb/endpoint.js.map +1 -0
  26. package/dist/usb/index.d.ts +25 -0
  27. package/dist/usb/index.js +116 -0
  28. package/dist/usb/index.js.map +1 -0
  29. package/dist/usb/interface.d.ts +78 -0
  30. package/dist/usb/interface.js +137 -0
  31. package/dist/usb/interface.js.map +1 -0
  32. package/dist/webusb/index.d.ts +56 -0
  33. package/dist/webusb/index.js +412 -0
  34. package/dist/webusb/index.js.map +1 -0
  35. package/dist/webusb/mutex.d.ts +22 -0
  36. package/dist/webusb/mutex.js +89 -0
  37. package/dist/webusb/mutex.js.map +1 -0
  38. package/dist/webusb/webusb-device.d.ts +49 -0
  39. package/dist/webusb/webusb-device.js +892 -0
  40. package/dist/webusb/webusb-device.js.map +1 -0
  41. package/package.json +28 -15
  42. package/prebuilds/win32-ia32/node.napi.node +0 -0
  43. package/prebuilds/win32-x64/node.napi.node +0 -0
  44. package/src/device.cc +2 -2
  45. package/test/usb.coffee +13 -7
  46. package/test/webusb.coffee +189 -0
  47. package/tsc/index.ts +72 -0
  48. package/tsc/usb/bindings.ts +304 -0
  49. package/tsc/usb/capability.ts +22 -0
  50. package/tsc/usb/descriptors.ts +180 -0
  51. package/tsc/usb/device.ts +325 -0
  52. package/tsc/usb/endpoint.ts +228 -0
  53. package/tsc/usb/index.ts +111 -0
  54. package/tsc/usb/interface.ts +172 -0
  55. package/tsc/webusb/index.ts +363 -0
  56. package/tsc/webusb/mutex.ts +38 -0
  57. package/tsc/webusb/webusb-device.ts +541 -0
  58. package/tsconfig.json +17 -0
  59. package/typedoc.json +9 -0
  60. package/usb.js +0 -573
@@ -0,0 +1,172 @@
1
+ import { LibUSBException, LIBUSB_ENDPOINT_IN, Device } from './bindings';
2
+ import { InterfaceDescriptor } from './descriptors';
3
+ import { Endpoint, InEndpoint, OutEndpoint } from './endpoint';
4
+
5
+ export class Interface {
6
+ /** Integer interface number. */
7
+ public interfaceNumber!: number;
8
+
9
+ /** Integer alternate setting number. */
10
+ public altSetting = 0;
11
+
12
+ /** Object with fields from the interface descriptor -- see libusb documentation or USB spec. */
13
+ public descriptor!: InterfaceDescriptor;
14
+
15
+ /** List of endpoints on this interface: InEndpoint and OutEndpoint objects. */
16
+ public endpoints!: Endpoint[];
17
+
18
+ constructor(protected device: Device, protected id: number) {
19
+ this.refresh();
20
+ }
21
+
22
+ protected refresh(): void {
23
+ if (!this.device.configDescriptor) {
24
+ return;
25
+ }
26
+
27
+ this.descriptor = this.device.configDescriptor.interfaces[this.id][this.altSetting];
28
+ this.interfaceNumber = this.descriptor.bInterfaceNumber;
29
+ this.endpoints = [];
30
+ const len = this.descriptor.endpoints.length;
31
+ for (let i = 0; i < len; i++) {
32
+ const desc = this.descriptor.endpoints[i];
33
+ const c = (desc.bEndpointAddress & LIBUSB_ENDPOINT_IN) ? InEndpoint : OutEndpoint;
34
+ this.endpoints[i] = new c(this.device, desc);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Claims the interface. This method must be called before using any endpoints of this interface.
40
+ *
41
+ * The device must be open to use this method.
42
+ */
43
+ public claim(): void {
44
+ this.device.__claimInterface(this.id);
45
+ }
46
+
47
+ /**
48
+ * Releases the interface and resets the alternate setting. Calls callback when complete.
49
+ *
50
+ * It is an error to release an interface with pending transfers.
51
+ *
52
+ * The device must be open to use this method.
53
+ * @param callback
54
+ */
55
+ public release(callback?: (error?: LibUSBException) => void): void;
56
+
57
+ /**
58
+ * Releases the interface and resets the alternate setting. Calls callback when complete.
59
+ *
60
+ * It is an error to release an interface with pending transfers. If the optional closeEndpoints
61
+ * parameter is true, any active endpoint streams are stopped (see `Endpoint.stopStream`),
62
+ * and the interface is released after the stream transfers are cancelled. Transfers submitted
63
+ * individually with `Endpoint.transfer` are not affected by this parameter.
64
+ *
65
+ * The device must be open to use this method.
66
+ * @param closeEndpoints
67
+ * @param callback
68
+ */
69
+ public release(closeEndpoints?: boolean, callback?: (error?: LibUSBException) => void): void;
70
+ public release(closeEndpointsOrCallback?: boolean | ((error?: LibUSBException) => void), callback?: (error: LibUSBException | undefined) => void): void {
71
+
72
+ let closeEndpoints = false;
73
+ if (typeof closeEndpointsOrCallback === 'boolean') {
74
+ closeEndpoints = closeEndpointsOrCallback;
75
+ } else {
76
+ callback = closeEndpointsOrCallback;
77
+ }
78
+
79
+ const next = () => {
80
+ this.device.__releaseInterface(this.id, error => {
81
+ if (!error) {
82
+ this.altSetting = 0;
83
+ this.refresh();
84
+ }
85
+ if (callback) {
86
+ callback.call(this, error);
87
+ }
88
+ });
89
+ };
90
+
91
+ if (!closeEndpoints || this.endpoints.length === 0) {
92
+ next();
93
+ } else {
94
+ let n = this.endpoints.length;
95
+ this.endpoints.forEach(ep => {
96
+ if (ep.direction === 'in' && (ep as InEndpoint).pollActive) {
97
+ ep.once('end', () => {
98
+ if (--n === 0) {
99
+ next();
100
+ }
101
+ });
102
+ (ep as InEndpoint).stopPoll();
103
+ } else {
104
+ if (--n === 0) {
105
+ next();
106
+ }
107
+ }
108
+ });
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Returns `false` if a kernel driver is not active; `true` if active.
114
+ *
115
+ * The device must be open to use this method.
116
+ */
117
+ public isKernelDriverActive(): boolean {
118
+ return this.device.__isKernelDriverActive(this.id);
119
+ }
120
+
121
+ /**
122
+ * Detaches the kernel driver from the interface.
123
+ *
124
+ * The device must be open to use this method.
125
+ */
126
+ public detachKernelDriver(): void {
127
+ return this.device.__detachKernelDriver(this.id);
128
+ }
129
+
130
+ /**
131
+ * Re-attaches the kernel driver for the interface.
132
+ *
133
+ * The device must be open to use this method.
134
+ */
135
+ public attachKernelDriver(): void {
136
+ return this.device.__attachKernelDriver(this.id);
137
+ }
138
+
139
+ /**
140
+ * Sets the alternate setting. It updates the `interface.endpoints` array to reflect the endpoints found in the alternate setting.
141
+ *
142
+ * The device must be open to use this method.
143
+ * @param altSetting
144
+ * @param callback
145
+ */
146
+ public setAltSetting(altSetting: number, callback?: (error: LibUSBException | undefined) => void): void {
147
+ this.device.__setInterface(this.id, altSetting, error => {
148
+ if (!error) {
149
+ this.altSetting = altSetting;
150
+ this.refresh();
151
+ }
152
+ if (callback) {
153
+ callback.call(this, error);
154
+ }
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Return the InEndpoint or OutEndpoint with the specified address.
160
+ *
161
+ * The device must be open to use this method.
162
+ * @param addr
163
+ */
164
+ public endpoint(addr: number): Endpoint {
165
+ const endpoint = this.endpoints.find(item => item.address === addr);
166
+ if (!endpoint) {
167
+ throw new Error(`Endpoint not found for address: ${addr}`);
168
+ }
169
+
170
+ return endpoint;
171
+ }
172
+ }
@@ -0,0 +1,363 @@
1
+ import * as usb from '../usb';
2
+ import { EventEmitter } from 'events';
3
+ import { WebUSBDevice } from './webusb-device';
4
+
5
+ /**
6
+ * USB Options
7
+ */
8
+ export interface USBOptions {
9
+ /**
10
+ * Optional `device found` callback function to allow the user to select a device
11
+ */
12
+ devicesFound?: (devices: USBDevice[]) => Promise<USBDevice | void>;
13
+
14
+ /**
15
+ * Optional array of preconfigured allowed devices
16
+ */
17
+ allowedDevices?: USBDeviceFilter[];
18
+
19
+ /**
20
+ * Optional flag to automatically allow all devices
21
+ */
22
+ allowAllDevices?: boolean;
23
+
24
+ /**
25
+ * Optional timeout (in milliseconds) to use for the device control transfers
26
+ */
27
+ deviceTimeout?: number;
28
+ }
29
+
30
+ export class WebUSB implements USB {
31
+
32
+ protected emitter = new EventEmitter();
33
+ protected knownDevices: Map<string, USBDevice> = new Map();
34
+ protected allowedDevices: USBDeviceFilter[];
35
+
36
+ constructor(private options: USBOptions = {}) {
37
+ this.allowedDevices = options.allowedDevices || [];
38
+
39
+ const deviceConnectCallback = async (device: usb.Device) => {
40
+ const webDevice = await WebUSBDevice.createInstance(device);
41
+
42
+ // When connected, emit an event if it is an allowed device
43
+ if (webDevice && this.isAllowedDevice(webDevice)) {
44
+ const deviceId = this.getDeviceId(device);
45
+ if (deviceId) {
46
+ this.knownDevices.set(deviceId, webDevice);
47
+ }
48
+
49
+ const event = {
50
+ type: 'connect',
51
+ device: webDevice
52
+ };
53
+
54
+ this.emitter.emit('connect', event);
55
+ }
56
+ };
57
+
58
+ const deviceDisconnectCallback = async (device: usb.Device) => {
59
+ const deviceId = this.getDeviceId(device);
60
+
61
+ // When disconnected, emit an event if the device was a known allowed device
62
+ if (deviceId !== undefined && this.knownDevices.has(deviceId)) {
63
+ const webDevice = this.knownDevices.get(deviceId);
64
+
65
+ if (webDevice && this.isAllowedDevice(webDevice)) {
66
+ const event = {
67
+ type: 'disconnect',
68
+ device: webDevice
69
+ };
70
+
71
+ this.emitter.emit('disconnect', event);
72
+ }
73
+ }
74
+ };
75
+
76
+ this.emitter.on('newListener', event => {
77
+ const listenerCount = this.emitter.listenerCount(event);
78
+
79
+ if (listenerCount !== 0) {
80
+ return;
81
+ }
82
+
83
+ if (event === 'connect') {
84
+ usb.addListener('attach', deviceConnectCallback);
85
+ } else if (event === 'disconnect') {
86
+ usb.addListener('detach', deviceDisconnectCallback);
87
+ }
88
+ });
89
+
90
+ this.emitter.on('removeListener', event => {
91
+ const listenerCount = this.emitter.listenerCount(event);
92
+
93
+ if (listenerCount !== 0) {
94
+ return;
95
+ }
96
+
97
+ if (event === 'connect') {
98
+ usb.removeListener('attach', deviceConnectCallback);
99
+ } else if (event === 'disconnect') {
100
+ usb.removeListener('detach', deviceDisconnectCallback);
101
+ }
102
+ });
103
+ }
104
+
105
+ private _onconnect: ((ev: USBConnectionEvent) => void) | undefined;
106
+ public set onconnect(fn: (ev: USBConnectionEvent) => void) {
107
+ if (this._onconnect) {
108
+ this.removeEventListener('connect', this._onconnect);
109
+ this._onconnect = undefined;
110
+ }
111
+
112
+ if (fn) {
113
+ this._onconnect = fn;
114
+ this.addEventListener('connect', this._onconnect);
115
+ }
116
+ }
117
+
118
+ private _ondisconnect: ((ev: USBConnectionEvent) => void) | undefined;
119
+ public set ondisconnect(fn: (ev: USBConnectionEvent) => void) {
120
+ if (this._ondisconnect) {
121
+ this.removeEventListener('disconnect', this._ondisconnect);
122
+ this._ondisconnect = undefined;
123
+ }
124
+
125
+ if (fn) {
126
+ this._ondisconnect = fn;
127
+ this.addEventListener('disconnect', this._ondisconnect);
128
+ }
129
+ }
130
+
131
+ public addEventListener(type: 'connect' | 'disconnect', listener: (this: this, ev: USBConnectionEvent) => void): void;
132
+ public addEventListener(type: 'connect' | 'disconnect', listener: EventListener): void;
133
+ public addEventListener(type: string, listener: (ev: USBConnectionEvent) => void): void {
134
+ this.emitter.addListener(type, listener);
135
+ }
136
+
137
+ public removeEventListener(type: 'connect' | 'disconnect', callback: (this: this, ev: USBConnectionEvent) => void): void;
138
+ public removeEventListener(type: 'connect' | 'disconnect', callback: EventListener): void;
139
+ public removeEventListener(type: string, callback: (this: this, ev: USBConnectionEvent) => void): void {
140
+ this.emitter.removeListener(type, callback);
141
+ }
142
+
143
+ public dispatchEvent(_event: Event): boolean {
144
+ // Don't dispatch from here
145
+ return false;
146
+ }
147
+
148
+ /**
149
+ * Requests a single Web USB device
150
+ * @param options The options to use when scanning
151
+ * @returns Promise containing the selected device
152
+ */
153
+ public async requestDevice(options?: USBDeviceRequestOptions): Promise<USBDevice> {
154
+ // Must have options
155
+ if (!options) {
156
+ throw new TypeError('requestDevice error: 1 argument required, but only 0 present');
157
+ }
158
+
159
+ // Options must be an object
160
+ if (options.constructor !== {}.constructor) {
161
+ throw new TypeError('requestDevice error: parameter 1 (options) is not an object');
162
+ }
163
+
164
+ // Must have a filter
165
+ if (!options.filters) {
166
+ throw new TypeError('requestDevice error: required member filters is undefined');
167
+ }
168
+
169
+ // Filter must be an array
170
+ if (options.filters.constructor !== [].constructor) {
171
+ throw new TypeError('requestDevice error: the provided value cannot be converted to a sequence');
172
+ }
173
+
174
+ // Check filters
175
+ options.filters.forEach(filter => {
176
+ // Protocol & Subclass
177
+ if (filter.protocolCode && !filter.subclassCode) {
178
+ throw new TypeError('requestDevice error: subclass code is required');
179
+ }
180
+
181
+ // Subclass & Class
182
+ if (filter.subclassCode && !filter.classCode) {
183
+ throw new TypeError('requestDevice error: class code is required');
184
+ }
185
+ });
186
+
187
+ let devices = await this.loadDevices(options.filters);
188
+ devices = devices.filter(device => this.filterDevice(options, device));
189
+
190
+ if (devices.length === 0) {
191
+ throw new Error('requestDevice error: no devices found');
192
+ }
193
+
194
+ try {
195
+ // If no devicesFound function, select the first device found
196
+ const device = this.options.devicesFound ? await this.options.devicesFound(devices) : devices[0];
197
+
198
+ if (!device) {
199
+ throw new Error('selected device not found');
200
+ }
201
+
202
+ if (!this.isAllowedDevice(device)) {
203
+ this.allowedDevices.push({
204
+ vendorId: device.vendorId,
205
+ productId: device.productId,
206
+ serialNumber: device.serialNumber
207
+ });
208
+ }
209
+
210
+ return device;
211
+ } catch (error) {
212
+ throw new Error(`requestDevice error: ${error}`);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Gets all allowed Web USB devices which are connected
218
+ * @returns Promise containing an array of devices
219
+ */
220
+ public async getDevices(): Promise<USBDevice[]> {
221
+ let preFilters: USBDeviceFilter[] | undefined;
222
+
223
+ if (!this.options.allowAllDevices) {
224
+ // Create pre-filters
225
+ preFilters = this.allowedDevices.map(device => ({
226
+ vendorId: device.vendorId || undefined,
227
+ productId: device.productId || undefined,
228
+ serialNumber: device.serialNumber || undefined
229
+ }));
230
+ }
231
+
232
+ // Refresh devices and filter for allowed ones
233
+ const devices = await this.loadDevices(preFilters);
234
+ return devices.filter(device => this.isAllowedDevice(device));
235
+ }
236
+
237
+ private async loadDevices(preFilters?: USBDeviceFilter[]): Promise<USBDevice[]> {
238
+ let devices = usb.getDeviceList();
239
+
240
+ // Pre-filter devices
241
+ devices = this.preFilterDevices(devices, preFilters);
242
+
243
+ const webDevices: USBDevice[] = [];
244
+
245
+ for (const device of devices) {
246
+ if (this.options.deviceTimeout) {
247
+ device.timeout = this.options.deviceTimeout;
248
+ }
249
+
250
+ const webDevice = await WebUSBDevice.createInstance(device);
251
+ if (webDevice) {
252
+ webDevices.push(webDevice);
253
+
254
+ const deviceId = this.getDeviceId(device);
255
+ if (deviceId) {
256
+ this.knownDevices.set(deviceId, webDevice);
257
+ }
258
+ }
259
+ }
260
+
261
+ return webDevices;
262
+ }
263
+
264
+ private preFilterDevices(devices: usb.Device[], preFilters?: USBDeviceFilter[]): usb.Device[] {
265
+ if (!preFilters || !preFilters.length) {
266
+ return devices;
267
+ }
268
+
269
+ // Just pre-filter on vid/pid
270
+ return devices.filter(device => preFilters.some(filter => {
271
+ // Vendor
272
+ if (filter.vendorId && filter.vendorId !== device.deviceDescriptor.idVendor) return false;
273
+
274
+ // Product
275
+ if (filter.productId && filter.productId !== device.deviceDescriptor.idProduct) return false;
276
+
277
+ // Ignore serial number for node-usb as it requires device connection
278
+ return true;
279
+ }));
280
+ }
281
+
282
+ private filterDevice(options: USBDeviceRequestOptions, device: USBDevice): boolean {
283
+ if (!options.filters || !options.filters.length) {
284
+ return true;
285
+ }
286
+
287
+ return options.filters.some(filter => {
288
+ // Vendor
289
+ if (filter.vendorId && filter.vendorId !== device.vendorId) return false;
290
+
291
+ // Product
292
+ if (filter.productId && filter.productId !== device.productId) return false;
293
+
294
+ // Class
295
+ if (filter.classCode) {
296
+
297
+ if (!device.configuration) {
298
+ return false;
299
+ }
300
+
301
+ // Interface Descriptors
302
+ const match = device.configuration.interfaces.some(iface => {
303
+ // Class
304
+ if (filter.classCode && filter.classCode !== iface.alternate.interfaceClass) return false;
305
+
306
+ // Subclass
307
+ if (filter.subclassCode && filter.subclassCode !== iface.alternate.interfaceSubclass) return false;
308
+
309
+ // Protocol
310
+ if (filter.protocolCode && filter.protocolCode !== iface.alternate.interfaceProtocol) return false;
311
+
312
+ return true;
313
+ });
314
+
315
+ if (match) {
316
+ return true;
317
+ }
318
+ }
319
+
320
+ // Class
321
+ if (filter.classCode && filter.classCode !== device.deviceClass) return false;
322
+
323
+ // Subclass
324
+ if (filter.subclassCode && filter.subclassCode !== device.deviceSubclass) return false;
325
+
326
+ // Protocol
327
+ if (filter.protocolCode && filter.protocolCode !== device.deviceProtocol) return false;
328
+
329
+ // Serial
330
+ if (filter.serialNumber && filter.serialNumber !== device.serialNumber) return false;
331
+
332
+ return true;
333
+ });
334
+ }
335
+
336
+ private getDeviceId(device: usb.Device): string | undefined {
337
+ if (device.busNumber === undefined || device.deviceAddress === undefined) {
338
+ return undefined;
339
+ }
340
+
341
+ return `${device.busNumber}.${device.deviceAddress}`;
342
+ }
343
+
344
+ private isAllowedDevice(device: USBDeviceFilter): boolean {
345
+ if (this.options.allowAllDevices) {
346
+ return true;
347
+ }
348
+
349
+ const isSameDevice = (device1: USBDeviceFilter, device2: USBDeviceFilter): boolean => {
350
+ return (device1.productId === device2.productId
351
+ && device1.vendorId === device2.vendorId
352
+ && device1.serialNumber === device2.serialNumber);
353
+ };
354
+
355
+ for (const i in this.allowedDevices) {
356
+ if (isSameDevice(device, this.allowedDevices[i])) {
357
+ return true;
358
+ }
359
+ }
360
+
361
+ return false;
362
+ }
363
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A mutex implementation that can be used to lock access between concurrent async functions
3
+ */
4
+ export class Mutex {
5
+ private locked: boolean;
6
+
7
+ /**
8
+ * Create a new Mutex
9
+ */
10
+ constructor() {
11
+ this.locked = false;
12
+ }
13
+
14
+ /**
15
+ * Yield the current execution context, effectively moving it to the back of the promise queue
16
+ */
17
+ private async sleep(): Promise<void> {
18
+ return new Promise(resolve => setTimeout(resolve, 1));
19
+ }
20
+
21
+ /**
22
+ * Wait until the Mutex is available and claim it
23
+ */
24
+ public async lock(): Promise<void> {
25
+ while (this.locked) {
26
+ await this.sleep();
27
+ }
28
+
29
+ this.locked = true;
30
+ }
31
+
32
+ /**
33
+ * Unlock the Mutex
34
+ */
35
+ public unlock(): void {
36
+ this.locked = false;
37
+ }
38
+ }