@myshkouski/web-serial-polyfill 2.0.4 → 2.2.0

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.
package/serial.ts CHANGED
@@ -1,641 +1,656 @@
1
- /*
2
- * Copyright 2019 Google LLC
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the
5
- * "License"); you may not use this file except in
6
- * compliance with the License. You may obtain a copy of
7
- * the License at
8
- *
9
- * https://www.apache.org/licenses/LICENSE-2.0
10
- *
11
- * Unless required by applicable law or agreed to in
12
- * writing, software distributed under the License is
13
- * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
14
- * OR CONDITIONS OF ANY KIND, either express or implied.
15
- * See the License for the specific language governing
16
- * permissions and limitations under the License.
17
- */
18
- 'use strict';
19
-
20
- export enum SerialPolyfillProtocol {
21
- UsbCdcAcm, // eslint-disable-line no-unused-vars
22
- }
23
-
24
- export interface SerialPolyfillOptions {
25
- protocol?: SerialPolyfillProtocol;
26
- usbControlInterfaceClass?: number;
27
- usbTransferInterfaceClass?: number;
28
- }
29
-
30
- const kSetLineCoding = 0x20;
31
- const kSetControlLineState = 0x22;
32
- const kSendBreak = 0x23;
33
-
34
- const kDefaultBufferSize = 255;
35
- const kDefaultDataBits = 8;
36
- const kDefaultParity = 'none';
37
- const kDefaultStopBits = 1;
38
-
39
- const kAcceptableDataBits = [16, 8, 7, 6, 5];
40
- const kAcceptableStopBits = [1, 2];
41
- const kAcceptableParity = ['none', 'even', 'odd'];
42
-
43
- const kParityIndexMapping: ParityType[] =
44
- ['none', 'odd', 'even'];
45
- const kStopBitsIndexMapping = [1, 1.5, 2];
46
-
47
- const kDefaultPolyfillOptions = {
48
- protocol: SerialPolyfillProtocol.UsbCdcAcm,
49
- usbControlInterfaceClass: 2,
50
- usbTransferInterfaceClass: 10,
51
- };
52
-
53
- /**
54
- * Utility function to get the interface implementing a desired class.
55
- * @param {USBDevice} device The USB device.
56
- * @param {number} classCode The desired interface class.
57
- * @return {USBInterface} The first interface found that implements the desired
58
- * class.
59
- * @throws TypeError if no interface is found.
60
- */
61
- function findInterface(device: USBDevice, classCode: number): USBInterface {
62
- const configuration = device.configurations[0];
63
- for (const iface of configuration.interfaces) {
64
- const alternate = iface.alternates[0];
65
- if (alternate.interfaceClass === classCode) {
66
- return iface;
67
- }
68
- }
69
- throw new TypeError(`Unable to find interface with class ${classCode}.`);
70
- }
71
-
72
- /**
73
- * Utility function to get an endpoint with a particular direction.
74
- * @param {USBInterface} iface The interface to search.
75
- * @param {USBDirection} direction The desired transfer direction.
76
- * @return {USBEndpoint} The first endpoint with the desired transfer direction.
77
- * @throws TypeError if no endpoint is found.
78
- */
79
- function findEndpoint(iface: USBInterface, direction: USBDirection):
80
- USBEndpoint {
81
- const alternate = iface.alternates[0];
82
- for (const endpoint of alternate.endpoints) {
83
- if (endpoint.direction == direction) {
84
- return endpoint;
85
- }
86
- }
87
- throw new TypeError(`Interface ${iface.interfaceNumber} does not have an ` +
88
- `${direction} endpoint.`);
89
- }
90
-
91
- /**
92
- * Implementation of the underlying source API[1] which reads data from a USB
93
- * endpoint. This can be used to construct a ReadableStream.
94
- *
95
- * [1]: https://streams.spec.whatwg.org/#underlying-source-api
96
- */
97
- class UsbEndpointUnderlyingSource implements UnderlyingByteSource {
98
- private device_: USBDevice;
99
- private endpoint_: USBEndpoint;
100
- private onError_: () => void;
101
-
102
- type: 'bytes';
103
-
104
- /**
105
- * Constructs a new UnderlyingSource that will pull data from the specified
106
- * endpoint on the given USB device.
107
- *
108
- * @param {USBDevice} device
109
- * @param {USBEndpoint} endpoint
110
- * @param {function} onError function to be called on error
111
- */
112
- constructor(device: USBDevice, endpoint: USBEndpoint, onError: () => void) {
113
- this.type = 'bytes';
114
- this.device_ = device;
115
- this.endpoint_ = endpoint;
116
- this.onError_ = onError;
117
- }
118
-
119
- /**
120
- * Reads a chunk of data from the device.
121
- *
122
- * @param {ReadableByteStreamController} controller
123
- */
124
- async pull(controller: ReadableByteStreamController): Promise<void> {
125
- let chunkSize;
126
- if (controller.desiredSize) {
127
- const d = controller.desiredSize / this.endpoint_.packetSize;
128
- chunkSize = Math.ceil(d) * this.endpoint_.packetSize;
129
- } else {
130
- chunkSize = this.endpoint_.packetSize;
131
- }
132
-
133
- try {
134
- const result = await this.device_.transferIn(
135
- this.endpoint_.endpointNumber, chunkSize);
136
- if (result.status != 'ok') {
137
- controller.error(`USB error: ${result.status}`);
138
- this.onError_();
139
- }
140
- if (result.data?.buffer) {
141
- const chunk = new Uint8Array(
142
- toArrayBuffer(result.data.buffer), result.data.byteOffset,
143
- result.data.byteLength);
144
- controller.enqueue(chunk);
145
- }
146
- } catch (error) {
147
- controller.error(error.toString());
148
- this.onError_();
149
- }
150
- }
151
- }
152
-
153
- /**
154
- * Implementation of the underlying sink API[2] which writes data to a USB
155
- * endpoint. This can be used to construct a WritableStream.
156
- *
157
- * [2]: https://streams.spec.whatwg.org/#underlying-sink-api
158
- */
159
- class UsbEndpointUnderlyingSink implements UnderlyingSink<Uint8Array> {
160
- private device_: USBDevice;
161
- private endpoint_: USBEndpoint;
162
- private onError_: () => void;
163
-
164
- /**
165
- * Constructs a new UnderlyingSink that will write data to the specified
166
- * endpoint on the given USB device.
167
- *
168
- * @param {USBDevice} device
169
- * @param {USBEndpoint} endpoint
170
- * @param {function} onError function to be called on error
171
- */
172
- constructor(device: USBDevice, endpoint: USBEndpoint, onError: () => void) {
173
- this.device_ = device;
174
- this.endpoint_ = endpoint;
175
- this.onError_ = onError;
176
- }
177
-
178
- /**
179
- * Writes a chunk to the device.
180
- *
181
- * @param {Uint8Array} chunk
182
- * @param {WritableStreamDefaultController} controller
183
- */
184
- async write(
185
- chunk: Uint8Array<ArrayBuffer>,
186
- controller: WritableStreamDefaultController): Promise<void> {
187
- try {
188
- const result =
189
- await this.device_.transferOut(this.endpoint_.endpointNumber, chunk);
190
- if (result.status != 'ok') {
191
- controller.error(result.status);
192
- this.onError_();
193
- }
194
- } catch (error) {
195
- controller.error(error.toString());
196
- this.onError_();
197
- }
198
- }
199
- }
200
-
201
- /** a class used to control serial devices over WebUSB */
202
- export class SerialPort {
203
- private polyfillOptions_: SerialPolyfillOptions;
204
- private device_: USBDevice;
205
- private controlInterface_: USBInterface;
206
- private transferInterface_: USBInterface;
207
- private inEndpoint_: USBEndpoint;
208
- private outEndpoint_: USBEndpoint;
209
-
210
- private serialOptions_: SerialOptions;
211
- private readable_: ReadableStream<Uint8Array> | null;
212
- private writable_: WritableStream<Uint8Array> | null;
213
- private outputSignals_: SerialOutputSignals;
214
-
215
- /**
216
- * constructor taking a WebUSB device that creates a SerialPort instance.
217
- * @param {USBDevice} device A device acquired from the WebUSB API
218
- * @param {SerialPolyfillOptions} polyfillOptions Optional options to
219
- * configure the polyfill.
220
- */
221
- public constructor(
222
- device: USBDevice,
223
- polyfillOptions?: SerialPolyfillOptions) {
224
- this.polyfillOptions_ = {...kDefaultPolyfillOptions, ...polyfillOptions};
225
- this.outputSignals_ = {
226
- dataTerminalReady: false,
227
- requestToSend: false,
228
- break: false,
229
- };
230
-
231
- this.device_ = device;
232
- this.controlInterface_ = findInterface(
233
- this.device_,
234
- this.polyfillOptions_.usbControlInterfaceClass as number);
235
- this.transferInterface_ = findInterface(
236
- this.device_,
237
- this.polyfillOptions_.usbTransferInterfaceClass as number);
238
- this.inEndpoint_ = findEndpoint(this.transferInterface_, 'in');
239
- this.outEndpoint_ = findEndpoint(this.transferInterface_, 'out');
240
- }
241
-
242
- /**
243
- * Getter for the readable attribute. Constructs a new ReadableStream as
244
- * necessary.
245
- * @return {ReadableStream} the current readable stream
246
- */
247
- public get readable(): ReadableStream<Uint8Array> | null {
248
- if (!this.readable_ && this.device_.opened) {
249
- this.readable_ = new ReadableStream<Uint8Array>(
250
- new UsbEndpointUnderlyingSource(
251
- this.device_, this.inEndpoint_, () => {
252
- this.readable_ = null;
253
- }),
254
- {
255
- highWaterMark: this.serialOptions_.bufferSize ?? kDefaultBufferSize,
256
- });
257
- }
258
- return this.readable_;
259
- }
260
-
261
- /**
262
- * Getter for the writable attribute. Constructs a new WritableStream as
263
- * necessary.
264
- * @return {WritableStream} the current writable stream
265
- */
266
- public get writable(): WritableStream<Uint8Array> | null {
267
- if (!this.writable_ && this.device_.opened) {
268
- this.writable_ = new WritableStream(
269
- new UsbEndpointUnderlyingSink(
270
- this.device_, this.outEndpoint_, () => {
271
- this.writable_ = null;
272
- }),
273
- new ByteLengthQueuingStrategy({
274
- highWaterMark: this.serialOptions_.bufferSize ?? kDefaultBufferSize,
275
- }));
276
- }
277
- return this.writable_;
278
- }
279
-
280
- /**
281
- * Release transfer and control interfaces.
282
- * Used before closing device.
283
- */
284
- private async releaseInterfaces_(): Promise<void> {
285
- await this.device_.releaseInterface(
286
- this.transferInterface_.interfaceNumber
287
- );
288
- await this.device_.releaseInterface(
289
- this.controlInterface_.interfaceNumber
290
- );
291
- }
292
-
293
- /**
294
- * a function that opens the device and claims all interfaces needed to
295
- * control and communicate to and from the serial device
296
- * @param {SerialOptions} options Object containing serial options
297
- * @return {Promise<void>} A promise that will resolve when device is ready
298
- * for communication
299
- */
300
- public async open(options: SerialOptions): Promise<void> {
301
- this.serialOptions_ = options;
302
- this.validateOptions();
303
-
304
- try {
305
- await this.device_.open();
306
- if (this.device_.configuration === null) {
307
- await this.device_.selectConfiguration(1);
308
- }
309
-
310
- await this.device_.claimInterface(this.controlInterface_.interfaceNumber);
311
- if (this.controlInterface_ !== this.transferInterface_) {
312
- await this.device_.claimInterface(
313
- this.transferInterface_.interfaceNumber);
314
- }
315
-
316
- await this.setLineCoding();
317
- await this.setSignals({dataTerminalReady: true});
318
- } catch (error) {
319
- if (this.device_.opened) {
320
- await this.releaseInterfaces_();
321
- await this.device_.close();
322
- }
323
- throw new Error('Error setting up device: ' + error.toString());
324
- }
325
- }
326
-
327
- /**
328
- * Closes the port.
329
- *
330
- * @return {Promise<void>} A promise that will resolve when the port is
331
- * closed.
332
- */
333
- public async close(): Promise<void> {
334
- const promises = [];
335
- if (this.readable_) {
336
- promises.push(this.readable_.cancel());
337
- }
338
- if (this.writable_) {
339
- promises.push(this.writable_.abort());
340
- }
341
- await Promise.all(promises);
342
- this.readable_ = null;
343
- this.writable_ = null;
344
- if (this.device_.opened) {
345
- await this.setSignals({dataTerminalReady: false, requestToSend: false});
346
- await this.releaseInterfaces_();
347
- await this.device_.close();
348
- }
349
- }
350
-
351
- /**
352
- * Forgets the port.
353
- *
354
- * @return {Promise<void>} A promise that will resolve when the port is
355
- * forgotten.
356
- */
357
- public async forget(): Promise<void> {
358
- return this.device_.forget();
359
- }
360
-
361
- /**
362
- * A function that returns properties of the device.
363
- * @return {SerialPortInfo} Device properties.
364
- */
365
- public getInfo(): SerialPortInfo {
366
- return {
367
- usbVendorId: this.device_.vendorId,
368
- usbProductId: this.device_.productId,
369
- };
370
- }
371
-
372
- /**
373
- * A function used to change the serial settings of the device
374
- * @param {object} options the object which carries serial settings data
375
- * @return {Promise<void>} A promise that will resolve when the options are
376
- * set
377
- */
378
- public reconfigure(options: SerialOptions): Promise<void> {
379
- this.serialOptions_ = {...this.serialOptions_, ...options};
380
- this.validateOptions();
381
- return this.setLineCoding();
382
- }
383
-
384
- /**
385
- * Sets control signal state for the port.
386
- * @param {SerialOutputSignals} signals The signals to enable or disable.
387
- * @return {Promise<void>} a promise that is resolved when the signal state
388
- * has been changed.
389
- */
390
- public async setSignals(signals: SerialOutputSignals): Promise<void> {
391
- this.outputSignals_ = {...this.outputSignals_, ...signals};
392
-
393
- if (signals.dataTerminalReady !== undefined ||
394
- signals.requestToSend !== undefined) {
395
- // The Set_Control_Line_State command expects a bitmap containing the
396
- // values of all output signals that should be enabled or disabled.
397
- //
398
- // Ref: USB CDC specification version 1.1 §6.2.14.
399
- const value = (this.outputSignals_.dataTerminalReady ? 1 << 0 : 0) |
400
- (this.outputSignals_.requestToSend ? 1 << 1 : 0);
401
-
402
- await this.device_.controlTransferOut({
403
- 'requestType': 'class',
404
- 'recipient': 'interface',
405
- 'request': kSetControlLineState,
406
- 'value': value,
407
- 'index': this.controlInterface_.interfaceNumber,
408
- });
409
- }
410
-
411
- if (signals.break !== undefined) {
412
- // The SendBreak command expects to be given a duration for how long the
413
- // break signal should be asserted. Passing 0xFFFF enables the signal
414
- // until 0x0000 is send.
415
- //
416
- // Ref: USB CDC specification version 1.1 §6.2.15.
417
- const value = this.outputSignals_.break ? 0xFFFF : 0x0000;
418
-
419
- await this.device_.controlTransferOut({
420
- 'requestType': 'class',
421
- 'recipient': 'interface',
422
- 'request': kSendBreak,
423
- 'value': value,
424
- 'index': this.controlInterface_.interfaceNumber,
425
- });
426
- }
427
- }
428
-
429
- /**
430
- * Checks the serial options for validity and throws an error if it is
431
- * not valid
432
- */
433
- private validateOptions(): void {
434
- if (!this.isValidBaudRate(this.serialOptions_.baudRate)) {
435
- throw new RangeError('invalid Baud Rate ' + this.serialOptions_.baudRate);
436
- }
437
-
438
- if (!this.isValidDataBits(this.serialOptions_.dataBits)) {
439
- throw new RangeError('invalid dataBits ' + this.serialOptions_.dataBits);
440
- }
441
-
442
- if (!this.isValidStopBits(this.serialOptions_.stopBits)) {
443
- throw new RangeError('invalid stopBits ' + this.serialOptions_.stopBits);
444
- }
445
-
446
- if (!this.isValidParity(this.serialOptions_.parity)) {
447
- throw new RangeError('invalid parity ' + this.serialOptions_.parity);
448
- }
449
- }
450
-
451
- /**
452
- * Checks the baud rate for validity
453
- * @param {number} baudRate the baud rate to check
454
- * @return {boolean} A boolean that reflects whether the baud rate is valid
455
- */
456
- private isValidBaudRate(baudRate: number): boolean {
457
- return baudRate % 1 === 0;
458
- }
459
-
460
- /**
461
- * Checks the data bits for validity
462
- * @param {number} dataBits the data bits to check
463
- * @return {boolean} A boolean that reflects whether the data bits setting is
464
- * valid
465
- */
466
- private isValidDataBits(dataBits: number | undefined): boolean {
467
- if (typeof dataBits === 'undefined') {
468
- return true;
469
- }
470
- return kAcceptableDataBits.includes(dataBits);
471
- }
472
-
473
- /**
474
- * Checks the stop bits for validity
475
- * @param {number} stopBits the stop bits to check
476
- * @return {boolean} A boolean that reflects whether the stop bits setting is
477
- * valid
478
- */
479
- private isValidStopBits(stopBits: number | undefined): boolean {
480
- if (typeof stopBits === 'undefined') {
481
- return true;
482
- }
483
- return kAcceptableStopBits.includes(stopBits);
484
- }
485
-
486
- /**
487
- * Checks the parity for validity
488
- * @param {string} parity the parity to check
489
- * @return {boolean} A boolean that reflects whether the parity is valid
490
- */
491
- private isValidParity(parity: ParityType | undefined): boolean {
492
- if (typeof parity === 'undefined') {
493
- return true;
494
- }
495
- return kAcceptableParity.includes(parity);
496
- }
497
-
498
- /**
499
- * sends the options alog the control interface to set them on the device
500
- * @return {Promise} a promise that will resolve when the options are set
501
- */
502
- private async setLineCoding(): Promise<void> {
503
- // Ref: USB CDC specification version 1.1 §6.2.12.
504
- const buffer = new ArrayBuffer(7);
505
- const view = new DataView(buffer);
506
- view.setUint32(0, this.serialOptions_.baudRate, true);
507
- view.setUint8(
508
- 4, kStopBitsIndexMapping.indexOf(
509
- this.serialOptions_.stopBits ?? kDefaultStopBits));
510
- view.setUint8(
511
- 5, kParityIndexMapping.indexOf(
512
- this.serialOptions_.parity ?? kDefaultParity));
513
- view.setUint8(6, this.serialOptions_.dataBits ?? kDefaultDataBits);
514
-
515
- const result = await this.device_.controlTransferOut({
516
- 'requestType': 'class',
517
- 'recipient': 'interface',
518
- 'request': kSetLineCoding,
519
- 'value': 0x00,
520
- 'index': this.controlInterface_.interfaceNumber,
521
- }, buffer);
522
- if (result.status != 'ok') {
523
- throw new DOMException('NetworkError', 'Failed to set line coding.');
524
- }
525
- }
526
- }
527
-
528
- /** generic implementation of navigator.serial object */
529
- export abstract class BaseSerial<T extends SerialPort> {
530
- /**
531
- * @param {USB} usb Instance of navigator.usb object
532
- */
533
- constructor(
534
- protected readonly usb: USB,
535
- ) { }
536
-
537
- protected abstract createPort(device: USBDevice,
538
- options?: SerialPolyfillOptions): T
539
-
540
- /**
541
- * Requests permission to access a new port.
542
- *
543
- * @param {SerialPortRequestOptions} options
544
- * @param {SerialPolyfillOptions} polyfillOptions
545
- * @return {Promise<SerialPort>}
546
- */
547
- async requestPort(
548
- options?: SerialPortRequestOptions,
549
- polyfillOptions?: SerialPolyfillOptions): Promise<T> {
550
- polyfillOptions = {...kDefaultPolyfillOptions, ...polyfillOptions};
551
-
552
- const usbFilters: USBDeviceFilter[] = [];
553
- if (options && options.filters) {
554
- for (const filter of options.filters) {
555
- const usbFilter: USBDeviceFilter = {
556
- classCode: polyfillOptions.usbControlInterfaceClass,
557
- };
558
- if (filter.usbVendorId !== undefined) {
559
- usbFilter.vendorId = filter.usbVendorId;
560
- }
561
- if (filter.usbProductId !== undefined) {
562
- usbFilter.productId = filter.usbProductId;
563
- }
564
- usbFilters.push(usbFilter);
565
- }
566
- }
567
-
568
- if (usbFilters.length === 0) {
569
- usbFilters.push({
570
- classCode: polyfillOptions.usbControlInterfaceClass,
571
- });
572
- }
573
-
574
- const device = await this.usb.requestDevice({'filters': usbFilters});
575
- const port = this.createPort(device, polyfillOptions);
576
- return port;
577
- }
578
-
579
- /**
580
- * Get the set of currently available ports.
581
- *
582
- * @param {SerialPolyfillOptions} polyfillOptions Polyfill configuration that
583
- * should be applied to these ports.
584
- * @return {Promise<SerialPort[]>} a promise that is resolved with a list of
585
- * ports.
586
- */
587
- async getPorts(polyfillOptions?: SerialPolyfillOptions): Promise<T[]> {
588
- polyfillOptions = {...kDefaultPolyfillOptions, ...polyfillOptions};
589
-
590
- const devices = await this.usb.getDevices();
591
- const ports: T[] = [];
592
- devices.forEach((device) => {
593
- try {
594
- const port = this.createPort(device, polyfillOptions);
595
- ports.push(port);
596
- } catch (e) {
597
- // Skip unrecognized port.
598
- }
599
- });
600
- return ports;
601
- }
602
- }
603
-
604
- /** default implementation of the global navigator.serial object */
605
- export class Serial extends BaseSerial<SerialPort> {
606
- /**
607
- * @param {USBDevice} device
608
- * @param {SerialPolyfillOptions} options
609
- * @return {SerialPort} Default serial port implementation
610
- */
611
- protected createPort(device: USBDevice,
612
- options?: SerialPolyfillOptions): SerialPort {
613
- return new SerialPort(device, options);
614
- }
615
- }
616
-
617
- /**
618
- * Converts ArrayBufferLike to ArrayBuffer.
619
- *
620
- * @param {buffer} buffer
621
- * @return {ArrayBuffer} original ArrayBuffer
622
- * or new ArrayBuffer with contents of SharedArrayBuffer
623
- */
624
- function toArrayBuffer(buffer: ArrayBufferLike) {
625
- if (buffer instanceof ArrayBuffer) {
626
- // Return buffer when it's already an ArrayBuffer
627
- return buffer;
628
- }
629
-
630
- // Create a new ArrayBuffer with the same byte length
631
- const arrayBuffer = new ArrayBuffer(buffer.byteLength);
632
-
633
- // Create views for both buffers
634
- const sharedArrayBufferView = new Uint8Array(buffer);
635
- const arrayBufferView = new Uint8Array(arrayBuffer);
636
-
637
- // Copy the contents
638
- arrayBufferView.set(sharedArrayBufferView);
639
-
640
- return arrayBuffer;
641
- }
1
+ /*
2
+ * Copyright 2019 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the
5
+ * "License"); you may not use this file except in
6
+ * compliance with the License. You may obtain a copy of
7
+ * the License at
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in
12
+ * writing, software distributed under the License is
13
+ * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
14
+ * OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing
16
+ * permissions and limitations under the License.
17
+ */
18
+ 'use strict';
19
+
20
+ export enum SerialPolyfillProtocol {
21
+ UsbCdcAcm, // eslint-disable-line no-unused-vars
22
+ }
23
+
24
+ export interface SerialPolyfillOptions {
25
+ protocol?: SerialPolyfillProtocol;
26
+ usbControlInterfaceClass?: number;
27
+ usbTransferInterfaceClass?: number;
28
+ }
29
+
30
+ const kSetLineCoding = 0x20;
31
+ const kSetControlLineState = 0x22;
32
+ const kSendBreak = 0x23;
33
+
34
+ const kDefaultBufferSize = 255;
35
+ const kDefaultDataBits = 8;
36
+ const kDefaultParity = 'none';
37
+ const kDefaultStopBits = 1;
38
+
39
+ const kAcceptableDataBits = [16, 8, 7, 6, 5];
40
+ const kAcceptableStopBits = [1, 2];
41
+ const kAcceptableParity = ['none', 'even', 'odd'];
42
+
43
+ const kParityIndexMapping: ParityType[] =
44
+ ['none', 'odd', 'even'];
45
+ const kStopBitsIndexMapping = [1, 1.5, 2];
46
+
47
+ const kDefaultPolyfillOptions = {
48
+ protocol: SerialPolyfillProtocol.UsbCdcAcm,
49
+ usbControlInterfaceClass: 2,
50
+ usbTransferInterfaceClass: 10,
51
+ };
52
+
53
+ /**
54
+ * Utility function to get the interface implementing a desired class.
55
+ * @param {USBDevice} device The USB device.
56
+ * @param {number} classCode The desired interface class.
57
+ * @return {USBInterface} The first interface found that implements the desired
58
+ * class.
59
+ * @throws TypeError if no interface is found.
60
+ */
61
+ function findInterface(device: USBDevice, classCode: number): USBInterface {
62
+ const configuration = device.configurations[0];
63
+ for (const iface of configuration.interfaces) {
64
+ const alternate = iface.alternates[0];
65
+ if (alternate.interfaceClass === classCode) {
66
+ return iface;
67
+ }
68
+ }
69
+ throw new TypeError(`Unable to find interface with class ${classCode}.`);
70
+ }
71
+
72
+ /**
73
+ * Utility function to get an endpoint with a particular direction.
74
+ * @param {USBInterface} iface The interface to search.
75
+ * @param {USBDirection} direction The desired transfer direction.
76
+ * @return {USBEndpoint} The first endpoint with the desired transfer direction.
77
+ * @throws TypeError if no endpoint is found.
78
+ */
79
+ function findEndpoint(iface: USBInterface, direction: USBDirection):
80
+ USBEndpoint {
81
+ const alternate = iface.alternates[0];
82
+ for (const endpoint of alternate.endpoints) {
83
+ if (endpoint.direction == direction) {
84
+ return endpoint;
85
+ }
86
+ }
87
+ throw new TypeError(`Interface ${iface.interfaceNumber} does not have an ` +
88
+ `${direction} endpoint.`);
89
+ }
90
+
91
+ /**
92
+ * Implementation of the underlying source API[1] which reads data from a USB
93
+ * endpoint. This can be used to construct a ReadableStream.
94
+ *
95
+ * [1]: https://streams.spec.whatwg.org/#underlying-source-api
96
+ */
97
+ class UsbEndpointUnderlyingSource implements UnderlyingByteSource {
98
+ private device_: USBDevice;
99
+ private endpoint_: USBEndpoint;
100
+ private onError_: () => void;
101
+
102
+ type: 'bytes';
103
+
104
+ /**
105
+ * Constructs a new UnderlyingSource that will pull data from the specified
106
+ * endpoint on the given USB device.
107
+ *
108
+ * @param {USBDevice} device
109
+ * @param {USBEndpoint} endpoint
110
+ * @param {function} onError function to be called on error
111
+ */
112
+ constructor(device: USBDevice, endpoint: USBEndpoint, onError: () => void) {
113
+ this.type = 'bytes';
114
+ this.device_ = device;
115
+ this.endpoint_ = endpoint;
116
+ this.onError_ = onError;
117
+ }
118
+
119
+ /**
120
+ * Reads a chunk of data from the device.
121
+ *
122
+ * @param {ReadableByteStreamController} controller
123
+ */
124
+ async pull(controller: ReadableByteStreamController): Promise<void> {
125
+ let chunkSize;
126
+ if (controller.desiredSize) {
127
+ const d = controller.desiredSize / this.endpoint_.packetSize;
128
+ chunkSize = Math.ceil(d) * this.endpoint_.packetSize;
129
+ } else {
130
+ chunkSize = this.endpoint_.packetSize;
131
+ }
132
+
133
+ try {
134
+ const result = await this.device_.transferIn(
135
+ this.endpoint_.endpointNumber, chunkSize);
136
+ if (result.status != 'ok') {
137
+ controller.error(`USB error: ${result.status}`);
138
+ this.onError_();
139
+ }
140
+ if (result.data?.buffer) {
141
+ const chunk = new Uint8Array(
142
+ toArrayBuffer(result.data.buffer), result.data.byteOffset,
143
+ result.data.byteLength);
144
+ controller.enqueue(chunk);
145
+ }
146
+ } catch (error) {
147
+ controller.error(error);
148
+ this.onError_();
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Implementation of the underlying sink API[2] which writes data to a USB
155
+ * endpoint. This can be used to construct a WritableStream.
156
+ *
157
+ * [2]: https://streams.spec.whatwg.org/#underlying-sink-api
158
+ */
159
+ class UsbEndpointUnderlyingSink implements UnderlyingSink<Uint8Array> {
160
+ private device_: USBDevice;
161
+ private endpoint_: USBEndpoint;
162
+ private onError_: () => void;
163
+
164
+ /**
165
+ * Constructs a new UnderlyingSink that will write data to the specified
166
+ * endpoint on the given USB device.
167
+ *
168
+ * @param {USBDevice} device
169
+ * @param {USBEndpoint} endpoint
170
+ * @param {function} onError function to be called on error
171
+ */
172
+ constructor(device: USBDevice, endpoint: USBEndpoint, onError: () => void) {
173
+ this.device_ = device;
174
+ this.endpoint_ = endpoint;
175
+ this.onError_ = onError;
176
+ }
177
+
178
+ /**
179
+ * Writes a chunk to the device.
180
+ *
181
+ * @param {Uint8Array} chunk
182
+ * @param {WritableStreamDefaultController} controller
183
+ */
184
+ async write(
185
+ chunk: Uint8Array<ArrayBufferLike>,
186
+ controller: WritableStreamDefaultController): Promise<void> {
187
+ try {
188
+ const result =
189
+ await this.device_.transferOut(this.endpoint_.endpointNumber, new Uint8Array(chunk));
190
+ if (result.status != 'ok') {
191
+ controller.error(result.status);
192
+ this.onError_();
193
+ }
194
+ } catch (error) {
195
+ controller.error(error);
196
+ this.onError_();
197
+ }
198
+ }
199
+ }
200
+
201
+ export type BaseSerialPort = Omit<SerialPort, "getSignals" | "onconnect" | "ondisconnect" | keyof EventTarget>
202
+
203
+ /** a class used to control serial devices over WebUSB */
204
+ class SerialPortPolyfill implements BaseSerialPort {
205
+ private polyfillOptions_: SerialPolyfillOptions;
206
+ private device_: USBDevice;
207
+ private controlInterface_: USBInterface;
208
+ private transferInterface_: USBInterface;
209
+ private inEndpoint_: USBEndpoint;
210
+ private outEndpoint_: USBEndpoint;
211
+
212
+ private serialOptions_: SerialOptions;
213
+ private readable_: ReadableStream<Uint8Array> | null;
214
+ private writable_: WritableStream<Uint8Array> | null;
215
+ private outputSignals_: SerialOutputSignals;
216
+
217
+ /**
218
+ * constructor taking a WebUSB device that creates a SerialPort instance.
219
+ * @param {USBDevice} device A device acquired from the WebUSB API
220
+ * @param {SerialPolyfillOptions} polyfillOptions Optional options to
221
+ * configure the polyfill.
222
+ */
223
+ public constructor(
224
+ device: USBDevice,
225
+ polyfillOptions?: SerialPolyfillOptions) {
226
+ this.polyfillOptions_ = {...kDefaultPolyfillOptions, ...polyfillOptions};
227
+ this.outputSignals_ = {
228
+ dataTerminalReady: false,
229
+ requestToSend: false,
230
+ break: false,
231
+ };
232
+
233
+ this.device_ = device;
234
+ this.controlInterface_ = findInterface(
235
+ this.device_,
236
+ this.polyfillOptions_.usbControlInterfaceClass as number);
237
+ this.transferInterface_ = findInterface(
238
+ this.device_,
239
+ this.polyfillOptions_.usbTransferInterfaceClass as number);
240
+ this.inEndpoint_ = findEndpoint(this.transferInterface_, 'in');
241
+ this.outEndpoint_ = findEndpoint(this.transferInterface_, 'out');
242
+ }
243
+
244
+ /**
245
+ * Getter for checking whether device is connected.
246
+ * Since there is no equivalent getter on USBDevice instance,
247
+ * this getter returns "true" when device is opened, not connected.
248
+ *
249
+ * @returns {boolean} "true" when the underlying USB device is opened.
250
+ */
251
+ public get connected(): boolean {
252
+ return this.device_.opened
253
+ }
254
+
255
+ /**
256
+ * Getter for the readable attribute. Constructs a new ReadableStream as
257
+ * necessary.
258
+ * @return {ReadableStream} the current readable stream
259
+ */
260
+ public get readable(): ReadableStream<Uint8Array> | null {
261
+ if (!this.readable_ && this.device_.opened) {
262
+ this.readable_ = new ReadableStream<Uint8Array>(
263
+ new UsbEndpointUnderlyingSource(
264
+ this.device_, this.inEndpoint_, () => {
265
+ this.readable_ = null;
266
+ }),
267
+ {
268
+ highWaterMark: this.serialOptions_.bufferSize ?? kDefaultBufferSize,
269
+ });
270
+ }
271
+ return this.readable_;
272
+ }
273
+
274
+ /**
275
+ * Getter for the writable attribute. Constructs a new WritableStream as
276
+ * necessary.
277
+ * @return {WritableStream} the current writable stream
278
+ */
279
+ public get writable(): WritableStream<Uint8Array> | null {
280
+ if (!this.writable_ && this.device_.opened) {
281
+ this.writable_ = new WritableStream(
282
+ new UsbEndpointUnderlyingSink(
283
+ this.device_, this.outEndpoint_, () => {
284
+ this.writable_ = null;
285
+ }),
286
+ new ByteLengthQueuingStrategy({
287
+ highWaterMark: this.serialOptions_.bufferSize ?? kDefaultBufferSize,
288
+ }));
289
+ }
290
+ return this.writable_;
291
+ }
292
+
293
+ /**
294
+ * Release transfer and control interfaces.
295
+ * Used before closing device.
296
+ */
297
+ private async releaseInterfaces_(): Promise<void> {
298
+ await this.device_.releaseInterface(
299
+ this.transferInterface_.interfaceNumber
300
+ );
301
+ await this.device_.releaseInterface(
302
+ this.controlInterface_.interfaceNumber
303
+ );
304
+ }
305
+
306
+ /**
307
+ * a function that opens the device and claims all interfaces needed to
308
+ * control and communicate to and from the serial device
309
+ * @param {SerialOptions} options Object containing serial options
310
+ * @return {Promise<void>} A promise that will resolve when device is ready
311
+ * for communication
312
+ */
313
+ public async open(options: SerialOptions): Promise<void> {
314
+ this.serialOptions_ = options;
315
+ this.validateOptions();
316
+
317
+ try {
318
+ await this.device_.open();
319
+ if (this.device_.configuration === null) {
320
+ await this.device_.selectConfiguration(1);
321
+ }
322
+
323
+ await this.device_.claimInterface(this.controlInterface_.interfaceNumber);
324
+ if (this.controlInterface_ !== this.transferInterface_) {
325
+ await this.device_.claimInterface(
326
+ this.transferInterface_.interfaceNumber);
327
+ }
328
+
329
+ await this.setLineCoding();
330
+ await this.setSignals({dataTerminalReady: true});
331
+ } catch (error) {
332
+ if (this.device_.opened) {
333
+ await this.releaseInterfaces_();
334
+ await this.device_.close();
335
+ }
336
+ throw new Error('Error setting up device', { cause: error });
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Closes the port.
342
+ *
343
+ * @return {Promise<void>} A promise that will resolve when the port is
344
+ * closed.
345
+ */
346
+ public async close(): Promise<void> {
347
+ const promises = [];
348
+ if (this.readable_) {
349
+ promises.push(this.readable_.cancel());
350
+ }
351
+ if (this.writable_) {
352
+ promises.push(this.writable_.abort());
353
+ }
354
+ await Promise.all(promises);
355
+ this.readable_ = null;
356
+ this.writable_ = null;
357
+ if (this.device_.opened) {
358
+ await this.setSignals({dataTerminalReady: false, requestToSend: false});
359
+ await this.releaseInterfaces_();
360
+ await this.device_.close();
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Forgets the port.
366
+ *
367
+ * @return {Promise<void>} A promise that will resolve when the port is
368
+ * forgotten.
369
+ */
370
+ public async forget(): Promise<void> {
371
+ return this.device_.forget();
372
+ }
373
+
374
+ /**
375
+ * A function that returns properties of the device.
376
+ * @return {SerialPortInfo} Device properties.
377
+ */
378
+ public getInfo(): SerialPortInfo {
379
+ return {
380
+ usbVendorId: this.device_.vendorId,
381
+ usbProductId: this.device_.productId,
382
+ };
383
+ }
384
+
385
+ /**
386
+ * A function used to change the serial settings of the device
387
+ * @param {object} options the object which carries serial settings data
388
+ * @return {Promise<void>} A promise that will resolve when the options are
389
+ * set
390
+ */
391
+ public reconfigure(options: SerialOptions): Promise<void> {
392
+ this.serialOptions_ = {...this.serialOptions_, ...options};
393
+ this.validateOptions();
394
+ return this.setLineCoding();
395
+ }
396
+
397
+ /**
398
+ * Sets control signal state for the port.
399
+ * @param {SerialOutputSignals} signals The signals to enable or disable.
400
+ * @return {Promise<void>} a promise that is resolved when the signal state
401
+ * has been changed.
402
+ */
403
+ public async setSignals(signals: SerialOutputSignals): Promise<void> {
404
+ this.outputSignals_ = {...this.outputSignals_, ...signals};
405
+
406
+ if (signals.dataTerminalReady !== undefined ||
407
+ signals.requestToSend !== undefined) {
408
+ // The Set_Control_Line_State command expects a bitmap containing the
409
+ // values of all output signals that should be enabled or disabled.
410
+ //
411
+ // Ref: USB CDC specification version 1.1 §6.2.14.
412
+ const value = (this.outputSignals_.dataTerminalReady ? 1 << 0 : 0) |
413
+ (this.outputSignals_.requestToSend ? 1 << 1 : 0);
414
+
415
+ await this.device_.controlTransferOut({
416
+ 'requestType': 'class',
417
+ 'recipient': 'interface',
418
+ 'request': kSetControlLineState,
419
+ 'value': value,
420
+ 'index': this.controlInterface_.interfaceNumber,
421
+ });
422
+ }
423
+
424
+ if (signals.break !== undefined) {
425
+ // The SendBreak command expects to be given a duration for how long the
426
+ // break signal should be asserted. Passing 0xFFFF enables the signal
427
+ // until 0x0000 is send.
428
+ //
429
+ // Ref: USB CDC specification version 1.1 §6.2.15.
430
+ const value = this.outputSignals_.break ? 0xFFFF : 0x0000;
431
+
432
+ await this.device_.controlTransferOut({
433
+ 'requestType': 'class',
434
+ 'recipient': 'interface',
435
+ 'request': kSendBreak,
436
+ 'value': value,
437
+ 'index': this.controlInterface_.interfaceNumber,
438
+ });
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Checks the serial options for validity and throws an error if it is
444
+ * not valid
445
+ */
446
+ private validateOptions(): void {
447
+ if (!this.isValidBaudRate(this.serialOptions_.baudRate)) {
448
+ throw new RangeError('invalid Baud Rate ' + this.serialOptions_.baudRate);
449
+ }
450
+
451
+ if (!this.isValidDataBits(this.serialOptions_.dataBits)) {
452
+ throw new RangeError('invalid dataBits ' + this.serialOptions_.dataBits);
453
+ }
454
+
455
+ if (!this.isValidStopBits(this.serialOptions_.stopBits)) {
456
+ throw new RangeError('invalid stopBits ' + this.serialOptions_.stopBits);
457
+ }
458
+
459
+ if (!this.isValidParity(this.serialOptions_.parity)) {
460
+ throw new RangeError('invalid parity ' + this.serialOptions_.parity);
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Checks the baud rate for validity
466
+ * @param {number} baudRate the baud rate to check
467
+ * @return {boolean} A boolean that reflects whether the baud rate is valid
468
+ */
469
+ private isValidBaudRate(baudRate: number): boolean {
470
+ return baudRate % 1 === 0;
471
+ }
472
+
473
+ /**
474
+ * Checks the data bits for validity
475
+ * @param {number} dataBits the data bits to check
476
+ * @return {boolean} A boolean that reflects whether the data bits setting is
477
+ * valid
478
+ */
479
+ private isValidDataBits(dataBits: number | undefined): boolean {
480
+ if (typeof dataBits === 'undefined') {
481
+ return true;
482
+ }
483
+ return kAcceptableDataBits.includes(dataBits);
484
+ }
485
+
486
+ /**
487
+ * Checks the stop bits for validity
488
+ * @param {number} stopBits the stop bits to check
489
+ * @return {boolean} A boolean that reflects whether the stop bits setting is
490
+ * valid
491
+ */
492
+ private isValidStopBits(stopBits: number | undefined): boolean {
493
+ if (typeof stopBits === 'undefined') {
494
+ return true;
495
+ }
496
+ return kAcceptableStopBits.includes(stopBits);
497
+ }
498
+
499
+ /**
500
+ * Checks the parity for validity
501
+ * @param {string} parity the parity to check
502
+ * @return {boolean} A boolean that reflects whether the parity is valid
503
+ */
504
+ private isValidParity(parity: ParityType | undefined): boolean {
505
+ if (typeof parity === 'undefined') {
506
+ return true;
507
+ }
508
+ return kAcceptableParity.includes(parity);
509
+ }
510
+
511
+ /**
512
+ * sends the options alog the control interface to set them on the device
513
+ * @return {Promise} a promise that will resolve when the options are set
514
+ */
515
+ private async setLineCoding(): Promise<void> {
516
+ // Ref: USB CDC specification version 1.1 §6.2.12.
517
+ const buffer = new ArrayBuffer(7);
518
+ const view = new DataView(buffer);
519
+ view.setUint32(0, this.serialOptions_.baudRate, true);
520
+ view.setUint8(
521
+ 4, kStopBitsIndexMapping.indexOf(
522
+ this.serialOptions_.stopBits ?? kDefaultStopBits));
523
+ view.setUint8(
524
+ 5, kParityIndexMapping.indexOf(
525
+ this.serialOptions_.parity ?? kDefaultParity));
526
+ view.setUint8(6, this.serialOptions_.dataBits ?? kDefaultDataBits);
527
+
528
+ const result = await this.device_.controlTransferOut({
529
+ 'requestType': 'class',
530
+ 'recipient': 'interface',
531
+ 'request': kSetLineCoding,
532
+ 'value': 0x00,
533
+ 'index': this.controlInterface_.interfaceNumber,
534
+ }, buffer);
535
+ if (result.status != 'ok') {
536
+ throw new DOMException('NetworkError', 'Failed to set line coding.');
537
+ }
538
+ }
539
+ }
540
+
541
+ export { SerialPortPolyfill as SerialPort }
542
+
543
+ /** generic implementation of navigator.serial object */
544
+ export abstract class BaseSerial<T extends BaseSerialPort> {
545
+ /**
546
+ * @param {USB} usb Instance of navigator.usb object
547
+ */
548
+ constructor(
549
+ protected readonly usb: USB,
550
+ ) { }
551
+
552
+ protected abstract createPort(device: USBDevice,
553
+ options?: SerialPolyfillOptions): T
554
+
555
+ /**
556
+ * Requests permission to access a new port.
557
+ *
558
+ * @param {SerialPortRequestOptions} options
559
+ * @param {SerialPolyfillOptions} polyfillOptions
560
+ * @return {Promise<T>}
561
+ */
562
+ async requestPort(
563
+ options?: SerialPortRequestOptions,
564
+ polyfillOptions?: SerialPolyfillOptions): Promise<T> {
565
+ polyfillOptions = {...kDefaultPolyfillOptions, ...polyfillOptions};
566
+
567
+ const usbFilters: USBDeviceFilter[] = [];
568
+ if (options && options.filters) {
569
+ for (const filter of options.filters) {
570
+ const usbFilter: USBDeviceFilter = {
571
+ classCode: polyfillOptions.usbControlInterfaceClass,
572
+ };
573
+ if (filter.usbVendorId !== undefined) {
574
+ usbFilter.vendorId = filter.usbVendorId;
575
+ }
576
+ if (filter.usbProductId !== undefined) {
577
+ usbFilter.productId = filter.usbProductId;
578
+ }
579
+ usbFilters.push(usbFilter);
580
+ }
581
+ }
582
+
583
+ if (usbFilters.length === 0) {
584
+ usbFilters.push({
585
+ classCode: polyfillOptions.usbControlInterfaceClass,
586
+ });
587
+ }
588
+
589
+ const device = await this.usb.requestDevice({'filters': usbFilters});
590
+ const port = this.createPort(device, polyfillOptions);
591
+ return port;
592
+ }
593
+
594
+ /**
595
+ * Get the set of currently available ports.
596
+ *
597
+ * @param {SerialPolyfillOptions} polyfillOptions Polyfill configuration that
598
+ * should be applied to these ports.
599
+ * @return {Promise<T[]>} a promise that is resolved with a list of
600
+ * ports.
601
+ */
602
+ async getPorts(polyfillOptions?: SerialPolyfillOptions): Promise<T[]> {
603
+ polyfillOptions = {...kDefaultPolyfillOptions, ...polyfillOptions};
604
+
605
+ const devices = await this.usb.getDevices();
606
+ const ports: T[] = [];
607
+ devices.forEach((device) => {
608
+ try {
609
+ const port = this.createPort(device, polyfillOptions);
610
+ ports.push(port);
611
+ } catch (e) {
612
+ // Skip unrecognized port.
613
+ }
614
+ });
615
+ return ports;
616
+ }
617
+ }
618
+
619
+ /** default implementation of the global navigator.serial object */
620
+ export class Serial extends BaseSerial<SerialPortPolyfill> {
621
+ /**
622
+ * @param {USBDevice} device
623
+ * @param {SerialPolyfillOptions} options
624
+ * @return {SerialPortPolyfill} Default serial port implementation
625
+ */
626
+ protected createPort(device: USBDevice,
627
+ options?: SerialPolyfillOptions): SerialPortPolyfill {
628
+ return new SerialPortPolyfill(device, options);
629
+ }
630
+ }
631
+
632
+ /**
633
+ * Converts ArrayBufferLike to ArrayBuffer.
634
+ *
635
+ * @param {buffer} buffer
636
+ * @return {ArrayBuffer} original ArrayBuffer
637
+ * or new ArrayBuffer with contents of SharedArrayBuffer
638
+ */
639
+ function toArrayBuffer(buffer: ArrayBufferLike) {
640
+ if (buffer instanceof ArrayBuffer) {
641
+ // Return buffer when it's already an ArrayBuffer
642
+ return buffer;
643
+ }
644
+
645
+ // Create a new ArrayBuffer with the same byte length
646
+ const arrayBuffer = new ArrayBuffer(buffer.byteLength);
647
+
648
+ // Create views for both buffers
649
+ const sharedArrayBufferView = new Uint8Array(buffer);
650
+ const arrayBufferView = new Uint8Array(arrayBuffer);
651
+
652
+ // Copy the contents
653
+ arrayBufferView.set(sharedArrayBufferView);
654
+
655
+ return arrayBuffer;
656
+ }