react-native-web-serial-api 0.0.3 → 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 +198 -104
- package/TESTING.md +542 -0
- package/android/build.gradle +16 -2
- 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 +58 -26
- package/lib/commonjs/UsbSerial.js.map +1 -1
- package/lib/commonjs/WebSerial.js +273 -77
- package/lib/commonjs/WebSerial.js.map +1 -1
- package/lib/commonjs/index.js +15 -3
- 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 +140 -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/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/in-memory-serial-transport.js +653 -0
- package/lib/commonjs/testing/in-memory-serial-transport.js.map +1 -0
- package/lib/commonjs/testing/index.js +153 -0
- package/lib/commonjs/testing/index.js.map +1 -0
- 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/simulated-device.js +164 -0
- 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 +61 -0
- package/lib/commonjs/transport.js.map +1 -0
- 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 +24 -67
- package/lib/typescript/src/UsbSerial.d.ts.map +1 -1
- package/lib/typescript/src/WebSerial.d.ts +16 -7
- package/lib/typescript/src/WebSerial.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -1
- 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 +55 -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/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/in-memory-serial-transport.d.ts +216 -0
- package/lib/typescript/src/testing/in-memory-serial-transport.d.ts.map +1 -0
- package/lib/typescript/src/testing/index.d.ts +33 -0
- package/lib/typescript/src/testing/index.d.ts.map +1 -0
- 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/simulated-device.d.ts +127 -0
- 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 +131 -0
- package/lib/typescript/src/transport.d.ts.map +1 -0
- 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 +57 -3
- package/src/UsbSerial.ts +65 -90
- package/src/WebSerial.ts +351 -113
- package/src/index.ts +6 -8
- package/src/lib/dom-exception.ts +129 -60
- package/src/lib/event-target.ts +58 -21
- package/src/lib/promise.ts +7 -7
- 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/in-memory-serial-transport.ts +840 -0
- package/src/testing/index.ts +90 -0
- package/src/testing/install-in-memory-serial-transport.ts +65 -0
- package/src/testing/serial-client.ts +313 -0
- package/src/testing/simulated-device.ts +193 -0
- package/src/testing/test-suite.ts +186 -0
- package/src/transport.ts +200 -0
- 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/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
|
@@ -4,10 +4,10 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.serial = exports.SerialPort = exports.Serial = void 0;
|
|
7
|
-
var _webStreamsPolyfill = require("web-streams-polyfill");
|
|
8
7
|
var _domException = require("./lib/dom-exception");
|
|
9
8
|
var _eventTarget = require("./lib/event-target");
|
|
10
9
|
var _promise = require("./lib/promise");
|
|
10
|
+
var _webStreams = require("./lib/web-streams");
|
|
11
11
|
var _UsbSerial = require("./UsbSerial");
|
|
12
12
|
/**
|
|
13
13
|
* @see https://wicg.github.io/serial/#dom-paritytype
|
|
@@ -65,6 +65,7 @@ const kDefaultFlowControl = 'none';
|
|
|
65
65
|
const kAcceptableDataBits = [7, 8];
|
|
66
66
|
const kAcceptableStopBits = [1, 2];
|
|
67
67
|
const kAcceptableParity = ['none', 'even', 'odd'];
|
|
68
|
+
const kAcceptableFlowControl = ['none', 'hardware'];
|
|
68
69
|
function parityToNative(parity) {
|
|
69
70
|
switch (parity) {
|
|
70
71
|
case 'odd':
|
|
@@ -75,8 +76,29 @@ function parityToNative(parity) {
|
|
|
75
76
|
return 0;
|
|
76
77
|
}
|
|
77
78
|
}
|
|
79
|
+
const FLOW_CONTROL_NATIVE = {
|
|
80
|
+
none: 'NONE',
|
|
81
|
+
hardware: 'RTS_CTS'
|
|
82
|
+
};
|
|
78
83
|
function flowControlToNative(flowControl) {
|
|
79
|
-
return flowControl
|
|
84
|
+
return FLOW_CONTROL_NATIVE[flowControl];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Normalise a writable chunk to a plain byte array. The Web Serial writable
|
|
89
|
+
* accepts any BufferSource — a bare `ArrayBuffer` or any `ArrayBufferView`
|
|
90
|
+
* (`DataView`, typed arrays, including views with a non-zero `byteOffset`). A
|
|
91
|
+
* naive `Array.from(chunk)` only works for the iterable typed arrays and
|
|
92
|
+
* silently yields `[]` for an `ArrayBuffer`/`DataView`, so handle each shape.
|
|
93
|
+
*/
|
|
94
|
+
function bufferSourceToBytes(chunk) {
|
|
95
|
+
if (chunk instanceof Uint8Array) {
|
|
96
|
+
return Array.from(chunk);
|
|
97
|
+
}
|
|
98
|
+
if (ArrayBuffer.isView(chunk)) {
|
|
99
|
+
return Array.from(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
|
|
100
|
+
}
|
|
101
|
+
return Array.from(new Uint8Array(chunk));
|
|
80
102
|
}
|
|
81
103
|
|
|
82
104
|
/**
|
|
@@ -113,15 +135,48 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
113
135
|
#portNumber;
|
|
114
136
|
#usbVendorId;
|
|
115
137
|
#usbProductId;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
#outputSignals = {
|
|
119
|
-
dataTerminalReady: false,
|
|
120
|
-
requestToSend: false,
|
|
121
|
-
break: false
|
|
122
|
-
};
|
|
138
|
+
// Baud rate of the current open(), used to size the native write timeout.
|
|
139
|
+
#baudRate = 0;
|
|
123
140
|
#dataSubscription = null;
|
|
124
141
|
#errorSubscription = null;
|
|
142
|
+
|
|
143
|
+
// The live stream controllers, captured in the readable/writable getters, so
|
|
144
|
+
// a device loss can error the in-flight read/write with a "NetworkError".
|
|
145
|
+
#readableController = null;
|
|
146
|
+
#writableController = null;
|
|
147
|
+
#forgetRequested = false;
|
|
148
|
+
#resetToClosedState(state = 'closed') {
|
|
149
|
+
this.#dataSubscription?.remove();
|
|
150
|
+
this.#errorSubscription?.remove();
|
|
151
|
+
this.#dataSubscription = null;
|
|
152
|
+
this.#errorSubscription = null;
|
|
153
|
+
this.#readableController = null;
|
|
154
|
+
this.#writableController = null;
|
|
155
|
+
this.#readable = null;
|
|
156
|
+
this.#writable = null;
|
|
157
|
+
this.#readFatal = false;
|
|
158
|
+
this.#writeFatal = false;
|
|
159
|
+
this.#pendingClosePromise = null;
|
|
160
|
+
this.#bufferSize = undefined;
|
|
161
|
+
this.#baudRate = 0;
|
|
162
|
+
this.#state = state;
|
|
163
|
+
this.#connected = false;
|
|
164
|
+
this.#forgetRequested = state === 'forgotten';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Compute a native write timeout (ms) large enough to actually drain `length`
|
|
169
|
+
* bytes at the negotiated baud rate. A fixed 2 s timeout spuriously fails
|
|
170
|
+
* large writes on slow links; this scales with the payload while keeping a 2 s
|
|
171
|
+
* floor. ~10 bits per byte (start + 8 data + stop) with a 2× safety margin.
|
|
172
|
+
*/
|
|
173
|
+
#writeTimeoutFor(length) {
|
|
174
|
+
const kMinTimeoutMs = 2000;
|
|
175
|
+
/* istanbul ignore next — baudRate is always > 0 when the port is open */
|
|
176
|
+
const baud = this.#baudRate > 0 ? this.#baudRate : 9600;
|
|
177
|
+
const estimatedMs = Math.ceil(length * 10 * 1000 / baud) * 2;
|
|
178
|
+
return Math.max(kMinTimeoutMs, estimatedMs);
|
|
179
|
+
}
|
|
125
180
|
constructor(usb, deviceId, portNumber, usbVendorId, usbProductId) {
|
|
126
181
|
super();
|
|
127
182
|
this.#usb = usb;
|
|
@@ -136,12 +191,13 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
136
191
|
getVid: () => this.#usbVendorId,
|
|
137
192
|
getPid: () => this.#usbProductId,
|
|
138
193
|
getPortNumber: () => this.#portNumber,
|
|
139
|
-
|
|
194
|
+
isForgotten: () => this.#state === 'forgotten',
|
|
140
195
|
setDeviceId: id => {
|
|
141
196
|
this.#deviceId = id;
|
|
142
197
|
serialPortDeviceIds.set(this, id);
|
|
143
198
|
},
|
|
144
|
-
handleDeviceLost: () => this.#handleDeviceLost()
|
|
199
|
+
handleDeviceLost: () => this.#handleDeviceLost(),
|
|
200
|
+
handleDeviceDetached: () => this.#handleDeviceDetached()
|
|
145
201
|
});
|
|
146
202
|
}
|
|
147
203
|
|
|
@@ -152,33 +208,70 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
152
208
|
* After this, a later open() (e.g. when the device is re-attached) succeeds.
|
|
153
209
|
*/
|
|
154
210
|
#handleDeviceLost() {
|
|
155
|
-
|
|
211
|
+
const wasForgotten = this.#state === 'forgotten';
|
|
212
|
+
|
|
213
|
+
// Reject any in-flight read/write with a "NetworkError" (per spec), erroring
|
|
214
|
+
// the stream controllers directly. We must NOT cancel()/abort() the streams:
|
|
215
|
+
// cancel() rejects on a reader-locked stream (leaving the read orphaned) and
|
|
216
|
+
// both would try to invoke the OS on the now-gone device.
|
|
217
|
+
const lost = new _domException.DOMException('The device has been lost.', 'NetworkError');
|
|
156
218
|
if (this.#readable) {
|
|
157
219
|
this.#readFatal = true;
|
|
158
220
|
try {
|
|
159
|
-
|
|
160
|
-
this.#readable.cancel().catch(() => {});
|
|
221
|
+
this.#readableController?.error(lost);
|
|
161
222
|
} catch {}
|
|
162
223
|
}
|
|
163
224
|
if (this.#writable) {
|
|
164
225
|
this.#writeFatal = true;
|
|
165
226
|
try {
|
|
166
|
-
this.#
|
|
227
|
+
this.#writableController?.error(lost);
|
|
167
228
|
} catch {}
|
|
168
229
|
}
|
|
169
|
-
this.#
|
|
170
|
-
this
|
|
171
|
-
|
|
172
|
-
|
|
230
|
+
this.#resetToClosedState(wasForgotten ? 'forgotten' : 'closed');
|
|
231
|
+
this.dispatchEvent(new _eventTarget.Event('disconnect'));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Reset this port to a closed, re-openable state after a clean physical
|
|
236
|
+
* detach. Unlike device loss, buffered readable bytes should still be
|
|
237
|
+
* observable before the stream ends.
|
|
238
|
+
*/
|
|
239
|
+
#handleDeviceDetached() {
|
|
240
|
+
const wasForgotten = this.#state === 'forgotten';
|
|
241
|
+
const readable = this.#readable;
|
|
242
|
+
const writable = this.#writable;
|
|
243
|
+
|
|
244
|
+
// Keep the public event and state transition synchronous so existing
|
|
245
|
+
// callers observe the detach immediately, but defer the stream teardown by
|
|
246
|
+
// one microtask to let already-queued data drain first.
|
|
247
|
+
this.#state = wasForgotten ? 'forgotten' : 'closed';
|
|
248
|
+
this.#connected = false;
|
|
173
249
|
this.#readable = null;
|
|
174
250
|
this.#writable = null;
|
|
175
|
-
this.#readFatal = false;
|
|
176
|
-
this.#writeFatal = false;
|
|
177
|
-
this.#pendingClosePromise = null;
|
|
178
|
-
this.#bufferSize = undefined;
|
|
179
|
-
this.#state = 'closed';
|
|
180
|
-
this.#connected = false;
|
|
181
251
|
this.dispatchEvent(new _eventTarget.Event('disconnect'));
|
|
252
|
+
queueMicrotask(() => {
|
|
253
|
+
// Close the readable stream so any bytes already queued can still be
|
|
254
|
+
// drained by the consumer before EOF.
|
|
255
|
+
try {
|
|
256
|
+
if (readable) this.#readableController?.close();
|
|
257
|
+
} catch {}
|
|
258
|
+
|
|
259
|
+
// A detached device can no longer accept writes, so reject any writable
|
|
260
|
+
// traffic and clear local state.
|
|
261
|
+
const lost = new _domException.DOMException('The device has been lost.', 'NetworkError');
|
|
262
|
+
if (writable) {
|
|
263
|
+
this.#writeFatal = true;
|
|
264
|
+
try {
|
|
265
|
+
this.#writableController?.error(lost);
|
|
266
|
+
} catch {}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Mirror the stream-closing bookkeeping without treating this as a hard
|
|
270
|
+
// read fatal error.
|
|
271
|
+
this.#handleClosingReadableStream();
|
|
272
|
+
this.#handleClosingWritableStream();
|
|
273
|
+
this.#resetToClosedState(wasForgotten ? 'forgotten' : 'closed');
|
|
274
|
+
});
|
|
182
275
|
}
|
|
183
276
|
|
|
184
277
|
/**
|
|
@@ -247,17 +340,29 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
247
340
|
const deviceId = this.#deviceId;
|
|
248
341
|
const portNumber = this.#portNumber;
|
|
249
342
|
const bufferSize = this.#bufferSize;
|
|
250
|
-
const stream = new
|
|
343
|
+
const stream = new _webStreams.ReadableStreamImpl({
|
|
251
344
|
start(controller) {
|
|
345
|
+
self.#readableController = controller;
|
|
252
346
|
self.#dataSubscription = self.#usb.onData(event => {
|
|
253
347
|
if (event.deviceId === deviceId && event.portNumber === portNumber) {
|
|
254
|
-
|
|
348
|
+
try {
|
|
349
|
+
controller.enqueue(new Uint8Array(event.data));
|
|
350
|
+
} catch {
|
|
351
|
+
// The stream may already be closing/closed — e.g. a data event
|
|
352
|
+
// racing cancel()/close(), where the stream is closed but this
|
|
353
|
+
// subscription is not yet torn down. Dropping the chunk is
|
|
354
|
+
// correct (the consumer has stopped reading) and avoids throwing
|
|
355
|
+
// out of the transport's event-dispatch loop.
|
|
356
|
+
}
|
|
255
357
|
}
|
|
256
358
|
});
|
|
257
359
|
self.#errorSubscription = self.#usb.onError(event => {
|
|
258
360
|
if (event.deviceId === deviceId && event.portNumber === portNumber) {
|
|
259
361
|
self.#readFatal = true; // Set this.[[readFatal]] to true.
|
|
260
|
-
|
|
362
|
+
// Surface the spec error type (BreakError, BufferOverrunError,
|
|
363
|
+
// FramingError, ParityError, …) when the transport reports one;
|
|
364
|
+
// fall back to NetworkError otherwise.
|
|
365
|
+
controller.error(new _domException.DOMException(event.error, event.errorName ?? 'NetworkError'));
|
|
261
366
|
self.#handleClosingReadableStream();
|
|
262
367
|
}
|
|
263
368
|
});
|
|
@@ -268,7 +373,7 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
268
373
|
await self.#usb.purgeHwBuffers(deviceId, portNumber, false, true).catch(() => {});
|
|
269
374
|
self.#handleClosingReadableStream();
|
|
270
375
|
}
|
|
271
|
-
}, new
|
|
376
|
+
}, new _webStreams.ByteLengthQueuingStrategyImpl({
|
|
272
377
|
highWaterMark: bufferSize
|
|
273
378
|
}));
|
|
274
379
|
|
|
@@ -299,10 +404,14 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
299
404
|
const deviceId = this.#deviceId;
|
|
300
405
|
const portNumber = this.#portNumber;
|
|
301
406
|
const bufferSize = this.#bufferSize;
|
|
302
|
-
const stream = new
|
|
407
|
+
const stream = new _webStreams.WritableStreamImpl({
|
|
408
|
+
start(controller) {
|
|
409
|
+
self.#writableController = controller;
|
|
410
|
+
},
|
|
303
411
|
async write(chunk) {
|
|
412
|
+
const bytes = bufferSourceToBytes(chunk);
|
|
304
413
|
try {
|
|
305
|
-
await self.#usb.write(deviceId, portNumber,
|
|
414
|
+
await self.#usb.write(deviceId, portNumber, bytes, self.#writeTimeoutFor(bytes.length));
|
|
306
415
|
} catch (e) {
|
|
307
416
|
// If the port was disconnected, set this.[[writeFatal]] to true.
|
|
308
417
|
self.#writeFatal = true;
|
|
@@ -321,7 +430,7 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
321
430
|
// of all software and hardware transmit buffers for the port.
|
|
322
431
|
self.#handleClosingWritableStream();
|
|
323
432
|
}
|
|
324
|
-
}, new
|
|
433
|
+
}, new _webStreams.ByteLengthQueuingStrategyImpl({
|
|
325
434
|
highWaterMark: bufferSize
|
|
326
435
|
}));
|
|
327
436
|
|
|
@@ -380,7 +489,7 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
380
489
|
|
|
381
490
|
// 3. If options["baudRate"] is 0, reject promise with a TypeError and
|
|
382
491
|
// return promise.
|
|
383
|
-
if (options.baudRate
|
|
492
|
+
if (!Number.isFinite(options.baudRate) || options.baudRate <= 0) {
|
|
384
493
|
throw new TypeError('baudRate must be a positive, non-zero value.');
|
|
385
494
|
}
|
|
386
495
|
|
|
@@ -401,7 +510,7 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
401
510
|
// 6. If options["bufferSize"] is 0, reject promise with a TypeError and
|
|
402
511
|
// return promise.
|
|
403
512
|
const bufferSize = options.bufferSize ?? kDefaultBufferSize;
|
|
404
|
-
if (bufferSize
|
|
513
|
+
if (!Number.isFinite(bufferSize) || bufferSize <= 0) {
|
|
405
514
|
throw new TypeError('bufferSize must be a positive, non-zero value.');
|
|
406
515
|
}
|
|
407
516
|
const parity = options.parity ?? kDefaultParity;
|
|
@@ -409,6 +518,9 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
409
518
|
throw new TypeError(`parity must be one of: ${kAcceptableParity.join(', ')}.`);
|
|
410
519
|
}
|
|
411
520
|
const flowControl = options.flowControl ?? kDefaultFlowControl;
|
|
521
|
+
if (!kAcceptableFlowControl.includes(flowControl)) {
|
|
522
|
+
throw new TypeError(`flowControl must be one of: ${kAcceptableFlowControl.join(', ')}.`);
|
|
523
|
+
}
|
|
412
524
|
|
|
413
525
|
// 8. Set this.[[state]] to "opening".
|
|
414
526
|
this.#state = 'opening';
|
|
@@ -429,13 +541,41 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
429
541
|
this.#state = 'closed';
|
|
430
542
|
throw new _domException.DOMException(`Failed to open serial port: ${e.message}`, 'NetworkError');
|
|
431
543
|
}
|
|
432
|
-
|
|
433
|
-
|
|
544
|
+
try {
|
|
545
|
+
if (flowControl !== 'none') {
|
|
546
|
+
await this.#usb.setFlowControl(this.#deviceId, this.#portNumber, flowControlToNative(flowControl));
|
|
547
|
+
}
|
|
548
|
+
await this.#usb.startReading(this.#deviceId, this.#portNumber);
|
|
549
|
+
} catch (e) {
|
|
550
|
+
// If post-open setup fails (flow-control/read pump), best-effort close
|
|
551
|
+
// and reset local state so a subsequent open() can succeed cleanly.
|
|
552
|
+
try {
|
|
553
|
+
await this.#usb.close(this.#deviceId, this.#portNumber);
|
|
554
|
+
} catch {
|
|
555
|
+
// ignore
|
|
556
|
+
}
|
|
557
|
+
this.#resetToClosedState(this.#forgetRequested ? 'forgotten' : 'closed');
|
|
558
|
+
throw new _domException.DOMException(`Failed to open serial port: ${e.message}`, 'NetworkError');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Guard against lifecycle races (e.g. forget() called while open() is
|
|
562
|
+
// still awaiting native setup). Do not revive a forgotten/changed state.
|
|
563
|
+
if (this.#state !== 'opening') {
|
|
564
|
+
try {
|
|
565
|
+
await this.#usb.close(this.#deviceId, this.#portNumber);
|
|
566
|
+
} catch {
|
|
567
|
+
// ignore
|
|
568
|
+
}
|
|
569
|
+
if (this.#state === 'forgotten' || this.#state === 'forgetting') {
|
|
570
|
+
this.#resetToClosedState('forgotten');
|
|
571
|
+
throw new _domException.DOMException('The port has been forgotten while opening.', 'InvalidStateError');
|
|
572
|
+
}
|
|
573
|
+
this.#resetToClosedState();
|
|
574
|
+
throw new _domException.DOMException('Failed to open serial port: state changed while opening.', 'NetworkError');
|
|
434
575
|
}
|
|
435
576
|
this.#state = 'opened'; // 9.3. Set this.[[state]] to "opened".
|
|
436
577
|
this.#bufferSize = bufferSize; // 9.4. Set this.[[bufferSize]] to options["bufferSize"].
|
|
437
|
-
|
|
438
|
-
await this.#usb.startReading(this.#deviceId, this.#portNumber);
|
|
578
|
+
this.#baudRate = options.baudRate;
|
|
439
579
|
|
|
440
580
|
// When the port becomes logically connected:
|
|
441
581
|
this.#connected = true; // 2. Set port.[[connected]] to true.
|
|
@@ -443,6 +583,13 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
443
583
|
// device presence (attach/detach), dispatched by Serial from the native
|
|
444
584
|
// USB state events — not logical open()/close(). So we do not dispatch them
|
|
445
585
|
// here; that also avoids re-entrancy with auto-reconnect listeners.
|
|
586
|
+
|
|
587
|
+
// Materialise the readable eagerly so its data subscription is live the
|
|
588
|
+
// instant the native read pump (startReading, above) begins emitting. The
|
|
589
|
+
// device may transmit immediately on open; without an active subscription
|
|
590
|
+
// those first bytes would be delivered to no listener and silently lost in
|
|
591
|
+
// the window between open() resolving and the first port.readable access.
|
|
592
|
+
void this.readable;
|
|
446
593
|
}
|
|
447
594
|
|
|
448
595
|
/**
|
|
@@ -492,17 +639,11 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
492
639
|
} catch {
|
|
493
640
|
// ignore
|
|
494
641
|
}
|
|
495
|
-
this.#dataSubscription?.remove();
|
|
496
|
-
this.#errorSubscription?.remove();
|
|
497
|
-
this.#dataSubscription = null;
|
|
498
|
-
this.#errorSubscription = null;
|
|
499
|
-
this.#state = 'closed'; // 10.1.2. Set this.[[state]] to "closed".
|
|
500
|
-
this.#readFatal = false; // 10.1.3. Set this.[[readFatal]] and this.[[writeFatal]] to false.
|
|
501
|
-
this.#writeFatal = false; // 10.1.3. (continued)
|
|
502
|
-
this.#pendingClosePromise = null; // 10.1.4. Set this.[[pendingClosePromise]] to null.
|
|
503
642
|
|
|
504
|
-
//
|
|
505
|
-
|
|
643
|
+
// 10.1.2-10.1.4 and logical disconnect bookkeeping.
|
|
644
|
+
// If forget() was requested while close() was in flight, preserve
|
|
645
|
+
// forgotten semantics instead of reviving the port to "closed".
|
|
646
|
+
this.#resetToClosedState(this.#forgetRequested ? 'forgotten' : 'closed');
|
|
506
647
|
// (No port-level "disconnect" dispatch here — that event signals physical
|
|
507
648
|
// detach, fired by Serial from the native USB state events. See open().)
|
|
508
649
|
}
|
|
@@ -513,6 +654,14 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
513
654
|
async forget() {
|
|
514
655
|
// The forget() method steps are:
|
|
515
656
|
|
|
657
|
+
this.#forgetRequested = true;
|
|
658
|
+
|
|
659
|
+
// Forgetting an open port must not leave active streams/native resources
|
|
660
|
+
// behind. Close first so the instance becomes cleanly unusable afterwards.
|
|
661
|
+
if (this.#state === 'opened') {
|
|
662
|
+
await this.close();
|
|
663
|
+
}
|
|
664
|
+
|
|
516
665
|
// 2.1. Set this.[[state]] to "forgetting".
|
|
517
666
|
this.#state = 'forgetting';
|
|
518
667
|
// 2.2. Remove this from the sequence of serial ports on the system which
|
|
@@ -539,12 +688,6 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
539
688
|
if (signals.dataTerminalReady === undefined && signals.requestToSend === undefined && signals.break === undefined) {
|
|
540
689
|
throw new TypeError('At least one signal must be specified.');
|
|
541
690
|
}
|
|
542
|
-
|
|
543
|
-
// Merge with current output signal state for partial updates
|
|
544
|
-
this.#outputSignals = {
|
|
545
|
-
...this.#outputSignals,
|
|
546
|
-
...signals
|
|
547
|
-
};
|
|
548
691
|
const promises = [];
|
|
549
692
|
|
|
550
693
|
// 4.1. If signals["dataTerminalReady"] is present, invoke the operating
|
|
@@ -618,6 +761,17 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
618
761
|
#handleClosingReadableStream() {
|
|
619
762
|
// To handle closing the readable stream perform the following steps:
|
|
620
763
|
|
|
764
|
+
// Drop the native data/error subscription tied to this readable. Without
|
|
765
|
+
// this a later readable (after cancel() + re-acquire) would coexist with the
|
|
766
|
+
// old subscription, which then enqueues into the cancelled controller and
|
|
767
|
+
// throws. close()/handleDeviceLost() also clear these; doing it here covers
|
|
768
|
+
// a standalone readable.cancel().
|
|
769
|
+
this.#dataSubscription?.remove();
|
|
770
|
+
this.#errorSubscription?.remove();
|
|
771
|
+
this.#dataSubscription = null;
|
|
772
|
+
this.#errorSubscription = null;
|
|
773
|
+
this.#readableController = null;
|
|
774
|
+
|
|
621
775
|
// 1. Set this.[[readable]] to null.
|
|
622
776
|
this.#readable = null;
|
|
623
777
|
|
|
@@ -634,6 +788,8 @@ class SerialPort extends _eventTarget.EventTarget {
|
|
|
634
788
|
#handleClosingWritableStream() {
|
|
635
789
|
// To handle closing the writable stream perform the following steps:
|
|
636
790
|
|
|
791
|
+
this.#writableController = null;
|
|
792
|
+
|
|
637
793
|
// 1. Set this.[[writable]] to null.
|
|
638
794
|
this.#writable = null;
|
|
639
795
|
|
|
@@ -657,6 +813,20 @@ class Serial extends _eventTarget.EventTarget {
|
|
|
657
813
|
#usb = null;
|
|
658
814
|
#knownPorts = new Map();
|
|
659
815
|
#initialized = false;
|
|
816
|
+
#injected;
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* @param transport Optional transport override. When provided, this `Serial`
|
|
820
|
+
* talks to it instead of the global native module — the transport override used by tests
|
|
821
|
+
* and the virtual serial device harness (`new Serial(new InMemorySerialTransport())`).
|
|
822
|
+
* Omit it for the normal native-backed instance; the singleton `serial`
|
|
823
|
+
* export is created this way and can still be redirected globally via
|
|
824
|
+
* `setUsbSerial()`.
|
|
825
|
+
*/
|
|
826
|
+
constructor(transport) {
|
|
827
|
+
super();
|
|
828
|
+
this.#injected = transport ?? null;
|
|
829
|
+
}
|
|
660
830
|
|
|
661
831
|
/**
|
|
662
832
|
* Subscribing to "connect"/"disconnect" must wire up the native USB state
|
|
@@ -678,32 +848,39 @@ class Serial extends _eventTarget.EventTarget {
|
|
|
678
848
|
if (this.#initialized) return this.#usb;
|
|
679
849
|
this.#initialized = true;
|
|
680
850
|
try {
|
|
681
|
-
this.#usb = (0, _UsbSerial.getUsbSerial)();
|
|
851
|
+
this.#usb = this.#injected ?? (0, _UsbSerial.getUsbSerial)();
|
|
682
852
|
|
|
683
853
|
// A USB device was attached. Android assigns a NEW deviceId on every
|
|
684
854
|
// attach, so match previously-known ports of the same physical device by
|
|
685
855
|
// VID/PID, update their deviceId, re-key them, and fire "connect" on the
|
|
686
856
|
// same SerialPort instance (W3C spec model: the port is reused).
|
|
687
857
|
this.#usb.onConnect(event => {
|
|
688
|
-
|
|
689
|
-
|
|
858
|
+
// Native connect events expose VID/PID but not a stable unique
|
|
859
|
+
// identifier. To avoid mis-associating identical devices, remap only
|
|
860
|
+
// when there is exactly one disconnected candidate.
|
|
861
|
+
const candidates = Array.from(this.#knownPorts.entries()).filter(([, port]) => {
|
|
690
862
|
const internals = portInternals.get(port);
|
|
691
|
-
if (
|
|
692
|
-
if (internals.
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
863
|
+
if (internals.getVid() !== event.usbVendorId) return false;
|
|
864
|
+
if (internals.getPid() !== event.usbProductId) return false;
|
|
865
|
+
if (internals.isForgotten()) return false;
|
|
866
|
+
return !port.connected;
|
|
867
|
+
});
|
|
868
|
+
if (candidates.length === 1) {
|
|
869
|
+
const [key, port] = candidates[0];
|
|
870
|
+
const internals = portInternals.get(port);
|
|
871
|
+
internals.setDeviceId(event.deviceId);
|
|
872
|
+
const newKey = this.#portKey(event.deviceId, internals.getPortNumber());
|
|
873
|
+
this.#knownPorts.delete(key);
|
|
874
|
+
this.#knownPorts.set(newKey, port);
|
|
875
|
+
// Dispatched on the port; it bubbles to this Serial (event.target
|
|
876
|
+
// stays the port, per spec — see setEventParent below).
|
|
877
|
+
port.dispatchEvent(new _eventTarget.Event('connect'));
|
|
878
|
+
return;
|
|
702
879
|
}
|
|
880
|
+
|
|
881
|
+
// No known port matched (or matching is ambiguous): fire a Serial-level
|
|
882
|
+
// "connect" so listeners refresh; getPorts() surfaces the new port.
|
|
703
883
|
this.dispatchEvent(new _eventTarget.Event('connect'));
|
|
704
|
-
// If we matched no known port this is a brand-new device; getPorts()
|
|
705
|
-
// will surface it on the next refresh.
|
|
706
|
-
void matched;
|
|
707
884
|
});
|
|
708
885
|
|
|
709
886
|
// A USB device was detached. Reset the matching port(s) to a closed,
|
|
@@ -711,13 +888,21 @@ class Serial extends _eventTarget.EventTarget {
|
|
|
711
888
|
// SerialPort instance so a re-attach can reuse it. handleDeviceLost()
|
|
712
889
|
// dispatches "disconnect" on the port.
|
|
713
890
|
this.#usb.onDisconnect(event => {
|
|
891
|
+
const wasLost = event.lost ?? true;
|
|
714
892
|
const prefix = `${event.deviceId}:`;
|
|
893
|
+
let matched = false;
|
|
715
894
|
for (const [key, port] of [...this.#knownPorts.entries()]) {
|
|
716
895
|
if (key.startsWith(prefix)) {
|
|
717
|
-
|
|
896
|
+
// Physical loss tears down the stream immediately; a clean detach
|
|
897
|
+
// lets any already-buffered bytes drain before EOF.
|
|
898
|
+
const internals = portInternals.get(port);
|
|
899
|
+
if (wasLost) internals?.handleDeviceLost();else internals?.handleDeviceDetached();
|
|
900
|
+
matched = true;
|
|
718
901
|
}
|
|
719
902
|
}
|
|
720
|
-
|
|
903
|
+
// No known port matched (e.g. a device never opened): fire a
|
|
904
|
+
// Serial-level "disconnect" so listeners can still refresh.
|
|
905
|
+
if (!matched) this.dispatchEvent(new _eventTarget.Event('disconnect'));
|
|
721
906
|
});
|
|
722
907
|
} catch {
|
|
723
908
|
this.#usb = null;
|
|
@@ -792,8 +977,12 @@ class Serial extends _eventTarget.EventTarget {
|
|
|
792
977
|
} of portIds) {
|
|
793
978
|
if (!hasPermission) continue;
|
|
794
979
|
const key = this.#portKey(deviceId, portNumber);
|
|
795
|
-
|
|
796
|
-
|
|
980
|
+
const known = this.#knownPorts.get(key);
|
|
981
|
+
if (!known || portInternals.get(known)?.isForgotten()) {
|
|
982
|
+
const port = new SerialPort(usb, deviceId, portNumber, usbVendorId, usbProductId);
|
|
983
|
+
// The port's connect/disconnect events bubble to this Serial.
|
|
984
|
+
(0, _eventTarget.setEventParent)(port, this);
|
|
985
|
+
this.#knownPorts.set(key, port);
|
|
797
986
|
}
|
|
798
987
|
ports.push(this.#knownPorts.get(key));
|
|
799
988
|
}
|
|
@@ -812,6 +1001,9 @@ class Serial extends _eventTarget.EventTarget {
|
|
|
812
1001
|
if (!usb) {
|
|
813
1002
|
throw new _domException.DOMException('NativeUsbSerial is not available.', 'NotFoundError');
|
|
814
1003
|
}
|
|
1004
|
+
if ((options.allowedBluetoothServiceClassIds?.length ?? 0) > 0) {
|
|
1005
|
+
throw new TypeError('allowedBluetoothServiceClassIds is not supported in Android USB mode.');
|
|
1006
|
+
}
|
|
815
1007
|
|
|
816
1008
|
// 4. If options["filters"] is present, then for each filter in
|
|
817
1009
|
// options["filters"] run the following steps:
|
|
@@ -854,8 +1046,12 @@ class Serial extends _eventTarget.EventTarget {
|
|
|
854
1046
|
|
|
855
1047
|
// 5.6. Let port be a SerialPort representing the port chosen by the user.
|
|
856
1048
|
const key = `${portId.deviceId}:${portId.portNumber}`;
|
|
857
|
-
|
|
858
|
-
|
|
1049
|
+
const known = this.#knownPorts.get(key);
|
|
1050
|
+
if (!known || portInternals.get(known)?.isForgotten()) {
|
|
1051
|
+
const port = new SerialPort(usb, portId.deviceId, portId.portNumber, portId.usbVendorId, portId.usbProductId);
|
|
1052
|
+
// The port's connect/disconnect events bubble to this Serial.
|
|
1053
|
+
(0, _eventTarget.setEventParent)(port, this);
|
|
1054
|
+
this.#knownPorts.set(key, port);
|
|
859
1055
|
}
|
|
860
1056
|
|
|
861
1057
|
// 5.7. Resolve promise with port.
|