react-native-web-serial-api 0.1.0 → 0.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/README.md +188 -117
- package/TESTING.md +417 -176
- package/android/build.gradle +14 -0
- package/android/src/main/java/dev/webserialapi/NativeUsbSerialModule.java +74 -11
- package/android/src/main/java/dev/webserialapi/PortPickerActivity.java +61 -59
- package/bin/expose-serial.js +205 -0
- package/lib/commonjs/UsbSerial.js +1 -1
- package/lib/commonjs/WebSerial.js +110 -26
- package/lib/commonjs/WebSerial.js.map +1 -1
- package/lib/commonjs/index.js +2 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/lib/event-target.js +3 -1
- package/lib/commonjs/lib/event-target.js.map +1 -1
- package/lib/commonjs/lib/web-streams.js +42 -0
- package/lib/commonjs/lib/web-streams.js.map +1 -0
- package/lib/commonjs/testing/device-fixture.js +70 -0
- package/lib/commonjs/testing/device-fixture.js.map +1 -0
- package/lib/commonjs/testing/expose.js +91 -0
- package/lib/commonjs/testing/expose.js.map +1 -0
- package/lib/commonjs/testing/harness.js +98 -0
- package/lib/commonjs/testing/harness.js.map +1 -0
- package/lib/commonjs/testing/{virtual-serial.js → in-memory-serial-transport.js} +66 -28
- package/lib/commonjs/testing/in-memory-serial-transport.js.map +1 -0
- package/lib/commonjs/testing/index.js +100 -17
- package/lib/commonjs/testing/index.js.map +1 -1
- package/lib/commonjs/testing/install-in-memory-serial-transport.js +54 -0
- package/lib/commonjs/testing/install-in-memory-serial-transport.js.map +1 -0
- package/lib/commonjs/testing/serial-client.js +277 -0
- package/lib/commonjs/testing/serial-client.js.map +1 -0
- package/lib/commonjs/testing/{serial-device.js → simulated-device.js} +17 -17
- package/lib/commonjs/testing/simulated-device.js.map +1 -0
- package/lib/commonjs/testing/test-suite.js +142 -0
- package/lib/commonjs/testing/test-suite.js.map +1 -0
- package/lib/commonjs/transport.js +3 -3
- package/lib/commonjs/websocket/WebSocketSerialTransport.js +659 -0
- package/lib/commonjs/websocket/WebSocketSerialTransport.js.map +1 -0
- package/lib/commonjs/websocket/bridge.js +234 -0
- package/lib/commonjs/websocket/bridge.js.map +1 -0
- package/lib/commonjs/websocket/index.js +33 -0
- package/lib/commonjs/websocket/index.js.map +1 -0
- package/lib/commonjs/websocket/protocol.js +55 -0
- package/lib/commonjs/websocket/protocol.js.map +1 -0
- package/lib/commonjs/websocket/serial-device-bridge.js +130 -0
- package/lib/commonjs/websocket/serial-device-bridge.js.map +1 -0
- package/lib/typescript/src/UsbSerial.d.ts +1 -1
- package/lib/typescript/src/WebSerial.d.ts +7 -7
- package/lib/typescript/src/WebSerial.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/lib/event-target.d.ts +2 -0
- package/lib/typescript/src/lib/event-target.d.ts.map +1 -1
- package/lib/typescript/src/lib/web-streams.d.ts +9 -0
- package/lib/typescript/src/lib/web-streams.d.ts.map +1 -0
- package/lib/typescript/src/testing/device-fixture.d.ts +70 -0
- package/lib/typescript/src/testing/device-fixture.d.ts.map +1 -0
- package/lib/typescript/src/testing/expose.d.ts +71 -0
- package/lib/typescript/src/testing/expose.d.ts.map +1 -0
- package/lib/typescript/src/testing/harness.d.ts +34 -0
- package/lib/typescript/src/testing/harness.d.ts.map +1 -0
- package/lib/typescript/src/testing/{virtual-serial.d.ts → in-memory-serial-transport.d.ts} +37 -26
- package/lib/typescript/src/testing/in-memory-serial-transport.d.ts.map +1 -0
- package/lib/typescript/src/testing/index.d.ts +18 -8
- package/lib/typescript/src/testing/index.d.ts.map +1 -1
- package/lib/typescript/src/testing/install-in-memory-serial-transport.d.ts +25 -0
- package/lib/typescript/src/testing/install-in-memory-serial-transport.d.ts.map +1 -0
- package/lib/typescript/src/testing/serial-client.d.ts +62 -0
- package/lib/typescript/src/testing/serial-client.d.ts.map +1 -0
- package/lib/typescript/src/testing/{serial-device.d.ts → simulated-device.d.ts} +23 -23
- package/lib/typescript/src/testing/simulated-device.d.ts.map +1 -0
- package/lib/typescript/src/testing/test-suite.d.ts +75 -0
- package/lib/typescript/src/testing/test-suite.d.ts.map +1 -0
- package/lib/typescript/src/transport.d.ts +3 -3
- package/lib/typescript/src/websocket/WebSocketSerialTransport.d.ts +111 -0
- package/lib/typescript/src/websocket/WebSocketSerialTransport.d.ts.map +1 -0
- package/lib/typescript/src/websocket/bridge.d.ts +66 -0
- package/lib/typescript/src/websocket/bridge.d.ts.map +1 -0
- package/lib/typescript/src/websocket/index.d.ts +19 -0
- package/lib/typescript/src/websocket/index.d.ts.map +1 -0
- package/lib/typescript/src/websocket/protocol.d.ts +64 -0
- package/lib/typescript/src/websocket/protocol.d.ts.map +1 -0
- package/lib/typescript/src/websocket/serial-device-bridge.d.ts +32 -0
- package/lib/typescript/src/websocket/serial-device-bridge.d.ts.map +1 -0
- package/package.json +21 -3
- package/src/UsbSerial.ts +1 -1
- package/src/WebSerial.ts +134 -35
- package/src/index.ts +4 -1
- package/src/lib/event-target.ts +12 -0
- package/src/lib/web-streams.ts +43 -0
- package/src/testing/device-fixture.ts +150 -0
- package/src/testing/expose.ts +147 -0
- package/src/testing/harness.ts +124 -0
- package/src/testing/{virtual-serial.ts → in-memory-serial-transport.ts} +95 -56
- package/src/testing/index.ts +69 -21
- package/src/testing/install-in-memory-serial-transport.ts +65 -0
- package/src/testing/serial-client.ts +313 -0
- package/src/testing/{serial-device.ts → simulated-device.ts} +23 -23
- package/src/testing/test-suite.ts +186 -0
- package/src/transport.ts +3 -3
- package/src/websocket/WebSocketSerialTransport.ts +796 -0
- package/src/websocket/bridge.ts +299 -0
- package/src/websocket/index.ts +38 -0
- package/src/websocket/protocol.ts +101 -0
- package/src/websocket/serial-device-bridge.ts +160 -0
- package/lib/commonjs/testing/install.js +0 -54
- package/lib/commonjs/testing/install.js.map +0 -1
- package/lib/commonjs/testing/serial-device.js.map +0 -1
- package/lib/commonjs/testing/virtual-serial.js.map +0 -1
- package/lib/typescript/src/testing/install.d.ts +0 -25
- package/lib/typescript/src/testing/install.d.ts.map +0 -1
- package/lib/typescript/src/testing/serial-device.d.ts.map +0 -1
- package/lib/typescript/src/testing/virtual-serial.d.ts.map +0 -1
- package/src/testing/install.ts +0 -65
package/src/UsbSerial.ts
CHANGED
|
@@ -266,7 +266,7 @@ export function getUsbSerial(): SerialTransport {
|
|
|
266
266
|
|
|
267
267
|
/**
|
|
268
268
|
* Override the transport returned by {@link getUsbSerial}. Pass a
|
|
269
|
-
* {@link SerialTransport} (e.g.
|
|
269
|
+
* {@link SerialTransport} (e.g. an `InMemorySerialTransport`) to make the
|
|
270
270
|
* singleton `serial` instance — and any `new Serial()` created without an
|
|
271
271
|
* explicit transport — talk to it instead of real hardware. Pass `null` to
|
|
272
272
|
* clear the override.
|
package/src/WebSerial.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ByteLengthQueuingStrategy,
|
|
3
|
-
ReadableStream,
|
|
4
|
-
WritableStream,
|
|
5
|
-
} from 'web-streams-polyfill';
|
|
6
1
|
import {DOMException} from './lib/dom-exception';
|
|
7
2
|
import {Event, EventTarget, setEventParent} from './lib/event-target';
|
|
8
3
|
import {createDeferredPromise} from './lib/promise';
|
|
4
|
+
import {
|
|
5
|
+
ByteLengthQueuingStrategyImpl as ByteLengthQueuingStrategy,
|
|
6
|
+
ReadableStreamImpl as ReadableStream,
|
|
7
|
+
WritableStreamImpl as WritableStream,
|
|
8
|
+
} from './lib/web-streams';
|
|
9
9
|
import type {
|
|
10
10
|
ConnectEvent,
|
|
11
11
|
DataEvent,
|
|
@@ -23,14 +23,14 @@ import {getUsbSerial} from './UsbSerial';
|
|
|
23
23
|
* even - Data word plus parity bit has even parity.
|
|
24
24
|
* odd - Data word plus parity bit has odd parity.
|
|
25
25
|
*/
|
|
26
|
-
type
|
|
26
|
+
export type Parity = 'none' | 'even' | 'odd';
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* @see https://wicg.github.io/serial/#dom-flowcontroltype
|
|
30
30
|
* none - No flow control is enabled.
|
|
31
31
|
* hardware - Hardware flow control using the RTS and CTS signals is enabled.
|
|
32
32
|
*/
|
|
33
|
-
type
|
|
33
|
+
type FlowControl = 'none' | 'hardware';
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* @see https://wicg.github.io/serial/#dom-serialoptions
|
|
@@ -60,7 +60,7 @@ export type SerialOptions = {
|
|
|
60
60
|
/**
|
|
61
61
|
* The parity mode.
|
|
62
62
|
*/
|
|
63
|
-
parity?:
|
|
63
|
+
parity?: Parity;
|
|
64
64
|
/**
|
|
65
65
|
* A positive, non-zero value indicating the size of the read and write
|
|
66
66
|
* buffers that should be created.
|
|
@@ -69,7 +69,7 @@ export type SerialOptions = {
|
|
|
69
69
|
/**
|
|
70
70
|
* The flow control mode.
|
|
71
71
|
*/
|
|
72
|
-
flowControl?:
|
|
72
|
+
flowControl?: FlowControl;
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
/**
|
|
@@ -175,21 +175,23 @@ type PortInternals = {
|
|
|
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;
|
|
178
|
+
/** Reset to a closed, re-openable state after a clean physical detach. */
|
|
179
|
+
handleDeviceDetached(): void;
|
|
178
180
|
};
|
|
179
181
|
const portInternals = new WeakMap<SerialPort, PortInternals>();
|
|
180
182
|
|
|
181
183
|
const kDefaultDataBits = 8;
|
|
182
184
|
const kDefaultStopBits = 1;
|
|
183
|
-
const kDefaultParity:
|
|
185
|
+
const kDefaultParity: Parity = 'none';
|
|
184
186
|
const kDefaultBufferSize = 255;
|
|
185
|
-
const kDefaultFlowControl:
|
|
187
|
+
const kDefaultFlowControl: FlowControl = 'none';
|
|
186
188
|
|
|
187
189
|
const kAcceptableDataBits = [7, 8] as const;
|
|
188
190
|
const kAcceptableStopBits = [1, 2] as const;
|
|
189
|
-
const kAcceptableParity:
|
|
190
|
-
const kAcceptableFlowControl:
|
|
191
|
+
const kAcceptableParity: Parity[] = ['none', 'even', 'odd'];
|
|
192
|
+
const kAcceptableFlowControl: FlowControl[] = ['none', 'hardware'];
|
|
191
193
|
|
|
192
|
-
function parityToNative(parity:
|
|
194
|
+
function parityToNative(parity: Parity): number {
|
|
193
195
|
switch (parity) {
|
|
194
196
|
case 'odd':
|
|
195
197
|
return 1;
|
|
@@ -200,17 +202,34 @@ function parityToNative(parity: ParityType): number {
|
|
|
200
202
|
}
|
|
201
203
|
}
|
|
202
204
|
|
|
203
|
-
const FLOW_CONTROL_NATIVE: Readonly<
|
|
204
|
-
Record<FlowControlType, 'RTS_CTS' | 'NONE'>
|
|
205
|
-
> = {
|
|
205
|
+
const FLOW_CONTROL_NATIVE: Readonly<Record<FlowControl, 'RTS_CTS' | 'NONE'>> = {
|
|
206
206
|
none: 'NONE',
|
|
207
207
|
hardware: 'RTS_CTS',
|
|
208
208
|
};
|
|
209
209
|
|
|
210
|
-
function flowControlToNative(flowControl:
|
|
210
|
+
function flowControlToNative(flowControl: FlowControl): 'RTS_CTS' | 'NONE' {
|
|
211
211
|
return FLOW_CONTROL_NATIVE[flowControl];
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Normalise a writable chunk to a plain byte array. The Web Serial writable
|
|
216
|
+
* accepts any BufferSource — a bare `ArrayBuffer` or any `ArrayBufferView`
|
|
217
|
+
* (`DataView`, typed arrays, including views with a non-zero `byteOffset`). A
|
|
218
|
+
* naive `Array.from(chunk)` only works for the iterable typed arrays and
|
|
219
|
+
* silently yields `[]` for an `ArrayBuffer`/`DataView`, so handle each shape.
|
|
220
|
+
*/
|
|
221
|
+
function bufferSourceToBytes(chunk: ArrayBufferView | ArrayBuffer): number[] {
|
|
222
|
+
if (chunk instanceof Uint8Array) {
|
|
223
|
+
return Array.from(chunk);
|
|
224
|
+
}
|
|
225
|
+
if (ArrayBuffer.isView(chunk)) {
|
|
226
|
+
return Array.from(
|
|
227
|
+
new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return Array.from(new Uint8Array(chunk as ArrayBuffer));
|
|
231
|
+
}
|
|
232
|
+
|
|
214
233
|
/**
|
|
215
234
|
* Methods on this interface typically complete asynchronously, queuing work on
|
|
216
235
|
* the serial port task source.
|
|
@@ -252,13 +271,8 @@ export class SerialPort extends EventTarget {
|
|
|
252
271
|
#portNumber: number;
|
|
253
272
|
#usbVendorId?: number;
|
|
254
273
|
#usbProductId?: number;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
#outputSignals: Required<SerialOutputSignals> = {
|
|
258
|
-
dataTerminalReady: false,
|
|
259
|
-
requestToSend: false,
|
|
260
|
-
break: false,
|
|
261
|
-
};
|
|
274
|
+
// Baud rate of the current open(), used to size the native write timeout.
|
|
275
|
+
#baudRate: number = 0;
|
|
262
276
|
|
|
263
277
|
#dataSubscription: {remove: () => void} | null = null;
|
|
264
278
|
#errorSubscription: {remove: () => void} | null = null;
|
|
@@ -284,11 +298,26 @@ export class SerialPort extends EventTarget {
|
|
|
284
298
|
this.#writeFatal = false;
|
|
285
299
|
this.#pendingClosePromise = null;
|
|
286
300
|
this.#bufferSize = undefined;
|
|
301
|
+
this.#baudRate = 0;
|
|
287
302
|
this.#state = state;
|
|
288
303
|
this.#connected = false;
|
|
289
304
|
this.#forgetRequested = state === 'forgotten';
|
|
290
305
|
}
|
|
291
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Compute a native write timeout (ms) large enough to actually drain `length`
|
|
309
|
+
* bytes at the negotiated baud rate. A fixed 2 s timeout spuriously fails
|
|
310
|
+
* large writes on slow links; this scales with the payload while keeping a 2 s
|
|
311
|
+
* floor. ~10 bits per byte (start + 8 data + stop) with a 2× safety margin.
|
|
312
|
+
*/
|
|
313
|
+
#writeTimeoutFor(length: number): number {
|
|
314
|
+
const kMinTimeoutMs = 2000;
|
|
315
|
+
/* istanbul ignore next — baudRate is always > 0 when the port is open */
|
|
316
|
+
const baud = this.#baudRate > 0 ? this.#baudRate : 9600;
|
|
317
|
+
const estimatedMs = Math.ceil((length * 10 * 1000) / baud) * 2;
|
|
318
|
+
return Math.max(kMinTimeoutMs, estimatedMs);
|
|
319
|
+
}
|
|
320
|
+
|
|
292
321
|
constructor(
|
|
293
322
|
usb: SerialTransport,
|
|
294
323
|
deviceId: number,
|
|
@@ -315,6 +344,7 @@ export class SerialPort extends EventTarget {
|
|
|
315
344
|
serialPortDeviceIds.set(this, id);
|
|
316
345
|
},
|
|
317
346
|
handleDeviceLost: () => this.#handleDeviceLost(),
|
|
347
|
+
handleDeviceDetached: () => this.#handleDeviceDetached(),
|
|
318
348
|
});
|
|
319
349
|
}
|
|
320
350
|
|
|
@@ -349,6 +379,53 @@ export class SerialPort extends EventTarget {
|
|
|
349
379
|
this.dispatchEvent(new Event('disconnect'));
|
|
350
380
|
}
|
|
351
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Reset this port to a closed, re-openable state after a clean physical
|
|
384
|
+
* detach. Unlike device loss, buffered readable bytes should still be
|
|
385
|
+
* observable before the stream ends.
|
|
386
|
+
*/
|
|
387
|
+
#handleDeviceDetached(): void {
|
|
388
|
+
const wasForgotten = this.#state === 'forgotten';
|
|
389
|
+
const readable = this.#readable;
|
|
390
|
+
const writable = this.#writable;
|
|
391
|
+
|
|
392
|
+
// Keep the public event and state transition synchronous so existing
|
|
393
|
+
// callers observe the detach immediately, but defer the stream teardown by
|
|
394
|
+
// one microtask to let already-queued data drain first.
|
|
395
|
+
this.#state = wasForgotten ? 'forgotten' : 'closed';
|
|
396
|
+
this.#connected = false;
|
|
397
|
+
this.#readable = null;
|
|
398
|
+
this.#writable = null;
|
|
399
|
+
this.dispatchEvent(new Event('disconnect'));
|
|
400
|
+
|
|
401
|
+
queueMicrotask(() => {
|
|
402
|
+
// Close the readable stream so any bytes already queued can still be
|
|
403
|
+
// drained by the consumer before EOF.
|
|
404
|
+
try {
|
|
405
|
+
if (readable) this.#readableController?.close();
|
|
406
|
+
} catch {}
|
|
407
|
+
|
|
408
|
+
// A detached device can no longer accept writes, so reject any writable
|
|
409
|
+
// traffic and clear local state.
|
|
410
|
+
const lost = new DOMException(
|
|
411
|
+
'The device has been lost.',
|
|
412
|
+
'NetworkError',
|
|
413
|
+
);
|
|
414
|
+
if (writable) {
|
|
415
|
+
this.#writeFatal = true;
|
|
416
|
+
try {
|
|
417
|
+
this.#writableController?.error(lost);
|
|
418
|
+
} catch {}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Mirror the stream-closing bookkeeping without treating this as a hard
|
|
422
|
+
// read fatal error.
|
|
423
|
+
this.#handleClosingReadableStream();
|
|
424
|
+
this.#handleClosingWritableStream();
|
|
425
|
+
this.#resetToClosedState(wasForgotten ? 'forgotten' : 'closed');
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
352
429
|
/**
|
|
353
430
|
* @see https://wicg.github.io/serial/#dom-serialport-onconnect
|
|
354
431
|
*/
|
|
@@ -427,7 +504,15 @@ export class SerialPort extends EventTarget {
|
|
|
427
504
|
event.deviceId === deviceId &&
|
|
428
505
|
event.portNumber === portNumber
|
|
429
506
|
) {
|
|
430
|
-
|
|
507
|
+
try {
|
|
508
|
+
controller.enqueue(new Uint8Array(event.data));
|
|
509
|
+
} catch {
|
|
510
|
+
// The stream may already be closing/closed — e.g. a data event
|
|
511
|
+
// racing cancel()/close(), where the stream is closed but this
|
|
512
|
+
// subscription is not yet torn down. Dropping the chunk is
|
|
513
|
+
// correct (the consumer has stopped reading) and avoids throwing
|
|
514
|
+
// out of the transport's event-dispatch loop.
|
|
515
|
+
}
|
|
431
516
|
}
|
|
432
517
|
});
|
|
433
518
|
|
|
@@ -496,8 +581,14 @@ export class SerialPort extends EventTarget {
|
|
|
496
581
|
self.#writableController = controller;
|
|
497
582
|
},
|
|
498
583
|
async write(chunk) {
|
|
584
|
+
const bytes = bufferSourceToBytes(chunk);
|
|
499
585
|
try {
|
|
500
|
-
await self.#usb.write(
|
|
586
|
+
await self.#usb.write(
|
|
587
|
+
deviceId,
|
|
588
|
+
portNumber,
|
|
589
|
+
bytes,
|
|
590
|
+
self.#writeTimeoutFor(bytes.length),
|
|
591
|
+
);
|
|
501
592
|
} catch (e) {
|
|
502
593
|
// If the port was disconnected, set this.[[writeFatal]] to true.
|
|
503
594
|
self.#writeFatal = true;
|
|
@@ -696,6 +787,7 @@ export class SerialPort extends EventTarget {
|
|
|
696
787
|
|
|
697
788
|
this.#state = 'opened'; // 9.3. Set this.[[state]] to "opened".
|
|
698
789
|
this.#bufferSize = bufferSize; // 9.4. Set this.[[bufferSize]] to options["bufferSize"].
|
|
790
|
+
this.#baudRate = options.baudRate;
|
|
699
791
|
|
|
700
792
|
// When the port becomes logically connected:
|
|
701
793
|
this.#connected = true; // 2. Set port.[[connected]] to true.
|
|
@@ -703,6 +795,13 @@ export class SerialPort extends EventTarget {
|
|
|
703
795
|
// device presence (attach/detach), dispatched by Serial from the native
|
|
704
796
|
// USB state events — not logical open()/close(). So we do not dispatch them
|
|
705
797
|
// here; that also avoids re-entrancy with auto-reconnect listeners.
|
|
798
|
+
|
|
799
|
+
// Materialise the readable eagerly so its data subscription is live the
|
|
800
|
+
// instant the native read pump (startReading, above) begins emitting. The
|
|
801
|
+
// device may transmit immediately on open; without an active subscription
|
|
802
|
+
// those first bytes would be delivered to no listener and silently lost in
|
|
803
|
+
// the window between open() resolving and the first port.readable access.
|
|
804
|
+
void this.readable;
|
|
706
805
|
}
|
|
707
806
|
|
|
708
807
|
/**
|
|
@@ -814,9 +913,6 @@ export class SerialPort extends EventTarget {
|
|
|
814
913
|
throw new TypeError('At least one signal must be specified.');
|
|
815
914
|
}
|
|
816
915
|
|
|
817
|
-
// Merge with current output signal state for partial updates
|
|
818
|
-
this.#outputSignals = {...this.#outputSignals, ...signals};
|
|
819
|
-
|
|
820
916
|
const promises: Promise<void>[] = [];
|
|
821
917
|
|
|
822
918
|
// 4.1. If signals["dataTerminalReady"] is present, invoke the operating
|
|
@@ -965,8 +1061,8 @@ export class Serial extends EventTarget {
|
|
|
965
1061
|
|
|
966
1062
|
/**
|
|
967
1063
|
* @param transport Optional transport override. When provided, this `Serial`
|
|
968
|
-
* talks to it instead of the global native module — the
|
|
969
|
-
* and the virtual
|
|
1064
|
+
* talks to it instead of the global native module — the transport override used by tests
|
|
1065
|
+
* and the virtual serial device harness (`new Serial(new InMemorySerialTransport())`).
|
|
970
1066
|
* Omit it for the normal native-backed instance; the singleton `serial`
|
|
971
1067
|
* export is created this way and can still be redirected globally via
|
|
972
1068
|
* `setUsbSerial()`.
|
|
@@ -1042,13 +1138,16 @@ export class Serial extends EventTarget {
|
|
|
1042
1138
|
// SerialPort instance so a re-attach can reuse it. handleDeviceLost()
|
|
1043
1139
|
// dispatches "disconnect" on the port.
|
|
1044
1140
|
this.#usb.onDisconnect((event: ConnectEvent) => {
|
|
1141
|
+
const wasLost = (event as ConnectEvent & {lost?: boolean}).lost ?? true;
|
|
1045
1142
|
const prefix = `${event.deviceId}:`;
|
|
1046
1143
|
let matched = false;
|
|
1047
1144
|
for (const [key, port] of [...this.#knownPorts.entries()]) {
|
|
1048
1145
|
if (key.startsWith(prefix)) {
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
portInternals.get(port)
|
|
1146
|
+
// Physical loss tears down the stream immediately; a clean detach
|
|
1147
|
+
// lets any already-buffered bytes drain before EOF.
|
|
1148
|
+
const internals = portInternals.get(port);
|
|
1149
|
+
if (wasLost) internals?.handleDeviceLost();
|
|
1150
|
+
else internals?.handleDeviceDetached();
|
|
1052
1151
|
matched = true;
|
|
1053
1152
|
}
|
|
1054
1153
|
}
|
package/src/index.ts
CHANGED
|
@@ -14,5 +14,8 @@ export {Serial, SerialPort} from './WebSerial';
|
|
|
14
14
|
|
|
15
15
|
import * as UsbSerial from './UsbSerial';
|
|
16
16
|
|
|
17
|
-
export {
|
|
17
|
+
export {
|
|
18
|
+
EventImpl as Event,
|
|
19
|
+
EventTargetImpl as EventTarget,
|
|
20
|
+
} from './lib/event-target';
|
|
18
21
|
export {UsbSerial};
|
package/src/lib/event-target.ts
CHANGED
|
@@ -193,3 +193,15 @@ export class EventTarget {
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
type MaybeGlobal = Record<string, unknown>;
|
|
198
|
+
|
|
199
|
+
export const EventImpl =
|
|
200
|
+
typeof (globalThis as MaybeGlobal).Event === 'function'
|
|
201
|
+
? ((globalThis as MaybeGlobal).Event as typeof Event)
|
|
202
|
+
: Event;
|
|
203
|
+
|
|
204
|
+
export const EventTargetImpl =
|
|
205
|
+
typeof (globalThis as MaybeGlobal).EventTarget === 'function'
|
|
206
|
+
? ((globalThis as MaybeGlobal).EventTarget as typeof EventTarget)
|
|
207
|
+
: EventTarget;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ByteLengthQueuingStrategy as ByteLengthQueuingStrategyPolyfill,
|
|
3
|
+
ReadableStream as ReadableStreamPolyfill,
|
|
4
|
+
WritableStream as WritableStreamPolyfill,
|
|
5
|
+
} from 'web-streams-polyfill';
|
|
6
|
+
|
|
7
|
+
type MaybeGlobal = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
function getCtor<T>(name: string, fallback: T): T {
|
|
10
|
+
const value = (globalThis as MaybeGlobal)[name];
|
|
11
|
+
return typeof value === 'function' ? (value as T) : fallback;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
ByteLengthQueuingStrategyPolyfill,
|
|
16
|
+
ReadableStreamPolyfill,
|
|
17
|
+
WritableStreamPolyfill,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const ReadableStreamImpl = getCtor(
|
|
21
|
+
'ReadableStream',
|
|
22
|
+
ReadableStreamPolyfill,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const WritableStreamImpl = getCtor(
|
|
26
|
+
'WritableStream',
|
|
27
|
+
WritableStreamPolyfill,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const ByteLengthQueuingStrategyImpl = getCtor(
|
|
31
|
+
'ByteLengthQueuingStrategy',
|
|
32
|
+
ByteLengthQueuingStrategyPolyfill,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Companion TYPE aliases so that `Impl as X` imports carry a type too. Without
|
|
36
|
+
// these, an annotation like `ReadableStream<Uint8Array>` (where `ReadableStream`
|
|
37
|
+
// is the imported value) falls back to the ambient *global* ReadableStream and
|
|
38
|
+
// no longer matches the polyfill instances actually constructed — the runtime
|
|
39
|
+
// pick stays native-or-polyfill; only the static type is anchored to the
|
|
40
|
+
// (structurally faithful) polyfill instance type.
|
|
41
|
+
export type ReadableStreamImpl<R = unknown> = ReadableStreamPolyfill<R>;
|
|
42
|
+
export type WritableStreamImpl<W = unknown> = WritableStreamPolyfill<W>;
|
|
43
|
+
export type ByteLengthQueuingStrategyImpl = ByteLengthQueuingStrategyPolyfill;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createDeviceFixture` — the one-call fixture for testing serial code. It wires a
|
|
3
|
+
* {@link SimulatedDevice} simulator to an {@link InMemorySerialTransport} + `Serial`,
|
|
4
|
+
* and hands back everything a test needs to drive *both* sides:
|
|
5
|
+
*
|
|
6
|
+
* - `serial`/`port` — what the app-under-test consumes,
|
|
7
|
+
* - `simulatedDevice` (typed) + `device` handle — drive the device (inject data,
|
|
8
|
+
* move the GPS, inject faults),
|
|
9
|
+
* - `client` — a host-side {@link SerialClient} (for protocol tests), and
|
|
10
|
+
* - `whenOpened()/whenClosed()` — `await` the app connecting.
|
|
11
|
+
*
|
|
12
|
+
* @example Test how your app reacts to a device event
|
|
13
|
+
* const {simulatedDevice, device, whenOpened} = await createDeviceFixture(new MyGps());
|
|
14
|
+
* renderMyApp(); // opens the port itself
|
|
15
|
+
* await whenOpened();
|
|
16
|
+
* simulatedDevice.update({latitude: 51.48, longitude: 0});
|
|
17
|
+
* // …assert your app's UI updated
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {setUsbSerial} from '../UsbSerial';
|
|
21
|
+
import type {SerialPort} from '../WebSerial';
|
|
22
|
+
import {Serial} from '../WebSerial';
|
|
23
|
+
import type {
|
|
24
|
+
DeviceOptions,
|
|
25
|
+
InMemorySerialTransportOptions,
|
|
26
|
+
} from './in-memory-serial-transport';
|
|
27
|
+
import {
|
|
28
|
+
type DeviceHandle,
|
|
29
|
+
InMemorySerialTransport,
|
|
30
|
+
} from './in-memory-serial-transport';
|
|
31
|
+
import {SerialClient} from './serial-client';
|
|
32
|
+
import type {
|
|
33
|
+
SimulatedDevice,
|
|
34
|
+
SimulatedDeviceOpenOptions,
|
|
35
|
+
} from './simulated-device';
|
|
36
|
+
|
|
37
|
+
export type DeviceFixtureOptions = {
|
|
38
|
+
/** Per-device transport knobs (single-device form). */
|
|
39
|
+
device?: DeviceOptions;
|
|
40
|
+
/** Per-device transport knobs, by index (multi-device form). */
|
|
41
|
+
devices?: DeviceOptions[];
|
|
42
|
+
/** Transport-level options (latencyMs, chunkSize, autoGrantPermission). */
|
|
43
|
+
transport?: InMemorySerialTransportOptions;
|
|
44
|
+
/** Whether each device is already permitted. Defaults to true (so it lists). */
|
|
45
|
+
hasPermission?: boolean;
|
|
46
|
+
/** Also `setUsbSerial(transport)` so a running app's `serial` sees it. Default false. */
|
|
47
|
+
installGlobally?: boolean;
|
|
48
|
+
/** Options for the host-side {@link SerialClient}. */
|
|
49
|
+
client?: {defaultTimeoutMs?: number};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type MountedDeviceFixture<D extends SimulatedDevice = SimulatedDevice> =
|
|
53
|
+
{
|
|
54
|
+
transport: InMemorySerialTransport;
|
|
55
|
+
serial: Serial;
|
|
56
|
+
port: SerialPort;
|
|
57
|
+
/** The transport-side handle: push/emitError/failNext/written/attach/detach/… */
|
|
58
|
+
device: DeviceHandle;
|
|
59
|
+
/** The concrete device simulator, typed (e.g. call `gps.update(...)`). */
|
|
60
|
+
simulatedDevice: D;
|
|
61
|
+
/** A host-side client bound to `port` — NOT opened (call `client.open()`). */
|
|
62
|
+
client: SerialClient;
|
|
63
|
+
/** Resolve when the app opens the port (now if already open). */
|
|
64
|
+
whenOpened(): Promise<SimulatedDeviceOpenOptions>;
|
|
65
|
+
/** Resolve when the app closes the port (now if not open). */
|
|
66
|
+
whenClosed(): Promise<void>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type MountedDeviceFixtures = {
|
|
70
|
+
transport: InMemorySerialTransport;
|
|
71
|
+
serial: Serial;
|
|
72
|
+
ports: SerialPort[];
|
|
73
|
+
devices: DeviceHandle[];
|
|
74
|
+
simulatedDevices: SimulatedDevice[];
|
|
75
|
+
clients: SerialClient[];
|
|
76
|
+
/** Resolve when device `index` opens, or any device when `index` is omitted. */
|
|
77
|
+
whenOpened(index?: number): Promise<SimulatedDeviceOpenOptions>;
|
|
78
|
+
/** Resolve when device `index` closes, or any device when `index` is omitted. */
|
|
79
|
+
whenClosed(index?: number): Promise<void>;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function createDeviceFixture<D extends SimulatedDevice>(
|
|
83
|
+
device: D,
|
|
84
|
+
options?: DeviceFixtureOptions,
|
|
85
|
+
): Promise<MountedDeviceFixture<D>>;
|
|
86
|
+
export function createDeviceFixture(
|
|
87
|
+
devices: SimulatedDevice[],
|
|
88
|
+
options?: DeviceFixtureOptions,
|
|
89
|
+
): Promise<MountedDeviceFixtures>;
|
|
90
|
+
export async function createDeviceFixture(
|
|
91
|
+
deviceOrDevices: SimulatedDevice | SimulatedDevice[],
|
|
92
|
+
options: DeviceFixtureOptions = {},
|
|
93
|
+
): Promise<MountedDeviceFixture | MountedDeviceFixtures> {
|
|
94
|
+
const list = Array.isArray(deviceOrDevices)
|
|
95
|
+
? deviceOrDevices
|
|
96
|
+
: [deviceOrDevices];
|
|
97
|
+
const hasPermission = options.hasPermission ?? true;
|
|
98
|
+
const transport = new InMemorySerialTransport(options.transport);
|
|
99
|
+
const handles = list.map((dev, i) =>
|
|
100
|
+
transport.addDevice(dev, {
|
|
101
|
+
hasPermission,
|
|
102
|
+
...(Array.isArray(deviceOrDevices)
|
|
103
|
+
? options.devices?.[i]
|
|
104
|
+
: options.device),
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
const serial = new Serial(transport);
|
|
108
|
+
if (options.installGlobally) setUsbSerial(transport);
|
|
109
|
+
|
|
110
|
+
const ports = await serial.getPorts();
|
|
111
|
+
const clients = ports.map(
|
|
112
|
+
p =>
|
|
113
|
+
new SerialClient(p, {
|
|
114
|
+
defaultTimeoutMs: options.client?.defaultTimeoutMs,
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (!Array.isArray(deviceOrDevices)) {
|
|
119
|
+
const handle = handles[0];
|
|
120
|
+
const port = ports[0];
|
|
121
|
+
if (!port) throw new Error('createDeviceFixture: device did not enumerate');
|
|
122
|
+
return {
|
|
123
|
+
transport,
|
|
124
|
+
serial,
|
|
125
|
+
port,
|
|
126
|
+
device: handle,
|
|
127
|
+
simulatedDevice: deviceOrDevices,
|
|
128
|
+
client: clients[0],
|
|
129
|
+
whenOpened: () => handle.whenOpened(),
|
|
130
|
+
whenClosed: () => handle.whenClosed(),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
transport,
|
|
136
|
+
serial,
|
|
137
|
+
ports,
|
|
138
|
+
devices: handles,
|
|
139
|
+
simulatedDevices: list,
|
|
140
|
+
clients,
|
|
141
|
+
whenOpened: (index?: number) =>
|
|
142
|
+
index === undefined
|
|
143
|
+
? Promise.race(handles.map(h => h.whenOpened()))
|
|
144
|
+
: handles[index].whenOpened(),
|
|
145
|
+
whenClosed: (index?: number) =>
|
|
146
|
+
index === undefined
|
|
147
|
+
? Promise.race(handles.map(h => h.whenClosed()))
|
|
148
|
+
: handles[index].whenClosed(),
|
|
149
|
+
};
|
|
150
|
+
}
|