react-native-web-serial-api 0.0.3 → 0.1.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/README.md +23 -0
- package/TESTING.md +301 -0
- package/android/build.gradle +2 -2
- package/lib/commonjs/UsbSerial.js +58 -26
- package/lib/commonjs/UsbSerial.js.map +1 -1
- package/lib/commonjs/WebSerial.js +169 -57
- package/lib/commonjs/WebSerial.js.map +1 -1
- package/lib/commonjs/index.js +13 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/lib/dom-exception.js +176 -0
- package/lib/commonjs/lib/dom-exception.js.map +1 -0
- package/lib/commonjs/lib/event-target.js +138 -0
- package/lib/commonjs/lib/event-target.js.map +1 -0
- package/lib/commonjs/lib/promise.js +23 -0
- package/lib/commonjs/lib/promise.js.map +1 -0
- package/lib/commonjs/testing/index.js +70 -0
- package/lib/commonjs/testing/index.js.map +1 -0
- package/lib/commonjs/testing/install.js +54 -0
- package/lib/commonjs/testing/install.js.map +1 -0
- package/lib/commonjs/testing/serial-device.js +164 -0
- package/lib/commonjs/testing/serial-device.js.map +1 -0
- package/lib/commonjs/testing/virtual-serial.js +615 -0
- package/lib/commonjs/testing/virtual-serial.js.map +1 -0
- package/lib/commonjs/transport.js +61 -0
- package/lib/commonjs/transport.js.map +1 -0
- package/lib/typescript/src/UsbSerial.d.ts +24 -67
- package/lib/typescript/src/UsbSerial.d.ts.map +1 -1
- package/lib/typescript/src/WebSerial.d.ts +11 -2
- package/lib/typescript/src/WebSerial.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/lib/dom-exception.d.ts +100 -0
- package/lib/typescript/src/lib/dom-exception.d.ts.map +1 -0
- package/lib/typescript/src/lib/event-target.d.ts +53 -0
- package/lib/typescript/src/lib/event-target.d.ts.map +1 -0
- package/lib/typescript/src/lib/promise.d.ts +11 -0
- package/lib/typescript/src/lib/promise.d.ts.map +1 -0
- package/lib/typescript/src/testing/index.d.ts +23 -0
- package/lib/typescript/src/testing/index.d.ts.map +1 -0
- package/lib/typescript/src/testing/install.d.ts +25 -0
- package/lib/typescript/src/testing/install.d.ts.map +1 -0
- package/lib/typescript/src/testing/serial-device.d.ts +127 -0
- package/lib/typescript/src/testing/serial-device.d.ts.map +1 -0
- package/lib/typescript/src/testing/virtual-serial.d.ts +205 -0
- package/lib/typescript/src/testing/virtual-serial.d.ts.map +1 -0
- package/lib/typescript/src/transport.d.ts +131 -0
- package/lib/typescript/src/transport.d.ts.map +1 -0
- package/package.json +38 -2
- package/src/UsbSerial.ts +65 -90
- package/src/WebSerial.ts +227 -88
- package/src/index.ts +2 -7
- package/src/lib/dom-exception.ts +129 -60
- package/src/lib/event-target.ts +46 -21
- package/src/lib/promise.ts +7 -7
- package/src/testing/index.ts +42 -0
- package/src/testing/install.ts +65 -0
- package/src/testing/serial-device.ts +193 -0
- package/src/testing/virtual-serial.ts +801 -0
- package/src/transport.ts +200 -0
- package/babel.config.js +0 -3
- package/biome.json +0 -35
- package/example/.watchmanconfig +0 -1
- package/example/App.tsx +0 -71
- package/example/__tests__/App.test.tsx +0 -16
- package/example/__tests__/connectEvents.test.tsx +0 -81
- package/example/__tests__/getPorts.test.tsx +0 -140
- package/example/android/app/build.gradle +0 -120
- package/example/android/app/debug.keystore +0 -0
- package/example/android/app/proguard-rules.pro +0 -10
- package/example/android/app/src/debug/AndroidManifest.xml +0 -9
- package/example/android/app/src/main/AndroidManifest.xml +0 -38
- package/example/android/app/src/main/java/dev/uzlopak/MainActivity.kt +0 -22
- package/example/android/app/src/main/java/dev/uzlopak/MainApplication.kt +0 -41
- package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +0 -37
- package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/example/android/app/src/main/res/values/strings.xml +0 -3
- package/example/android/app/src/main/res/values/styles.xml +0 -9
- package/example/android/build.gradle +0 -22
- package/example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/example/android/gradle/wrapper/gradle-wrapper.properties +0 -7
- package/example/android/gradle.properties +0 -47
- package/example/android/gradlew +0 -252
- package/example/android/gradlew.bat +0 -94
- package/example/android/settings.gradle +0 -6
- package/example/app.json +0 -4
- package/example/babel.config.js +0 -21
- package/example/biome.json +0 -47
- package/example/deploy.sh +0 -11
- package/example/index.html +0 -26
- package/example/index.js +0 -9
- package/example/index.web.js +0 -8
- package/example/jest.config.js +0 -12
- package/example/metro.config.js +0 -58
- package/example/package-lock.json +0 -14510
- package/example/package.json +0 -48
- package/example/react-native.config.js +0 -17
- package/example/src/components/AppBar.tsx +0 -73
- package/example/src/components/Menu.tsx +0 -90
- package/example/src/components/SingleChoiceDialog.tsx +0 -120
- package/example/src/screens/ConnectScreen.tsx +0 -195
- package/example/src/screens/DevicesScreen.tsx +0 -252
- package/example/src/screens/TerminalScreen.tsx +0 -572
- package/example/src/settings.ts +0 -43
- package/example/src/theme.ts +0 -19
- package/example/src/util/TextUtil.ts +0 -129
- package/example/tsconfig.json +0 -10
- package/example/vite.config.mjs +0 -55
- package/scripts/deploy-release.sh +0 -127
- package/tsconfig.build.json +0 -7
- package/tsconfig.json +0 -20
package/src/WebSerial.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
WritableStream,
|
|
5
5
|
} from 'web-streams-polyfill';
|
|
6
6
|
import {DOMException} from './lib/dom-exception';
|
|
7
|
-
import {Event, EventTarget} from './lib/event-target';
|
|
7
|
+
import {Event, EventTarget, setEventParent} from './lib/event-target';
|
|
8
8
|
import {createDeferredPromise} from './lib/promise';
|
|
9
9
|
import type {
|
|
10
10
|
ConnectEvent,
|
|
@@ -12,8 +12,8 @@ import type {
|
|
|
12
12
|
ErrorEvent,
|
|
13
13
|
OpenOptions,
|
|
14
14
|
PortId,
|
|
15
|
-
|
|
16
|
-
} from './
|
|
15
|
+
SerialTransport,
|
|
16
|
+
} from './transport';
|
|
17
17
|
import {getUsbSerial} from './UsbSerial';
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -171,7 +171,7 @@ type PortInternals = {
|
|
|
171
171
|
getVid(): number | undefined;
|
|
172
172
|
getPid(): number | undefined;
|
|
173
173
|
getPortNumber(): number;
|
|
174
|
-
|
|
174
|
+
isForgotten(): boolean;
|
|
175
175
|
setDeviceId(id: number): void;
|
|
176
176
|
/** Reset to a closed, re-openable state after the device is physically lost. */
|
|
177
177
|
handleDeviceLost(): void;
|
|
@@ -187,6 +187,7 @@ const kDefaultFlowControl: FlowControlType = 'none';
|
|
|
187
187
|
const kAcceptableDataBits = [7, 8] as const;
|
|
188
188
|
const kAcceptableStopBits = [1, 2] as const;
|
|
189
189
|
const kAcceptableParity: ParityType[] = ['none', 'even', 'odd'];
|
|
190
|
+
const kAcceptableFlowControl: FlowControlType[] = ['none', 'hardware'];
|
|
190
191
|
|
|
191
192
|
function parityToNative(parity: ParityType): number {
|
|
192
193
|
switch (parity) {
|
|
@@ -199,8 +200,15 @@ function parityToNative(parity: ParityType): number {
|
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
|
|
203
|
+
const FLOW_CONTROL_NATIVE: Readonly<
|
|
204
|
+
Record<FlowControlType, 'RTS_CTS' | 'NONE'>
|
|
205
|
+
> = {
|
|
206
|
+
none: 'NONE',
|
|
207
|
+
hardware: 'RTS_CTS',
|
|
208
|
+
};
|
|
209
|
+
|
|
202
210
|
function flowControlToNative(flowControl: FlowControlType): 'RTS_CTS' | 'NONE' {
|
|
203
|
-
return flowControl
|
|
211
|
+
return FLOW_CONTROL_NATIVE[flowControl];
|
|
204
212
|
}
|
|
205
213
|
|
|
206
214
|
/**
|
|
@@ -239,7 +247,7 @@ export class SerialPort extends EventTarget {
|
|
|
239
247
|
#pendingClosePromise: ReturnType<typeof createDeferredPromise<void>> | null =
|
|
240
248
|
null; // [[pendingClosePromise]] = null
|
|
241
249
|
|
|
242
|
-
#usb:
|
|
250
|
+
#usb: SerialTransport;
|
|
243
251
|
#deviceId: number;
|
|
244
252
|
#portNumber: number;
|
|
245
253
|
#usbVendorId?: number;
|
|
@@ -255,8 +263,34 @@ export class SerialPort extends EventTarget {
|
|
|
255
263
|
#dataSubscription: {remove: () => void} | null = null;
|
|
256
264
|
#errorSubscription: {remove: () => void} | null = null;
|
|
257
265
|
|
|
266
|
+
// The live stream controllers, captured in the readable/writable getters, so
|
|
267
|
+
// a device loss can error the in-flight read/write with a "NetworkError".
|
|
268
|
+
#readableController: ReadableStreamDefaultController<Uint8Array> | null =
|
|
269
|
+
null;
|
|
270
|
+
#writableController: WritableStreamDefaultController | null = null;
|
|
271
|
+
#forgetRequested: boolean = false;
|
|
272
|
+
|
|
273
|
+
#resetToClosedState(state: 'closed' | 'forgotten' = 'closed'): void {
|
|
274
|
+
this.#dataSubscription?.remove();
|
|
275
|
+
this.#errorSubscription?.remove();
|
|
276
|
+
this.#dataSubscription = null;
|
|
277
|
+
this.#errorSubscription = null;
|
|
278
|
+
this.#readableController = null;
|
|
279
|
+
this.#writableController = null;
|
|
280
|
+
|
|
281
|
+
this.#readable = null;
|
|
282
|
+
this.#writable = null;
|
|
283
|
+
this.#readFatal = false;
|
|
284
|
+
this.#writeFatal = false;
|
|
285
|
+
this.#pendingClosePromise = null;
|
|
286
|
+
this.#bufferSize = undefined;
|
|
287
|
+
this.#state = state;
|
|
288
|
+
this.#connected = false;
|
|
289
|
+
this.#forgetRequested = state === 'forgotten';
|
|
290
|
+
}
|
|
291
|
+
|
|
258
292
|
constructor(
|
|
259
|
-
usb:
|
|
293
|
+
usb: SerialTransport,
|
|
260
294
|
deviceId: number,
|
|
261
295
|
portNumber: number,
|
|
262
296
|
usbVendorId: number,
|
|
@@ -275,7 +309,7 @@ export class SerialPort extends EventTarget {
|
|
|
275
309
|
getVid: () => this.#usbVendorId,
|
|
276
310
|
getPid: () => this.#usbProductId,
|
|
277
311
|
getPortNumber: () => this.#portNumber,
|
|
278
|
-
|
|
312
|
+
isForgotten: () => this.#state === 'forgotten',
|
|
279
313
|
setDeviceId: (id: number) => {
|
|
280
314
|
this.#deviceId = id;
|
|
281
315
|
serialPortDeviceIds.set(this, id);
|
|
@@ -291,33 +325,26 @@ export class SerialPort extends EventTarget {
|
|
|
291
325
|
* After this, a later open() (e.g. when the device is re-attached) succeeds.
|
|
292
326
|
*/
|
|
293
327
|
#handleDeviceLost(): void {
|
|
294
|
-
|
|
328
|
+
const wasForgotten = this.#state === 'forgotten';
|
|
329
|
+
|
|
330
|
+
// Reject any in-flight read/write with a "NetworkError" (per spec), erroring
|
|
331
|
+
// the stream controllers directly. We must NOT cancel()/abort() the streams:
|
|
332
|
+
// cancel() rejects on a reader-locked stream (leaving the read orphaned) and
|
|
333
|
+
// both would try to invoke the OS on the now-gone device.
|
|
334
|
+
const lost = new DOMException('The device has been lost.', 'NetworkError');
|
|
295
335
|
if (this.#readable) {
|
|
296
336
|
this.#readFatal = true;
|
|
297
337
|
try {
|
|
298
|
-
|
|
299
|
-
this.#readable.cancel().catch(() => {});
|
|
338
|
+
this.#readableController?.error(lost);
|
|
300
339
|
} catch {}
|
|
301
340
|
}
|
|
302
341
|
if (this.#writable) {
|
|
303
342
|
this.#writeFatal = true;
|
|
304
343
|
try {
|
|
305
|
-
this.#
|
|
344
|
+
this.#writableController?.error(lost);
|
|
306
345
|
} catch {}
|
|
307
346
|
}
|
|
308
|
-
this.#
|
|
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;
|
|
347
|
+
this.#resetToClosedState(wasForgotten ? 'forgotten' : 'closed');
|
|
321
348
|
|
|
322
349
|
this.dispatchEvent(new Event('disconnect'));
|
|
323
350
|
}
|
|
@@ -394,6 +421,7 @@ export class SerialPort extends EventTarget {
|
|
|
394
421
|
const stream = new ReadableStream<Uint8Array>(
|
|
395
422
|
{
|
|
396
423
|
start(controller) {
|
|
424
|
+
self.#readableController = controller;
|
|
397
425
|
self.#dataSubscription = self.#usb.onData((event: DataEvent) => {
|
|
398
426
|
if (
|
|
399
427
|
event.deviceId === deviceId &&
|
|
@@ -409,7 +437,15 @@ export class SerialPort extends EventTarget {
|
|
|
409
437
|
event.portNumber === portNumber
|
|
410
438
|
) {
|
|
411
439
|
self.#readFatal = true; // Set this.[[readFatal]] to true.
|
|
412
|
-
|
|
440
|
+
// Surface the spec error type (BreakError, BufferOverrunError,
|
|
441
|
+
// FramingError, ParityError, …) when the transport reports one;
|
|
442
|
+
// fall back to NetworkError otherwise.
|
|
443
|
+
controller.error(
|
|
444
|
+
new DOMException(
|
|
445
|
+
event.error,
|
|
446
|
+
event.errorName ?? 'NetworkError',
|
|
447
|
+
),
|
|
448
|
+
);
|
|
413
449
|
self.#handleClosingReadableStream();
|
|
414
450
|
}
|
|
415
451
|
});
|
|
@@ -456,6 +492,9 @@ export class SerialPort extends EventTarget {
|
|
|
456
492
|
|
|
457
493
|
const stream = new WritableStream<Uint8Array>(
|
|
458
494
|
{
|
|
495
|
+
start(controller) {
|
|
496
|
+
self.#writableController = controller;
|
|
497
|
+
},
|
|
459
498
|
async write(chunk) {
|
|
460
499
|
try {
|
|
461
500
|
await self.#usb.write(deviceId, portNumber, Array.from(chunk));
|
|
@@ -541,7 +580,7 @@ export class SerialPort extends EventTarget {
|
|
|
541
580
|
|
|
542
581
|
// 3. If options["baudRate"] is 0, reject promise with a TypeError and
|
|
543
582
|
// return promise.
|
|
544
|
-
if (options.baudRate
|
|
583
|
+
if (!Number.isFinite(options.baudRate) || options.baudRate <= 0) {
|
|
545
584
|
throw new TypeError('baudRate must be a positive, non-zero value.');
|
|
546
585
|
}
|
|
547
586
|
|
|
@@ -562,7 +601,7 @@ export class SerialPort extends EventTarget {
|
|
|
562
601
|
// 6. If options["bufferSize"] is 0, reject promise with a TypeError and
|
|
563
602
|
// return promise.
|
|
564
603
|
const bufferSize = options.bufferSize ?? kDefaultBufferSize;
|
|
565
|
-
if (bufferSize
|
|
604
|
+
if (!Number.isFinite(bufferSize) || bufferSize <= 0) {
|
|
566
605
|
throw new TypeError('bufferSize must be a positive, non-zero value.');
|
|
567
606
|
}
|
|
568
607
|
|
|
@@ -574,6 +613,11 @@ export class SerialPort extends EventTarget {
|
|
|
574
613
|
}
|
|
575
614
|
|
|
576
615
|
const flowControl = options.flowControl ?? kDefaultFlowControl;
|
|
616
|
+
if (!kAcceptableFlowControl.includes(flowControl)) {
|
|
617
|
+
throw new TypeError(
|
|
618
|
+
`flowControl must be one of: ${kAcceptableFlowControl.join(', ')}.`,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
577
621
|
|
|
578
622
|
// 8. Set this.[[state]] to "opening".
|
|
579
623
|
this.#state = 'opening';
|
|
@@ -599,19 +643,60 @@ export class SerialPort extends EventTarget {
|
|
|
599
643
|
);
|
|
600
644
|
}
|
|
601
645
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
this.#
|
|
605
|
-
|
|
606
|
-
|
|
646
|
+
try {
|
|
647
|
+
if (flowControl !== 'none') {
|
|
648
|
+
await this.#usb.setFlowControl(
|
|
649
|
+
this.#deviceId,
|
|
650
|
+
this.#portNumber,
|
|
651
|
+
flowControlToNative(flowControl),
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
await this.#usb.startReading(this.#deviceId, this.#portNumber);
|
|
656
|
+
} catch (e) {
|
|
657
|
+
// If post-open setup fails (flow-control/read pump), best-effort close
|
|
658
|
+
// and reset local state so a subsequent open() can succeed cleanly.
|
|
659
|
+
try {
|
|
660
|
+
await this.#usb.close(this.#deviceId, this.#portNumber);
|
|
661
|
+
} catch {
|
|
662
|
+
// ignore
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
this.#resetToClosedState(this.#forgetRequested ? 'forgotten' : 'closed');
|
|
666
|
+
|
|
667
|
+
throw new DOMException(
|
|
668
|
+
`Failed to open serial port: ${(e as Error).message}`,
|
|
669
|
+
'NetworkError',
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Guard against lifecycle races (e.g. forget() called while open() is
|
|
674
|
+
// still awaiting native setup). Do not revive a forgotten/changed state.
|
|
675
|
+
if (this.#state !== 'opening') {
|
|
676
|
+
try {
|
|
677
|
+
await this.#usb.close(this.#deviceId, this.#portNumber);
|
|
678
|
+
} catch {
|
|
679
|
+
// ignore
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (this.#state === 'forgotten' || this.#state === 'forgetting') {
|
|
683
|
+
this.#resetToClosedState('forgotten');
|
|
684
|
+
throw new DOMException(
|
|
685
|
+
'The port has been forgotten while opening.',
|
|
686
|
+
'InvalidStateError',
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
this.#resetToClosedState();
|
|
691
|
+
throw new DOMException(
|
|
692
|
+
'Failed to open serial port: state changed while opening.',
|
|
693
|
+
'NetworkError',
|
|
607
694
|
);
|
|
608
695
|
}
|
|
609
696
|
|
|
610
697
|
this.#state = 'opened'; // 9.3. Set this.[[state]] to "opened".
|
|
611
698
|
this.#bufferSize = bufferSize; // 9.4. Set this.[[bufferSize]] to options["bufferSize"].
|
|
612
699
|
|
|
613
|
-
await this.#usb.startReading(this.#deviceId, this.#portNumber);
|
|
614
|
-
|
|
615
700
|
// When the port becomes logically connected:
|
|
616
701
|
this.#connected = true; // 2. Set port.[[connected]] to true.
|
|
617
702
|
// NOTE: the port-level "connect"/"disconnect" events represent physical
|
|
@@ -676,18 +761,10 @@ export class SerialPort extends EventTarget {
|
|
|
676
761
|
// ignore
|
|
677
762
|
}
|
|
678
763
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
this.#
|
|
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.
|
|
764
|
+
// 10.1.2-10.1.4 and logical disconnect bookkeeping.
|
|
765
|
+
// If forget() was requested while close() was in flight, preserve
|
|
766
|
+
// forgotten semantics instead of reviving the port to "closed".
|
|
767
|
+
this.#resetToClosedState(this.#forgetRequested ? 'forgotten' : 'closed');
|
|
691
768
|
// (No port-level "disconnect" dispatch here — that event signals physical
|
|
692
769
|
// detach, fired by Serial from the native USB state events. See open().)
|
|
693
770
|
}
|
|
@@ -698,6 +775,14 @@ export class SerialPort extends EventTarget {
|
|
|
698
775
|
async forget(): Promise<void> {
|
|
699
776
|
// The forget() method steps are:
|
|
700
777
|
|
|
778
|
+
this.#forgetRequested = true;
|
|
779
|
+
|
|
780
|
+
// Forgetting an open port must not leave active streams/native resources
|
|
781
|
+
// behind. Close first so the instance becomes cleanly unusable afterwards.
|
|
782
|
+
if (this.#state === 'opened') {
|
|
783
|
+
await this.close();
|
|
784
|
+
}
|
|
785
|
+
|
|
701
786
|
// 2.1. Set this.[[state]] to "forgetting".
|
|
702
787
|
this.#state = 'forgetting';
|
|
703
788
|
// 2.2. Remove this from the sequence of serial ports on the system which
|
|
@@ -824,6 +909,17 @@ export class SerialPort extends EventTarget {
|
|
|
824
909
|
#handleClosingReadableStream(): void {
|
|
825
910
|
// To handle closing the readable stream perform the following steps:
|
|
826
911
|
|
|
912
|
+
// Drop the native data/error subscription tied to this readable. Without
|
|
913
|
+
// this a later readable (after cancel() + re-acquire) would coexist with the
|
|
914
|
+
// old subscription, which then enqueues into the cancelled controller and
|
|
915
|
+
// throws. close()/handleDeviceLost() also clear these; doing it here covers
|
|
916
|
+
// a standalone readable.cancel().
|
|
917
|
+
this.#dataSubscription?.remove();
|
|
918
|
+
this.#errorSubscription?.remove();
|
|
919
|
+
this.#dataSubscription = null;
|
|
920
|
+
this.#errorSubscription = null;
|
|
921
|
+
this.#readableController = null;
|
|
922
|
+
|
|
827
923
|
// 1. Set this.[[readable]] to null.
|
|
828
924
|
this.#readable = null;
|
|
829
925
|
|
|
@@ -840,6 +936,8 @@ export class SerialPort extends EventTarget {
|
|
|
840
936
|
#handleClosingWritableStream(): void {
|
|
841
937
|
// To handle closing the writable stream perform the following steps:
|
|
842
938
|
|
|
939
|
+
this.#writableController = null;
|
|
940
|
+
|
|
843
941
|
// 1. Set this.[[writable]] to null.
|
|
844
942
|
this.#writable = null;
|
|
845
943
|
|
|
@@ -860,9 +958,23 @@ export class Serial extends EventTarget {
|
|
|
860
958
|
disconnect: null as ((event: Event) => void) | null,
|
|
861
959
|
};
|
|
862
960
|
|
|
863
|
-
#usb:
|
|
961
|
+
#usb: SerialTransport | null = null;
|
|
864
962
|
#knownPorts: Map<string, SerialPort> = new Map();
|
|
865
963
|
#initialized = false;
|
|
964
|
+
#injected: SerialTransport | null;
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* @param transport Optional transport override. When provided, this `Serial`
|
|
968
|
+
* talks to it instead of the global native module — the seam used by tests
|
|
969
|
+
* and the virtual-device harness (`new Serial(new VirtualSerialTransport())`).
|
|
970
|
+
* Omit it for the normal native-backed instance; the singleton `serial`
|
|
971
|
+
* export is created this way and can still be redirected globally via
|
|
972
|
+
* `setUsbSerial()`.
|
|
973
|
+
*/
|
|
974
|
+
constructor(transport?: SerialTransport) {
|
|
975
|
+
super();
|
|
976
|
+
this.#injected = transport ?? null;
|
|
977
|
+
}
|
|
866
978
|
|
|
867
979
|
/**
|
|
868
980
|
* Subscribing to "connect"/"disconnect" must wire up the native USB state
|
|
@@ -880,42 +992,49 @@ export class Serial extends EventTarget {
|
|
|
880
992
|
* Deferring native access out of the module-import path avoids touching the
|
|
881
993
|
* TurboModule before the runtime is ready.
|
|
882
994
|
*/
|
|
883
|
-
#ensureInit():
|
|
995
|
+
#ensureInit(): SerialTransport | null {
|
|
884
996
|
if (this.#initialized) return this.#usb;
|
|
885
997
|
this.#initialized = true;
|
|
886
998
|
try {
|
|
887
|
-
this.#usb = getUsbSerial();
|
|
999
|
+
this.#usb = this.#injected ?? getUsbSerial();
|
|
888
1000
|
|
|
889
1001
|
// A USB device was attached. Android assigns a NEW deviceId on every
|
|
890
1002
|
// attach, so match previously-known ports of the same physical device by
|
|
891
1003
|
// VID/PID, update their deviceId, re-key them, and fire "connect" on the
|
|
892
1004
|
// same SerialPort instance (W3C spec model: the port is reused).
|
|
893
1005
|
this.#usb.onConnect((event: ConnectEvent) => {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
internals.
|
|
900
|
-
internals.
|
|
901
|
-
|
|
902
|
-
internals.
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1006
|
+
// Native connect events expose VID/PID but not a stable unique
|
|
1007
|
+
// identifier. To avoid mis-associating identical devices, remap only
|
|
1008
|
+
// when there is exactly one disconnected candidate.
|
|
1009
|
+
const candidates = Array.from(this.#knownPorts.entries()).filter(
|
|
1010
|
+
([, port]) => {
|
|
1011
|
+
const internals = portInternals.get(port)!;
|
|
1012
|
+
if (internals.getVid() !== event.usbVendorId) return false;
|
|
1013
|
+
if (internals.getPid() !== event.usbProductId) return false;
|
|
1014
|
+
if (internals.isForgotten()) return false;
|
|
1015
|
+
return !port.connected;
|
|
1016
|
+
},
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
if (candidates.length === 1) {
|
|
1020
|
+
const [key, port] = candidates[0];
|
|
1021
|
+
const internals = portInternals.get(port)!;
|
|
1022
|
+
internals.setDeviceId(event.deviceId);
|
|
1023
|
+
const newKey = this.#portKey(
|
|
1024
|
+
event.deviceId,
|
|
1025
|
+
internals.getPortNumber(),
|
|
1026
|
+
);
|
|
1027
|
+
this.#knownPorts.delete(key);
|
|
1028
|
+
this.#knownPorts.set(newKey, port);
|
|
1029
|
+
// Dispatched on the port; it bubbles to this Serial (event.target
|
|
1030
|
+
// stays the port, per spec — see setEventParent below).
|
|
1031
|
+
port.dispatchEvent(new Event('connect'));
|
|
1032
|
+
return;
|
|
914
1033
|
}
|
|
1034
|
+
|
|
1035
|
+
// No known port matched (or matching is ambiguous): fire a Serial-level
|
|
1036
|
+
// "connect" so listeners refresh; getPorts() surfaces the new port.
|
|
915
1037
|
this.dispatchEvent(new Event('connect'));
|
|
916
|
-
// If we matched no known port this is a brand-new device; getPorts()
|
|
917
|
-
// will surface it on the next refresh.
|
|
918
|
-
void matched;
|
|
919
1038
|
});
|
|
920
1039
|
|
|
921
1040
|
// A USB device was detached. Reset the matching port(s) to a closed,
|
|
@@ -924,12 +1043,18 @@ export class Serial extends EventTarget {
|
|
|
924
1043
|
// dispatches "disconnect" on the port.
|
|
925
1044
|
this.#usb.onDisconnect((event: ConnectEvent) => {
|
|
926
1045
|
const prefix = `${event.deviceId}:`;
|
|
1046
|
+
let matched = false;
|
|
927
1047
|
for (const [key, port] of [...this.#knownPorts.entries()]) {
|
|
928
1048
|
if (key.startsWith(prefix)) {
|
|
1049
|
+
// handleDeviceLost() dispatches "disconnect" on the port, which
|
|
1050
|
+
// bubbles here with event.target === the port (per spec).
|
|
929
1051
|
portInternals.get(port)?.handleDeviceLost();
|
|
1052
|
+
matched = true;
|
|
930
1053
|
}
|
|
931
1054
|
}
|
|
932
|
-
|
|
1055
|
+
// No known port matched (e.g. a device never opened): fire a
|
|
1056
|
+
// Serial-level "disconnect" so listeners can still refresh.
|
|
1057
|
+
if (!matched) this.dispatchEvent(new Event('disconnect'));
|
|
933
1058
|
});
|
|
934
1059
|
} catch {
|
|
935
1060
|
this.#usb = null;
|
|
@@ -1007,11 +1132,18 @@ export class Serial extends EventTarget {
|
|
|
1007
1132
|
} of portIds) {
|
|
1008
1133
|
if (!hasPermission) continue;
|
|
1009
1134
|
const key = this.#portKey(deviceId, portNumber);
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1135
|
+
const known = this.#knownPorts.get(key);
|
|
1136
|
+
if (!known || portInternals.get(known)?.isForgotten()) {
|
|
1137
|
+
const port = new SerialPort(
|
|
1138
|
+
usb,
|
|
1139
|
+
deviceId,
|
|
1140
|
+
portNumber,
|
|
1141
|
+
usbVendorId,
|
|
1142
|
+
usbProductId,
|
|
1014
1143
|
);
|
|
1144
|
+
// The port's connect/disconnect events bubble to this Serial.
|
|
1145
|
+
setEventParent(port, this);
|
|
1146
|
+
this.#knownPorts.set(key, port);
|
|
1015
1147
|
}
|
|
1016
1148
|
ports.push(this.#knownPorts.get(key)!);
|
|
1017
1149
|
}
|
|
@@ -1036,6 +1168,12 @@ export class Serial extends EventTarget {
|
|
|
1036
1168
|
);
|
|
1037
1169
|
}
|
|
1038
1170
|
|
|
1171
|
+
if ((options.allowedBluetoothServiceClassIds?.length ?? 0) > 0) {
|
|
1172
|
+
throw new TypeError(
|
|
1173
|
+
'allowedBluetoothServiceClassIds is not supported in Android USB mode.',
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1039
1177
|
// 4. If options["filters"] is present, then for each filter in
|
|
1040
1178
|
// options["filters"] run the following steps:
|
|
1041
1179
|
if (options.filters) {
|
|
@@ -1083,17 +1221,18 @@ export class Serial extends EventTarget {
|
|
|
1083
1221
|
|
|
1084
1222
|
// 5.6. Let port be a SerialPort representing the port chosen by the user.
|
|
1085
1223
|
const key = `${portId.deviceId}:${portId.portNumber}`;
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
portId.usbProductId,
|
|
1095
|
-
),
|
|
1224
|
+
const known = this.#knownPorts.get(key);
|
|
1225
|
+
if (!known || portInternals.get(known)?.isForgotten()) {
|
|
1226
|
+
const port = new SerialPort(
|
|
1227
|
+
usb,
|
|
1228
|
+
portId.deviceId,
|
|
1229
|
+
portId.portNumber,
|
|
1230
|
+
portId.usbVendorId,
|
|
1231
|
+
portId.usbProductId,
|
|
1096
1232
|
);
|
|
1233
|
+
// The port's connect/disconnect events bubble to this Serial.
|
|
1234
|
+
setEventParent(port, this);
|
|
1235
|
+
this.#knownPorts.set(key, port);
|
|
1097
1236
|
}
|
|
1098
1237
|
|
|
1099
1238
|
// 5.7. Resolve promise with port.
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// Platform-resolved Web Serial API instance.
|
|
2
|
-
// On React Native (Android) this is the USB-serial-backed polyfill; on web it
|
|
3
|
-
// is the browser's native navigator.serial. (See serial.android.ts / serial.web.ts)
|
|
4
1
|
export {default as serial} from './serial';
|
|
2
|
+
export type {SerialTransport} from './transport';
|
|
3
|
+
export {resetUsbSerial, setUsbSerial} from './UsbSerial';
|
|
5
4
|
export type {
|
|
6
5
|
SerialInputSignals,
|
|
7
6
|
SerialOptions,
|
|
@@ -13,11 +12,7 @@ export type {
|
|
|
13
12
|
// W3C Web Serial API classes
|
|
14
13
|
export {Serial, SerialPort} from './WebSerial';
|
|
15
14
|
|
|
16
|
-
// Lower-level access to the raw USB-serial TurboModule (Android only).
|
|
17
|
-
// (Imported + re-exported rather than `export * as` so older Babel presets
|
|
18
|
-
// without @babel/plugin-transform-export-namespace-from can consume the source.)
|
|
19
15
|
import * as UsbSerial from './UsbSerial';
|
|
20
16
|
|
|
21
|
-
// Web Serial API event/exception primitives used by the polyfill
|
|
22
17
|
export {Event, EventTarget} from './lib/event-target';
|
|
23
18
|
export {UsbSerial};
|