react-native-web-serial-api 0.0.1

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 (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +147 -0
  3. package/android/build.gradle +62 -0
  4. package/android/gradle.properties +6 -0
  5. package/android/src/main/AndroidManifest.xml +21 -0
  6. package/android/src/main/java/dev/webserialapi/NativeUsbSerialModule.java +704 -0
  7. package/android/src/main/java/dev/webserialapi/NativeUsbSerialPackage.java +46 -0
  8. package/android/src/main/java/dev/webserialapi/PortPickerActivity.java +235 -0
  9. package/android/src/main/java/dev/webserialapi/UsbDetachReceiver.java +37 -0
  10. package/android/src/main/res/xml/device_filter.xml +13 -0
  11. package/babel.config.js +3 -0
  12. package/biome.json +35 -0
  13. package/example/.watchmanconfig +1 -0
  14. package/example/App.tsx +71 -0
  15. package/example/__tests__/App.test.tsx +16 -0
  16. package/example/android/app/build.gradle +120 -0
  17. package/example/android/app/debug.keystore +0 -0
  18. package/example/android/app/proguard-rules.pro +10 -0
  19. package/example/android/app/src/debug/AndroidManifest.xml +9 -0
  20. package/example/android/app/src/main/AndroidManifest.xml +38 -0
  21. package/example/android/app/src/main/java/dev/uzlopak/MainActivity.kt +22 -0
  22. package/example/android/app/src/main/java/dev/uzlopak/MainApplication.kt +41 -0
  23. package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  24. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  25. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  26. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  27. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  28. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  29. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  30. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  31. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  32. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  33. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  34. package/example/android/app/src/main/res/values/strings.xml +3 -0
  35. package/example/android/app/src/main/res/values/styles.xml +9 -0
  36. package/example/android/build.gradle +22 -0
  37. package/example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  38. package/example/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  39. package/example/android/gradle.properties +47 -0
  40. package/example/android/gradlew +252 -0
  41. package/example/android/gradlew.bat +94 -0
  42. package/example/android/settings.gradle +6 -0
  43. package/example/app.json +4 -0
  44. package/example/babel.config.js +21 -0
  45. package/example/biome.json +47 -0
  46. package/example/deploy.sh +11 -0
  47. package/example/index.html +26 -0
  48. package/example/index.js +9 -0
  49. package/example/index.web.js +8 -0
  50. package/example/jest.config.js +12 -0
  51. package/example/metro.config.js +58 -0
  52. package/example/package-lock.json +14510 -0
  53. package/example/package.json +48 -0
  54. package/example/react-native.config.js +17 -0
  55. package/example/src/components/AppBar.tsx +73 -0
  56. package/example/src/components/Menu.tsx +90 -0
  57. package/example/src/components/SingleChoiceDialog.tsx +120 -0
  58. package/example/src/screens/ConnectScreen.tsx +195 -0
  59. package/example/src/screens/DevicesScreen.tsx +141 -0
  60. package/example/src/screens/TerminalScreen.tsx +564 -0
  61. package/example/src/settings.ts +43 -0
  62. package/example/src/theme.ts +19 -0
  63. package/example/src/util/TextUtil.ts +129 -0
  64. package/example/tsconfig.json +10 -0
  65. package/example/vite.config.mjs +55 -0
  66. package/lib/commonjs/NativeUsbSerial.js +11 -0
  67. package/lib/commonjs/NativeUsbSerial.js.map +1 -0
  68. package/lib/commonjs/NativeUsbSerial.web.js +12 -0
  69. package/lib/commonjs/NativeUsbSerial.web.js.map +1 -0
  70. package/lib/commonjs/UsbSerial.js +149 -0
  71. package/lib/commonjs/UsbSerial.js.map +1 -0
  72. package/lib/commonjs/WebSerial.js +852 -0
  73. package/lib/commonjs/WebSerial.js.map +1 -0
  74. package/lib/commonjs/index.js +44 -0
  75. package/lib/commonjs/index.js.map +1 -0
  76. package/lib/commonjs/package.json +1 -0
  77. package/lib/commonjs/serial.android.js +13 -0
  78. package/lib/commonjs/serial.android.js.map +1 -0
  79. package/lib/commonjs/serial.js +13 -0
  80. package/lib/commonjs/serial.js.map +1 -0
  81. package/lib/commonjs/serial.web.js +11 -0
  82. package/lib/commonjs/serial.web.js.map +1 -0
  83. package/lib/typescript/src/NativeUsbSerial.d.ts +51 -0
  84. package/lib/typescript/src/NativeUsbSerial.d.ts.map +1 -0
  85. package/lib/typescript/src/NativeUsbSerial.web.d.ts +3 -0
  86. package/lib/typescript/src/NativeUsbSerial.web.d.ts.map +1 -0
  87. package/lib/typescript/src/UsbSerial.d.ts +97 -0
  88. package/lib/typescript/src/UsbSerial.d.ts.map +1 -0
  89. package/lib/typescript/src/WebSerial.d.ts +236 -0
  90. package/lib/typescript/src/WebSerial.d.ts.map +1 -0
  91. package/lib/typescript/src/index.d.ts +7 -0
  92. package/lib/typescript/src/index.d.ts.map +1 -0
  93. package/lib/typescript/src/serial.android.d.ts +2 -0
  94. package/lib/typescript/src/serial.android.d.ts.map +1 -0
  95. package/lib/typescript/src/serial.d.ts +2 -0
  96. package/lib/typescript/src/serial.d.ts.map +1 -0
  97. package/lib/typescript/src/serial.web.d.ts +4 -0
  98. package/lib/typescript/src/serial.web.d.ts.map +1 -0
  99. package/package.json +78 -0
  100. package/react-native.config.js +9 -0
  101. package/scripts/deploy-release.sh +127 -0
  102. package/src/NativeUsbSerial.ts +124 -0
  103. package/src/NativeUsbSerial.web.ts +5 -0
  104. package/src/UsbSerial.ts +305 -0
  105. package/src/WebSerial.ts +1084 -0
  106. package/src/index.ts +23 -0
  107. package/src/lib/dom-exception.ts +161 -0
  108. package/src/lib/event-target.ts +170 -0
  109. package/src/lib/promise.ts +19 -0
  110. package/src/serial.android.ts +1 -0
  111. package/src/serial.ts +1 -0
  112. package/src/serial.web.ts +6 -0
  113. package/tsconfig.build.json +7 -0
  114. package/tsconfig.json +20 -0
@@ -0,0 +1,1084 @@
1
+ import {
2
+ ByteLengthQueuingStrategy,
3
+ ReadableStream,
4
+ WritableStream,
5
+ } from 'web-streams-polyfill';
6
+ import {DOMException} from './lib/dom-exception';
7
+ import {Event, EventTarget} from './lib/event-target';
8
+ import {createDeferredPromise} from './lib/promise';
9
+ import type {
10
+ ConnectEvent,
11
+ DataEvent,
12
+ ErrorEvent,
13
+ OpenOptions,
14
+ PortId,
15
+ UsbSerialModule,
16
+ } from './UsbSerial';
17
+ import {getUsbSerial} from './UsbSerial';
18
+
19
+ /**
20
+ * @see https://wicg.github.io/serial/#dom-paritytype
21
+ *
22
+ * none - No parity bit is sent for each data word.
23
+ * even - Data word plus parity bit has even parity.
24
+ * odd - Data word plus parity bit has odd parity.
25
+ */
26
+ type ParityType = 'none' | 'even' | 'odd';
27
+
28
+ /**
29
+ * @see https://wicg.github.io/serial/#dom-flowcontroltype
30
+ * none - No flow control is enabled.
31
+ * hardware - Hardware flow control using the RTS and CTS signals is enabled.
32
+ */
33
+ type FlowControlType = 'none' | 'hardware';
34
+
35
+ /**
36
+ * @see https://wicg.github.io/serial/#dom-serialoptions
37
+ */
38
+ export type SerialOptions = {
39
+ /**
40
+ * A positive, non-zero value indicating the baud rate at which serial
41
+ * communication should be established.
42
+ *
43
+ * Note: baudRate is the only required member of this dictionary. While there
44
+ * are common default for other connection parameters it is important for
45
+ * developers to consider and consult with the documentation for devices they
46
+ * intend to connect to determine the correct values. While some values are
47
+ * common there is no standard baud rate. Requiring this parameter reduces the
48
+ * potential for confusion if an arbitrary default were chosen by this
49
+ * specification.
50
+ */
51
+ baudRate: number;
52
+ /**
53
+ * The number of data bits per frame. Either 7 or 8.
54
+ */
55
+ dataBits?: 7 | 8;
56
+ /**
57
+ * The number of stop bits at the end of a frame. Either 1 or 2.
58
+ */
59
+ stopBits?: 1 | 2;
60
+ /**
61
+ * The parity mode.
62
+ */
63
+ parity?: ParityType;
64
+ /**
65
+ * A positive, non-zero value indicating the size of the read and write
66
+ * buffers that should be created.
67
+ */
68
+ bufferSize?: number;
69
+ /**
70
+ * The flow control mode.
71
+ */
72
+ flowControl?: FlowControlType;
73
+ };
74
+
75
+ /**
76
+ * @see https://wicg.github.io/serial/#dom-serialoutputsignals
77
+ */
78
+ export type SerialOutputSignals = {
79
+ /**
80
+ * Data Terminal Ready (DTR)
81
+ */
82
+ dataTerminalReady?: boolean;
83
+ /**
84
+ * Request To Send (RTS)
85
+ */
86
+ requestToSend?: boolean;
87
+ /**
88
+ * Break
89
+ */
90
+ break?: boolean;
91
+ };
92
+
93
+ /**
94
+ * @see https://wicg.github.io/serial/#dom-serialinputsignals
95
+ */
96
+ export type SerialInputSignals = {
97
+ /**
98
+ * Data Carrier Detect (DCD)
99
+ */
100
+ dataCarrierDetect: boolean;
101
+ /**
102
+ * Clear To Send (CTS)
103
+ */
104
+ clearToSend: boolean;
105
+ /**
106
+ * Ring Indicator (RI)
107
+ */
108
+ ringIndicator: boolean;
109
+ /**
110
+ * Data Set Ready (DSR)
111
+ */
112
+ dataSetReady: boolean;
113
+ };
114
+
115
+ /**
116
+ * @see https://wicg.github.io/serial/#serialportinfo-dictionary
117
+ */
118
+ export type SerialPortInfo =
119
+ | {
120
+ /** USB Vendor ID */
121
+ usbVendorId: number;
122
+ /** USB Product ID */
123
+ usbProductId: number;
124
+ bluetoothServiceClassId?: never;
125
+ }
126
+ | {
127
+ usbVendorId?: never;
128
+ usbProductId?: never;
129
+ /** Bluetooth service class ID */
130
+ bluetoothServiceClassId: number | string;
131
+ };
132
+
133
+ /**
134
+ * @see https://wicg.github.io/serial/#dom-serialportfilter
135
+ */
136
+ export type SerialPortFilter = {
137
+ /** USB Vendor ID */
138
+ usbVendorId?: number;
139
+ /** USB Product ID */
140
+ usbProductId?: number;
141
+ /** Bluetooth service class ID */
142
+ bluetoothServiceClassId?: number | string;
143
+ };
144
+
145
+ /**
146
+ * @see https://wicg.github.io/serial/#dom-serialportrequestoptions
147
+ */
148
+ export type SerialPortRequestOptions = {
149
+ /**
150
+ * Filters for serial ports
151
+ */
152
+ filters?: SerialPortFilter[];
153
+ /**
154
+ * A list of BluetoothServiceUUID values representing Bluetooth service class
155
+ * IDs. Bluetooth ports with custom service class IDs are excluded from the
156
+ * list of ports presented to the user unless the service class ID is included
157
+ * in this list.
158
+ */
159
+ allowedBluetoothServiceClassIds?: (number | string)[];
160
+ };
161
+
162
+ // Maps SerialPort instances to their deviceId — avoids exposing deviceId on
163
+ // the public type while still allowing Serial to access it for requestPermission
164
+ const serialPortDeviceIds = new WeakMap<SerialPort, number>();
165
+
166
+ // "Friend" channel: lets Serial match/update/reset a SerialPort across USB
167
+ // detach + re-attach (Android assigns a new deviceId on every attach) without
168
+ // widening the public SerialPort API. Each port registers a set of closures
169
+ // over its #private fields in its constructor.
170
+ type PortInternals = {
171
+ getVid(): number | undefined;
172
+ getPid(): number | undefined;
173
+ getPortNumber(): number;
174
+ getDeviceId(): number;
175
+ setDeviceId(id: number): void;
176
+ /** Reset to a closed, re-openable state after the device is physically lost. */
177
+ handleDeviceLost(): void;
178
+ };
179
+ const portInternals = new WeakMap<SerialPort, PortInternals>();
180
+
181
+ const kDefaultDataBits = 8;
182
+ const kDefaultStopBits = 1;
183
+ const kDefaultParity: ParityType = 'none';
184
+ const kDefaultBufferSize = 255;
185
+ const kDefaultFlowControl: FlowControlType = 'none';
186
+
187
+ const kAcceptableDataBits = [7, 8] as const;
188
+ const kAcceptableStopBits = [1, 2] as const;
189
+ const kAcceptableParity: ParityType[] = ['none', 'even', 'odd'];
190
+
191
+ function parityToNative(parity: ParityType): number {
192
+ switch (parity) {
193
+ case 'odd':
194
+ return 1;
195
+ case 'even':
196
+ return 2;
197
+ default:
198
+ return 0;
199
+ }
200
+ }
201
+
202
+ function flowControlToNative(flowControl: FlowControlType): 'RTS_CTS' | 'NONE' {
203
+ return flowControl === 'hardware' ? 'RTS_CTS' : 'NONE';
204
+ }
205
+
206
+ /**
207
+ * Methods on this interface typically complete asynchronously, queuing work on
208
+ * the serial port task source.
209
+ *
210
+ * The get the parent algorithm for SerialPort returns the same Serial instance
211
+ * that is returned by the SerialPort's relevant global object's Navigator
212
+ * object's serial getter.
213
+ *
214
+ * Instances of SerialPort are created with the internal slots described in the
215
+ * following table:
216
+ *
217
+ * @see https://wicg.github.io/serial/#serialport-interface
218
+ */
219
+ export class SerialPort extends EventTarget {
220
+ #events = {
221
+ connect: null as ((event: Event) => void) | null,
222
+ disconnect: null as ((event: Event) => void) | null,
223
+ };
224
+
225
+ // Internal slots
226
+ #state:
227
+ | 'closed'
228
+ | 'opening'
229
+ | 'opened'
230
+ | 'closing'
231
+ | 'forgetting'
232
+ | 'forgotten' = 'closed'; // [[state]] = "closed"
233
+ #bufferSize: number | undefined = undefined; // [[bufferSize]] = undefined
234
+ #connected: boolean = false; // [[connected]] = false
235
+ #readable: ReadableStream<Uint8Array> | null = null; // [[readable]] = null
236
+ #readFatal: boolean = false; // [[readFatal]] = false
237
+ #writable: WritableStream<Uint8Array> | null = null; // [[writable]] = null
238
+ #writeFatal: boolean = false; // [[writeFatal]] = false
239
+ #pendingClosePromise: ReturnType<typeof createDeferredPromise<void>> | null =
240
+ null; // [[pendingClosePromise]] = null
241
+
242
+ #usb: UsbSerialModule;
243
+ #deviceId: number;
244
+ #portNumber: number;
245
+ #usbVendorId?: number;
246
+ #usbProductId?: number;
247
+
248
+ // Tracks the current state of output signals for partial updates
249
+ #outputSignals: Required<SerialOutputSignals> = {
250
+ dataTerminalReady: false,
251
+ requestToSend: false,
252
+ break: false,
253
+ };
254
+
255
+ #dataSubscription: {remove: () => void} | null = null;
256
+ #errorSubscription: {remove: () => void} | null = null;
257
+
258
+ constructor(
259
+ usb: UsbSerialModule,
260
+ deviceId: number,
261
+ portNumber: number,
262
+ usbVendorId: number,
263
+ usbProductId: number,
264
+ ) {
265
+ super();
266
+ this.#usb = usb;
267
+ this.#deviceId = deviceId;
268
+ this.#portNumber = portNumber;
269
+ serialPortDeviceIds.set(this, deviceId);
270
+ this.#usbVendorId = usbVendorId;
271
+ this.#usbProductId = usbProductId;
272
+
273
+ // Expose a private control channel to Serial (see portInternals).
274
+ portInternals.set(this, {
275
+ getVid: () => this.#usbVendorId,
276
+ getPid: () => this.#usbProductId,
277
+ getPortNumber: () => this.#portNumber,
278
+ getDeviceId: () => this.#deviceId,
279
+ setDeviceId: (id: number) => {
280
+ this.#deviceId = id;
281
+ serialPortDeviceIds.set(this, id);
282
+ },
283
+ handleDeviceLost: () => this.#handleDeviceLost(),
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Reset this port to a closed, re-openable state after the underlying USB
289
+ * device has been physically removed. Unlike close(), this must NOT touch the
290
+ * (now-gone) device — doing so would throw "USB get_status request failed".
291
+ * After this, a later open() (e.g. when the device is re-attached) succeeds.
292
+ */
293
+ #handleDeviceLost(): void {
294
+ // Tear down read/write streams without invoking the OS on the dead device.
295
+ if (this.#readable) {
296
+ this.#readFatal = true;
297
+ try {
298
+ // No active reader is required for cancel(); errors are non-fatal here.
299
+ this.#readable.cancel().catch(() => {});
300
+ } catch {}
301
+ }
302
+ if (this.#writable) {
303
+ this.#writeFatal = true;
304
+ try {
305
+ this.#writable.abort().catch(() => {});
306
+ } catch {}
307
+ }
308
+ this.#dataSubscription?.remove();
309
+ this.#errorSubscription?.remove();
310
+ this.#dataSubscription = null;
311
+ this.#errorSubscription = null;
312
+
313
+ this.#readable = null;
314
+ this.#writable = null;
315
+ this.#readFatal = false;
316
+ this.#writeFatal = false;
317
+ this.#pendingClosePromise = null;
318
+ this.#bufferSize = undefined;
319
+ this.#state = 'closed';
320
+ this.#connected = false;
321
+
322
+ this.dispatchEvent(new Event('disconnect'));
323
+ }
324
+
325
+ /**
326
+ * @see https://wicg.github.io/serial/#dom-serialport-onconnect
327
+ */
328
+ get onconnect() {
329
+ return this.#events.connect;
330
+ }
331
+
332
+ set onconnect(fn: ((event: Event) => void) | null) {
333
+ if (this.#events.connect) {
334
+ this.removeEventListener('connect', this.#events.connect);
335
+ }
336
+ if (fn !== null) {
337
+ this.addEventListener('connect', fn);
338
+ this.#events.connect = fn;
339
+ } else {
340
+ this.#events.connect = null;
341
+ }
342
+ }
343
+
344
+ /**
345
+ * @see https://wicg.github.io/serial/#dom-serialport-ondisconnect
346
+ */
347
+ get ondisconnect() {
348
+ return this.#events.disconnect;
349
+ }
350
+
351
+ set ondisconnect(fn: ((event: Event) => void) | null) {
352
+ if (this.#events.disconnect) {
353
+ this.removeEventListener('disconnect', this.#events.disconnect);
354
+ }
355
+ if (fn !== null) {
356
+ this.addEventListener('disconnect', fn);
357
+ this.#events.disconnect = fn;
358
+ } else {
359
+ this.#events.disconnect = null;
360
+ }
361
+ }
362
+
363
+ /**
364
+ * @see https://wicg.github.io/serial/#dom-serialport-connected
365
+ */
366
+ get connected(): boolean {
367
+ // The connected getter steps are:
368
+
369
+ // 1. Return this.[[connected]].
370
+ return this.#connected;
371
+ }
372
+
373
+ /**
374
+ * @see https://wicg.github.io/serial/#dom-serialport-readable
375
+ */
376
+ get readable(): ReadableStream<Uint8Array> | null {
377
+ // The readable getter steps are:
378
+
379
+ // 1. If this.[[readable]] is not null, return this.[[readable]].
380
+ if (this.#readable !== null) return this.#readable;
381
+
382
+ // 2. If this.[[state]] is not "opened", return null.
383
+ if (this.#state !== 'opened') return null;
384
+
385
+ // 3. If this.[[readFatal]] is true, return null.
386
+ if (this.#readFatal) return null;
387
+
388
+ // 4. Let stream be a new ReadableStream.
389
+ const self = this;
390
+ const deviceId = this.#deviceId;
391
+ const portNumber = this.#portNumber;
392
+ const bufferSize = this.#bufferSize!;
393
+
394
+ const stream = new ReadableStream<Uint8Array>(
395
+ {
396
+ start(controller) {
397
+ self.#dataSubscription = self.#usb.onData((event: DataEvent) => {
398
+ if (
399
+ event.deviceId === deviceId &&
400
+ event.portNumber === portNumber
401
+ ) {
402
+ controller.enqueue(new Uint8Array(event.data));
403
+ }
404
+ });
405
+
406
+ self.#errorSubscription = self.#usb.onError((event: ErrorEvent) => {
407
+ if (
408
+ event.deviceId === deviceId &&
409
+ event.portNumber === portNumber
410
+ ) {
411
+ self.#readFatal = true; // Set this.[[readFatal]] to true.
412
+ controller.error(new DOMException(event.error, 'NetworkError'));
413
+ self.#handleClosingReadableStream();
414
+ }
415
+ });
416
+ },
417
+ async cancel() {
418
+ // cancelAlgorithm: Invoke the operating system to discard the contents
419
+ // of all software and hardware receive buffers for the port.
420
+ await self.#usb
421
+ .purgeHwBuffers(deviceId, portNumber, false, true)
422
+ .catch(() => {});
423
+ self.#handleClosingReadableStream();
424
+ },
425
+ },
426
+ new ByteLengthQueuingStrategy({highWaterMark: bufferSize}),
427
+ );
428
+
429
+ // Set this.[[readable]] to stream.
430
+ this.#readable = stream;
431
+
432
+ // Return stream.
433
+ return stream;
434
+ }
435
+
436
+ /**
437
+ * @see https://wicg.github.io/serial/#dom-serialport-writable
438
+ */
439
+ get writable(): WritableStream<Uint8Array> | null {
440
+ // The writable getter steps are:
441
+
442
+ // 1. If this.[[writable]] is not null, return this.[[writable]].
443
+ if (this.#writable !== null) return this.#writable;
444
+
445
+ // 2. If this.[[state]] is not "opened", return null.
446
+ if (this.#state !== 'opened') return null;
447
+
448
+ // 3. If this.[[writeFatal]] is true, return null.
449
+ if (this.#writeFatal) return null;
450
+
451
+ // 4. Let stream be a new WritableStream.
452
+ const self = this;
453
+ const deviceId = this.#deviceId;
454
+ const portNumber = this.#portNumber;
455
+ const bufferSize = this.#bufferSize!;
456
+
457
+ const stream = new WritableStream<Uint8Array>(
458
+ {
459
+ async write(chunk) {
460
+ try {
461
+ await self.#usb.write(deviceId, portNumber, Array.from(chunk));
462
+ } catch (e) {
463
+ // If the port was disconnected, set this.[[writeFatal]] to true.
464
+ self.#writeFatal = true;
465
+ self.#handleClosingWritableStream();
466
+ throw new DOMException(
467
+ `Write failed: ${(e as Error).message}`,
468
+ 'NetworkError',
469
+ );
470
+ }
471
+ },
472
+ async abort() {
473
+ // abortAlgorithm: Invoke the operating system to discard the contents
474
+ // of all software and hardware transmit buffers for the port.
475
+ await self.#usb
476
+ .purgeHwBuffers(deviceId, portNumber, true, false)
477
+ .catch(() => {});
478
+ self.#handleClosingWritableStream();
479
+ },
480
+ async close() {
481
+ // closeAlgorithm: Invoke the operating system to flush the contents
482
+ // of all software and hardware transmit buffers for the port.
483
+ self.#handleClosingWritableStream();
484
+ },
485
+ },
486
+ new ByteLengthQueuingStrategy({highWaterMark: bufferSize}),
487
+ );
488
+
489
+ // Set this.[[writable]] to stream.
490
+ this.#writable = stream;
491
+
492
+ // Return stream.
493
+ return stream;
494
+ }
495
+
496
+ /**
497
+ * @see https://wicg.github.io/serial/#dom-serialport-getinfo
498
+ */
499
+ getInfo(): SerialPortInfo {
500
+ // The getInfo() method steps are:
501
+
502
+ // 1. Let info be an empty ordered map.
503
+ const info: SerialPortInfo = Object.create(null);
504
+
505
+ // 2. If the port is part of a USB device, perform the following steps:
506
+ if (this.#usbVendorId !== undefined) {
507
+ // 1. Set info["usbVendorId"] to the vendor ID of the device.
508
+ info.usbVendorId = this.#usbVendorId;
509
+ // 2. Set info["usbProductId"] to the product ID of the device.
510
+ info.usbProductId = this.#usbProductId;
511
+ }
512
+
513
+ // 3. If the port is a service on a Bluetooth device, perform the following steps:
514
+ // 1. Set info["bluetoothServiceClassId"] to the service class UUID of the Bluetooth service.
515
+
516
+ // 4. Return info.
517
+ return info;
518
+ }
519
+
520
+ /**
521
+ * Before communicating on a serial port it must be opened. Opening the port
522
+ * allows the site to specify the necessary parameters which control how data
523
+ * is transmitted and received. Developers should check the documentation for
524
+ * the device they are connecting to for the appropriate parameters.
525
+ *
526
+ * await port.open({ baudRate: 115200 });
527
+ *
528
+ * Once open() has resolved the readable and writable attributes can be
529
+ * accessed to get the ReadableStream and WritableStream instances for
530
+ * receiving data from and sending data to the connected device.
531
+ * @see https://wicg.github.io/serial/#dom-serialport-open
532
+ */
533
+ async open(options: SerialOptions): Promise<void> {
534
+ // The open() method steps are:
535
+
536
+ // 2. If this.[[state]] is not "closed", reject promise with an
537
+ // "InvalidStateError" DOMException and return promise.
538
+ if (this.#state !== 'closed') {
539
+ throw new DOMException('The port is already open.', 'InvalidStateError');
540
+ }
541
+
542
+ // 3. If options["baudRate"] is 0, reject promise with a TypeError and
543
+ // return promise.
544
+ if (options.baudRate === 0) {
545
+ throw new TypeError('baudRate must be a positive, non-zero value.');
546
+ }
547
+
548
+ // 4. If options["dataBits"] is not 7 or 8, reject promise with a TypeError
549
+ // and return promise.
550
+ const dataBits = options.dataBits ?? kDefaultDataBits;
551
+ if (!kAcceptableDataBits.includes(dataBits)) {
552
+ throw new TypeError('dataBits must be 7 or 8.');
553
+ }
554
+
555
+ // 5. If options["stopBits"] is not 1 or 2, reject promise with a TypeError
556
+ // and return promise.
557
+ const stopBits = options.stopBits ?? kDefaultStopBits;
558
+ if (!kAcceptableStopBits.includes(stopBits)) {
559
+ throw new TypeError('stopBits must be 1 or 2.');
560
+ }
561
+
562
+ // 6. If options["bufferSize"] is 0, reject promise with a TypeError and
563
+ // return promise.
564
+ const bufferSize = options.bufferSize ?? kDefaultBufferSize;
565
+ if (bufferSize === 0) {
566
+ throw new TypeError('bufferSize must be a positive, non-zero value.');
567
+ }
568
+
569
+ const parity = options.parity ?? kDefaultParity;
570
+ if (!kAcceptableParity.includes(parity)) {
571
+ throw new TypeError(
572
+ `parity must be one of: ${kAcceptableParity.join(', ')}.`,
573
+ );
574
+ }
575
+
576
+ const flowControl = options.flowControl ?? kDefaultFlowControl;
577
+
578
+ // 8. Set this.[[state]] to "opening".
579
+ this.#state = 'opening';
580
+
581
+ // 9.1. Invoke the operating system to open the serial port using the
582
+ // connection parameters.
583
+ const nativeOptions: OpenOptions = {
584
+ baudRate: options.baudRate,
585
+ dataBits,
586
+ stopBits,
587
+ parity: parityToNative(parity),
588
+ };
589
+
590
+ try {
591
+ await this.#usb.open(this.#deviceId, this.#portNumber, nativeOptions);
592
+ } catch (e) {
593
+ // 9.2. If this fails for any reason, reject promise with a "NetworkError"
594
+ // DOMException and abort these steps.
595
+ this.#state = 'closed';
596
+ throw new DOMException(
597
+ `Failed to open serial port: ${(e as Error).message}`,
598
+ 'NetworkError',
599
+ );
600
+ }
601
+
602
+ if (flowControl !== 'none') {
603
+ await this.#usb.setFlowControl(
604
+ this.#deviceId,
605
+ this.#portNumber,
606
+ flowControlToNative(flowControl),
607
+ );
608
+ }
609
+
610
+ this.#state = 'opened'; // 9.3. Set this.[[state]] to "opened".
611
+ this.#bufferSize = bufferSize; // 9.4. Set this.[[bufferSize]] to options["bufferSize"].
612
+
613
+ await this.#usb.startReading(this.#deviceId, this.#portNumber);
614
+
615
+ // When the port becomes logically connected:
616
+ this.#connected = true; // 2. Set port.[[connected]] to true.
617
+ // NOTE: the port-level "connect"/"disconnect" events represent physical
618
+ // device presence (attach/detach), dispatched by Serial from the native
619
+ // USB state events — not logical open()/close(). So we do not dispatch them
620
+ // here; that also avoids re-entrancy with auto-reconnect listeners.
621
+ }
622
+
623
+ /**
624
+ * @see https://wicg.github.io/serial/#dom-serialport-close
625
+ */
626
+ async close(): Promise<void> {
627
+ // The close() method steps are:
628
+
629
+ // 2. If this.[[state]] is not "opened", reject promise with an
630
+ // "InvalidStateError" DOMException and return promise.
631
+ if (this.#state !== 'opened') {
632
+ throw new DOMException('The port is not open.', 'InvalidStateError');
633
+ }
634
+
635
+ // 3. Let cancelPromise be the result of invoking cancel on this.[[readable]]
636
+ // or a promise resolved with undefined if this.[[readable]] is null.
637
+ const cancelPromise = this.#readable
638
+ ? this.#readable.cancel().catch(() => {})
639
+ : Promise.resolve();
640
+
641
+ // 4. Let abortPromise be the result of invoking abort on this.[[writable]]
642
+ // or a promise resolved with undefined if this.[[writable]] is null.
643
+ const abortPromise = this.#writable
644
+ ? this.#writable.abort().catch(() => {})
645
+ : Promise.resolve();
646
+
647
+ // 5. Let pendingClosePromise be a new promise.
648
+ const pendingClosePromise = createDeferredPromise<void>();
649
+
650
+ // 6. If this.[[readable]] and this.[[writable]] are null, resolve
651
+ // pendingClosePromise with undefined.
652
+ if (this.#readable === null && this.#writable === null) {
653
+ pendingClosePromise.resolve();
654
+ } else {
655
+ // 7. Set this.[[pendingClosePromise]] to pendingClosePromise.
656
+ this.#pendingClosePromise = pendingClosePromise;
657
+ }
658
+
659
+ // 9. Set this.[[state]] to "closing".
660
+ this.#state = 'closing';
661
+
662
+ // 8. Let combinedPromise be the result of getting a promise to wait for all
663
+ // with «cancelPromise, abortPromise, pendingClosePromise».
664
+ // 10. React to combinedPromise.
665
+ await Promise.all([
666
+ cancelPromise,
667
+ abortPromise,
668
+ pendingClosePromise.promise,
669
+ ]);
670
+
671
+ // 10.1.1. Invoke the operating system to close the serial port and release
672
+ // any associated resources.
673
+ try {
674
+ await this.#usb.close(this.#deviceId, this.#portNumber);
675
+ } catch {
676
+ // ignore
677
+ }
678
+
679
+ this.#dataSubscription?.remove();
680
+ this.#errorSubscription?.remove();
681
+ this.#dataSubscription = null;
682
+ this.#errorSubscription = null;
683
+
684
+ this.#state = 'closed'; // 10.1.2. Set this.[[state]] to "closed".
685
+ this.#readFatal = false; // 10.1.3. Set this.[[readFatal]] and this.[[writeFatal]] to false.
686
+ this.#writeFatal = false; // 10.1.3. (continued)
687
+ this.#pendingClosePromise = null; // 10.1.4. Set this.[[pendingClosePromise]] to null.
688
+
689
+ // When the port is no longer logically connected:
690
+ this.#connected = false; // 2. Set port.[[connected]] to false.
691
+ // (No port-level "disconnect" dispatch here — that event signals physical
692
+ // detach, fired by Serial from the native USB state events. See open().)
693
+ }
694
+
695
+ /**
696
+ * @see https://wicg.github.io/serial/#dom-serialport-forget
697
+ */
698
+ async forget(): Promise<void> {
699
+ // The forget() method steps are:
700
+
701
+ // 2.1. Set this.[[state]] to "forgetting".
702
+ this.#state = 'forgetting';
703
+ // 2.2. Remove this from the sequence of serial ports on the system which
704
+ // the user has allowed the site to access.
705
+ // (no persistent permissions to revoke in this implementation)
706
+ // 2.3. Set this.[[state]] to "forgotten".
707
+ this.#state = 'forgotten';
708
+ }
709
+
710
+ /**
711
+ * @see https://wicg.github.io/serial/#dom-serialport-setsignals
712
+ */
713
+ async setSignals(signals: SerialOutputSignals = {}): Promise<void> {
714
+ // The setSignals() method steps are:
715
+
716
+ // 2. If this.[[state]] is not "opened", reject promise with an
717
+ // "InvalidStateError" DOMException and return promise.
718
+ if (this.#state !== 'opened') {
719
+ throw new DOMException('The port is not open.', 'InvalidStateError');
720
+ }
721
+
722
+ // 3. If all of the specified members of signals are not present, reject
723
+ // promise with a TypeError and return promise.
724
+ if (
725
+ signals.dataTerminalReady === undefined &&
726
+ signals.requestToSend === undefined &&
727
+ signals.break === undefined
728
+ ) {
729
+ throw new TypeError('At least one signal must be specified.');
730
+ }
731
+
732
+ // Merge with current output signal state for partial updates
733
+ this.#outputSignals = {...this.#outputSignals, ...signals};
734
+
735
+ const promises: Promise<void>[] = [];
736
+
737
+ // 4.1. If signals["dataTerminalReady"] is present, invoke the operating
738
+ // system to either assert (if true) or deassert (if false) the "DTR" signal.
739
+ if (signals.dataTerminalReady !== undefined) {
740
+ promises.push(
741
+ this.#usb.setDTR(
742
+ this.#deviceId,
743
+ this.#portNumber,
744
+ signals.dataTerminalReady,
745
+ ),
746
+ );
747
+ }
748
+
749
+ // 4.2. If signals["requestToSend"] is present, invoke the operating system
750
+ // to either assert (if true) or deassert (if false) the "RTS" signal.
751
+ if (signals.requestToSend !== undefined) {
752
+ promises.push(
753
+ this.#usb.setRTS(
754
+ this.#deviceId,
755
+ this.#portNumber,
756
+ signals.requestToSend,
757
+ ),
758
+ );
759
+ }
760
+
761
+ // 4.3. If signals["break"] is present, invoke the operating system to
762
+ // either assert (if true) or deassert (if false) the "break" signal.
763
+ if (signals.break !== undefined) {
764
+ promises.push(
765
+ this.#usb.setBreak(this.#deviceId, this.#portNumber, signals.break),
766
+ );
767
+ }
768
+
769
+ try {
770
+ await Promise.all(promises);
771
+ } catch (e) {
772
+ // 4.4. If the operating system fails to change the state of any of these
773
+ // signals for any reason, reject promise with a "NetworkError" DOMException.
774
+ throw new DOMException(
775
+ `Failed to set signals: ${(e as Error).message}`,
776
+ 'NetworkError',
777
+ );
778
+ }
779
+ }
780
+
781
+ /**
782
+ * @see https://wicg.github.io/serial/#dom-serialport-getsignals
783
+ */
784
+ async getSignals(): Promise<SerialInputSignals> {
785
+ // The getSignals() method steps are:
786
+
787
+ // 2. If this.[[state]] is not "opened", reject promise with an
788
+ // "InvalidStateError" DOMException and return promise.
789
+ if (this.#state !== 'opened') {
790
+ throw new DOMException('The port is not open.', 'InvalidStateError');
791
+ }
792
+
793
+ // 3. Query the operating system for the status of the control signals that
794
+ // may be asserted by the device connected to the serial port.
795
+ try {
796
+ const [dcd, cts, ri, dsr] = await Promise.all([
797
+ this.#usb.getCD(this.#deviceId, this.#portNumber), // 3.3. Let dataCarrierDetect be true if the "DCD" signal has been asserted by the device, and false otherwise.
798
+ this.#usb.getCTS(this.#deviceId, this.#portNumber), // 3.4. Let clearToSend be true if the "CTS" signal has been asserted by the device, and false otherwise.
799
+ this.#usb.getRI(this.#deviceId, this.#portNumber), // 3.5. Let ringIndicator be true if the "RI" signal has been asserted by the device, and false otherwise.
800
+ this.#usb.getDSR(this.#deviceId, this.#portNumber), // 3.6. Let dataSetReady be true if the "DSR" signal has been asserted by the device, and false otherwise.
801
+ ]);
802
+
803
+ // 3.7. Let signals be the ordered map «[ "dataCarrierDetect" → dataCarrierDetect, "clearToSend" → clearToSend, "ringIndicator" → ringIndicator, "dataSetReady" → dataSetReady ]».
804
+ return {
805
+ dataCarrierDetect: dcd,
806
+ clearToSend: cts,
807
+ ringIndicator: ri,
808
+ dataSetReady: dsr,
809
+ };
810
+ } catch (e) {
811
+ // 3.2. If the operating system fails to determine the status of these
812
+ // signals for any reason, reject promise with a "NetworkError" DOMException
813
+ // and abort these steps.
814
+ throw new DOMException(
815
+ `Failed to get signals: ${(e as Error).message}`,
816
+ 'NetworkError',
817
+ );
818
+ }
819
+ }
820
+
821
+ /**
822
+ * @see https://wicg.github.io/serial/#dfn-handle-closing-the-readable-stream
823
+ */
824
+ #handleClosingReadableStream(): void {
825
+ // To handle closing the readable stream perform the following steps:
826
+
827
+ // 1. Set this.[[readable]] to null.
828
+ this.#readable = null;
829
+
830
+ // 2. If this.[[writable]] is null and this.[[pendingClosePromise]] is not
831
+ // null, resolve this.[[pendingClosePromise]] with undefined.
832
+ if (this.#writable === null && this.#pendingClosePromise !== null) {
833
+ this.#pendingClosePromise.resolve();
834
+ }
835
+ }
836
+
837
+ /**
838
+ * @see https://wicg.github.io/serial/#dfn-handle-closing-the-writable-stream
839
+ */
840
+ #handleClosingWritableStream(): void {
841
+ // To handle closing the writable stream perform the following steps:
842
+
843
+ // 1. Set this.[[writable]] to null.
844
+ this.#writable = null;
845
+
846
+ // 2. If this.[[readable]] is null and this.[[pendingClosePromise]] is not
847
+ // null, resolve this.[[pendingClosePromise]] with undefined.
848
+ if (this.#readable === null && this.#pendingClosePromise !== null) {
849
+ this.#pendingClosePromise.resolve();
850
+ }
851
+ }
852
+ }
853
+
854
+ /**
855
+ * @see https://wicg.github.io/serial/#serial-interface
856
+ */
857
+ export class Serial extends EventTarget {
858
+ #events = {
859
+ connect: null as ((event: Event) => void) | null,
860
+ disconnect: null as ((event: Event) => void) | null,
861
+ };
862
+
863
+ #usb: UsbSerialModule | null = null;
864
+ #knownPorts: Map<string, SerialPort> = new Map();
865
+ #initialized = false;
866
+
867
+ /**
868
+ * Lazily acquire the native USB-serial module and wire connect/disconnect
869
+ * listeners on first use (getPorts/requestPort) rather than at construction.
870
+ * Deferring native access out of the module-import path avoids touching the
871
+ * TurboModule before the runtime is ready.
872
+ */
873
+ #ensureInit(): UsbSerialModule | null {
874
+ if (this.#initialized) return this.#usb;
875
+ this.#initialized = true;
876
+ try {
877
+ this.#usb = getUsbSerial();
878
+
879
+ // A USB device was attached. Android assigns a NEW deviceId on every
880
+ // attach, so match previously-known ports of the same physical device by
881
+ // VID/PID, update their deviceId, re-key them, and fire "connect" on the
882
+ // same SerialPort instance (W3C spec model: the port is reused).
883
+ this.#usb.onConnect((event: ConnectEvent) => {
884
+ let matched = false;
885
+ for (const [key, port] of [...this.#knownPorts.entries()]) {
886
+ const internals = portInternals.get(port);
887
+ if (!internals) continue;
888
+ if (
889
+ internals.getVid() === event.usbVendorId &&
890
+ internals.getPid() === event.usbProductId
891
+ ) {
892
+ internals.setDeviceId(event.deviceId);
893
+ const newKey = this.#portKey(
894
+ event.deviceId,
895
+ internals.getPortNumber(),
896
+ );
897
+ if (newKey !== key) {
898
+ this.#knownPorts.delete(key);
899
+ this.#knownPorts.set(newKey, port);
900
+ }
901
+ port.dispatchEvent(new Event('connect'));
902
+ matched = true;
903
+ }
904
+ }
905
+ this.dispatchEvent(new Event('connect'));
906
+ // If we matched no known port this is a brand-new device; getPorts()
907
+ // will surface it on the next refresh.
908
+ void matched;
909
+ });
910
+
911
+ // A USB device was detached. Reset the matching port(s) to a closed,
912
+ // re-openable state (without touching the gone device) but KEEP the
913
+ // SerialPort instance so a re-attach can reuse it. handleDeviceLost()
914
+ // dispatches "disconnect" on the port.
915
+ this.#usb.onDisconnect((event: ConnectEvent) => {
916
+ const prefix = `${event.deviceId}:`;
917
+ for (const [key, port] of [...this.#knownPorts.entries()]) {
918
+ if (key.startsWith(prefix)) {
919
+ portInternals.get(port)?.handleDeviceLost();
920
+ }
921
+ }
922
+ this.dispatchEvent(new Event('disconnect'));
923
+ });
924
+ } catch {
925
+ this.#usb = null;
926
+ }
927
+ return this.#usb;
928
+ }
929
+ /**
930
+ * @see https://wicg.github.io/serial/#dom-serial-onconnect
931
+ */
932
+ get onconnect() {
933
+ return this.#events.connect;
934
+ }
935
+
936
+ set onconnect(fn: ((event: Event) => void) | null) {
937
+ if (this.#events.connect) {
938
+ this.removeEventListener('connect', this.#events.connect);
939
+ }
940
+ if (fn !== null) {
941
+ this.addEventListener('connect', fn);
942
+ this.#events.connect = fn;
943
+ } else {
944
+ this.#events.connect = null;
945
+ }
946
+ }
947
+
948
+ /**
949
+ * @see https://wicg.github.io/serial/#dom-serial-ondisconnect
950
+ */
951
+ get ondisconnect() {
952
+ return this.#events.disconnect;
953
+ }
954
+
955
+ set ondisconnect(fn: ((event: Event) => void) | null) {
956
+ if (this.#events.disconnect) {
957
+ this.removeEventListener('disconnect', this.#events.disconnect);
958
+ }
959
+ if (fn !== null) {
960
+ this.addEventListener('disconnect', fn);
961
+ this.#events.disconnect = fn;
962
+ } else {
963
+ this.#events.disconnect = null;
964
+ }
965
+ }
966
+
967
+ #portKey(deviceId: number, portNumber: number): string {
968
+ return `${deviceId}:${portNumber}`;
969
+ }
970
+
971
+ /**
972
+ * @see https://wicg.github.io/serial/#dom-serial-getports
973
+ */
974
+ async getPorts(): Promise<SerialPort[]> {
975
+ // The getPorts() method steps are:
976
+
977
+ const usb = this.#ensureInit();
978
+ if (!usb) return []; // Native module not available
979
+
980
+ // 3.1. Let availablePorts be the sequence of available serial ports which
981
+ // the user has allowed the site to access as the result of a previous call
982
+ // to requestPort().
983
+ const portIds = await usb.findAllDrivers();
984
+
985
+ // 3.2. Let ports be the sequence of the SerialPorts representing the ports
986
+ // in availablePorts.
987
+ const ports: SerialPort[] = [];
988
+ for (const {deviceId, portNumber, usbVendorId, usbProductId} of portIds) {
989
+ const key = this.#portKey(deviceId, portNumber);
990
+ if (!this.#knownPorts.has(key)) {
991
+ this.#knownPorts.set(
992
+ key,
993
+ new SerialPort(usb, deviceId, portNumber, usbVendorId, usbProductId),
994
+ );
995
+ }
996
+ ports.push(this.#knownPorts.get(key)!);
997
+ }
998
+
999
+ // 3.3. Resolve promise with ports.
1000
+ return ports;
1001
+ }
1002
+
1003
+ /**
1004
+ * @see https://wicg.github.io/serial/#dom-serial-requestport
1005
+ */
1006
+ async requestPort(
1007
+ options: SerialPortRequestOptions = {},
1008
+ ): Promise<SerialPort> {
1009
+ // The requestPort() method steps are:
1010
+
1011
+ const usb = this.#ensureInit();
1012
+ if (!usb) {
1013
+ throw new DOMException(
1014
+ 'NativeUsbSerial is not available.',
1015
+ 'NotFoundError',
1016
+ );
1017
+ }
1018
+
1019
+ // 4. If options["filters"] is present, then for each filter in
1020
+ // options["filters"] run the following steps:
1021
+ if (options.filters) {
1022
+ for (const filter of options.filters) {
1023
+ if (filter.bluetoothServiceClassId !== undefined) {
1024
+ // 4.1.1. If filter["usbVendorId"] is present, reject promise with a
1025
+ // TypeError and return promise.
1026
+ if (filter.usbVendorId !== undefined) {
1027
+ throw new TypeError(
1028
+ 'A filter cannot specify both bluetoothServiceClassId and usbVendorId.',
1029
+ );
1030
+ }
1031
+ // 4.1.2. If filter["usbProductId"] is present, reject promise with a
1032
+ // TypeError and return promise.
1033
+ if (filter.usbProductId !== undefined) {
1034
+ throw new TypeError(
1035
+ 'A filter cannot specify both bluetoothServiceClassId and usbProductId.',
1036
+ );
1037
+ }
1038
+ } else if (filter.usbVendorId === undefined) {
1039
+ // 4.2. If filter["usbVendorId"] is not present, reject promise with a
1040
+ // TypeError and return promise.
1041
+ throw new TypeError(
1042
+ 'A filter must specify usbVendorId if usbProductId is specified, or must not be empty.',
1043
+ );
1044
+ }
1045
+ }
1046
+ }
1047
+
1048
+ // 5.4. Prompt the user to grant the site access to a serial port —
1049
+ // shows a native Android dialog with available ports filtered by
1050
+ // options["filters"] and requests USB permission for the selected port.
1051
+ const nativeFilters = (options.filters ?? [])
1052
+ .filter(f => f.usbVendorId !== undefined)
1053
+ .map(f => ({usbVendorId: f.usbVendorId, usbProductId: f.usbProductId}));
1054
+
1055
+ let portId: PortId;
1056
+ try {
1057
+ portId = await usb.showPortPicker(nativeFilters);
1058
+ } catch {
1059
+ // 5.5. If the user does not choose a port, reject promise with a
1060
+ // "NotFoundError" DOMException and abort these steps.
1061
+ throw new DOMException('No port selected by the user.', 'NotFoundError');
1062
+ }
1063
+
1064
+ // 5.6. Let port be a SerialPort representing the port chosen by the user.
1065
+ const key = `${portId.deviceId}:${portId.portNumber}`;
1066
+ if (!this.#knownPorts.has(key)) {
1067
+ this.#knownPorts.set(
1068
+ key,
1069
+ new SerialPort(
1070
+ usb,
1071
+ portId.deviceId,
1072
+ portId.portNumber,
1073
+ portId.usbVendorId,
1074
+ portId.usbProductId,
1075
+ ),
1076
+ );
1077
+ }
1078
+
1079
+ // 5.7. Resolve promise with port.
1080
+ return this.#knownPorts.get(key)!;
1081
+ }
1082
+ }
1083
+
1084
+ export const serial = new Serial();