@gurezo/web-serial-rxjs 2.3.6 → 3.0.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.ja.md +1 -1
- package/README.md +1 -1
- package/dist/errors/serial-error-code.d.ts +92 -14
- package/dist/errors/serial-error-code.d.ts.map +1 -1
- package/dist/errors/serial-error-context.d.ts +47 -0
- package/dist/errors/serial-error-context.d.ts.map +1 -0
- package/dist/errors/serial-error.d.ts +30 -8
- package/dist/errors/serial-error.d.ts.map +1 -1
- package/dist/index.d.ts +13 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +1327 -493
- package/dist/index.mjs.map +4 -4
- package/dist/internal/assert-never.d.ts +2 -0
- package/dist/internal/assert-never.d.ts.map +1 -0
- package/dist/internal/branded-numbers.d.ts +37 -0
- package/dist/internal/branded-numbers.d.ts.map +1 -0
- package/dist/internal/newline-tokenizer.d.ts +38 -0
- package/dist/internal/newline-tokenizer.d.ts.map +1 -0
- package/dist/session/create-serial-session.d.ts +7 -3
- package/dist/session/create-serial-session.d.ts.map +1 -1
- package/dist/session/index.d.ts +7 -2
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/internal/build-request-options.d.ts +7 -7
- package/dist/session/internal/build-request-options.d.ts.map +1 -1
- package/dist/session/internal/error-severity.d.ts +38 -0
- package/dist/session/internal/error-severity.d.ts.map +1 -0
- package/dist/session/internal/line-buffer.d.ts +37 -4
- package/dist/session/internal/line-buffer.d.ts.map +1 -1
- package/dist/session/internal/receive-pipeline.d.ts +34 -0
- package/dist/session/internal/receive-pipeline.d.ts.map +1 -0
- package/dist/session/internal/receive-replay-buffer.d.ts +38 -0
- package/dist/session/internal/receive-replay-buffer.d.ts.map +1 -0
- package/dist/session/internal/session-error-reporter.d.ts +32 -0
- package/dist/session/internal/session-error-reporter.d.ts.map +1 -0
- package/dist/session/internal/session-lifecycle.d.ts +44 -0
- package/dist/session/internal/session-lifecycle.d.ts.map +1 -0
- package/dist/session/internal/validate-serial-port-filters.d.ts +12 -0
- package/dist/session/internal/validate-serial-port-filters.d.ts.map +1 -0
- package/dist/session/read-pump.d.ts +9 -1
- package/dist/session/read-pump.d.ts.map +1 -1
- package/dist/session/serial-session-options.d.ts +133 -46
- package/dist/session/serial-session-options.d.ts.map +1 -1
- package/dist/session/serial-session-state.d.ts +50 -12
- package/dist/session/serial-session-state.d.ts.map +1 -1
- package/dist/session/serial-session.d.ts +44 -9
- package/dist/session/serial-session.d.ts.map +1 -1
- package/dist/session/session-runtime.d.ts +140 -0
- package/dist/session/session-runtime.d.ts.map +1 -0
- package/dist/terminal/create-terminal-buffer.d.ts +48 -1
- package/dist/terminal/create-terminal-buffer.d.ts.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +2 -1
- package/dist/index.js +0 -724
- package/dist/session/session-state-machine.d.ts +0 -39
- package/dist/session/session-state-machine.d.ts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,190 +1,679 @@
|
|
|
1
|
+
// src/internal/assert-never.ts
|
|
2
|
+
function assertNever(value) {
|
|
3
|
+
throw new Error(`Unexpected value: ${String(value)}`);
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
// src/session/create-serial-session.ts
|
|
2
7
|
import {
|
|
3
|
-
BehaviorSubject as
|
|
8
|
+
BehaviorSubject as BehaviorSubject3,
|
|
4
9
|
distinctUntilChanged,
|
|
5
10
|
map as map2,
|
|
6
|
-
Observable as
|
|
7
|
-
|
|
8
|
-
share,
|
|
9
|
-
Subject,
|
|
10
|
-
switchMap
|
|
11
|
+
Observable as Observable5,
|
|
12
|
+
Subject as Subject3
|
|
11
13
|
} from "rxjs";
|
|
12
14
|
|
|
13
15
|
// src/errors/serial-error-code.ts
|
|
14
|
-
var SerialErrorCode =
|
|
15
|
-
SerialErrorCode2["BROWSER_NOT_SUPPORTED"] = "BROWSER_NOT_SUPPORTED";
|
|
16
|
-
SerialErrorCode2["PORT_NOT_AVAILABLE"] = "PORT_NOT_AVAILABLE";
|
|
17
|
-
SerialErrorCode2["PORT_OPEN_FAILED"] = "PORT_OPEN_FAILED";
|
|
18
|
-
SerialErrorCode2["PORT_ALREADY_OPEN"] = "PORT_ALREADY_OPEN";
|
|
19
|
-
SerialErrorCode2["PORT_NOT_OPEN"] = "PORT_NOT_OPEN";
|
|
20
|
-
SerialErrorCode2["READ_FAILED"] = "READ_FAILED";
|
|
21
|
-
SerialErrorCode2["WRITE_FAILED"] = "WRITE_FAILED";
|
|
22
|
-
SerialErrorCode2["CONNECTION_LOST"] = "CONNECTION_LOST";
|
|
23
|
-
SerialErrorCode2["INVALID_FILTER_OPTIONS"] = "INVALID_FILTER_OPTIONS";
|
|
24
|
-
SerialErrorCode2["OPERATION_CANCELLED"] = "OPERATION_CANCELLED";
|
|
25
|
-
SerialErrorCode2["OPERATION_TIMEOUT"] = "OPERATION_TIMEOUT";
|
|
26
|
-
SerialErrorCode2["UNKNOWN"] = "UNKNOWN";
|
|
27
|
-
return SerialErrorCode2;
|
|
28
|
-
})(SerialErrorCode || {});
|
|
29
|
-
|
|
30
|
-
// src/errors/serial-error.ts
|
|
31
|
-
var SerialError = class _SerialError extends Error {
|
|
16
|
+
var SerialErrorCode = {
|
|
32
17
|
/**
|
|
33
|
-
*
|
|
18
|
+
* Browser does not support the Web Serial API.
|
|
34
19
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
20
|
+
* This error occurs when attempting to use serial port functionality in a browser
|
|
21
|
+
* that doesn't support the Web Serial API. Supported desktop browsers are Chrome,
|
|
22
|
+
* Edge, Opera, and Firefox 151+. Safari and mobile browsers are not supported.
|
|
23
|
+
*
|
|
24
|
+
* **Suggested action**: Inform the user to use a supported browser.
|
|
38
25
|
*/
|
|
39
|
-
|
|
40
|
-
super(message);
|
|
41
|
-
this.name = "SerialError";
|
|
42
|
-
this.code = code;
|
|
43
|
-
this.originalError = originalError;
|
|
44
|
-
if (Error.captureStackTrace) {
|
|
45
|
-
Error.captureStackTrace(this, _SerialError);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
26
|
+
BROWSER_NOT_SUPPORTED: "BROWSER_NOT_SUPPORTED",
|
|
48
27
|
/**
|
|
49
|
-
*
|
|
28
|
+
* Serial port is not available.
|
|
50
29
|
*
|
|
51
|
-
* This
|
|
52
|
-
*
|
|
30
|
+
* This error occurs when a requested port cannot be accessed, such as when
|
|
31
|
+
* getting previously granted ports fails or when the port is already in use
|
|
32
|
+
* by another application.
|
|
53
33
|
*
|
|
54
|
-
*
|
|
55
|
-
|
|
34
|
+
* **Suggested action**: Check if the port is available or being used by another application.
|
|
35
|
+
*/
|
|
36
|
+
PORT_NOT_AVAILABLE: "PORT_NOT_AVAILABLE",
|
|
37
|
+
/**
|
|
38
|
+
* Failed to open the serial port.
|
|
56
39
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* }
|
|
62
|
-
* ```
|
|
40
|
+
* This error occurs when the port cannot be opened, typically due to incorrect
|
|
41
|
+
* connection parameters, hardware issues, or permission problems.
|
|
42
|
+
*
|
|
43
|
+
* **Suggested action**: Verify connection parameters and check hardware connections.
|
|
63
44
|
*/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
45
|
+
PORT_OPEN_FAILED: "PORT_OPEN_FAILED",
|
|
46
|
+
/**
|
|
47
|
+
* Serial port is already open.
|
|
48
|
+
*
|
|
49
|
+
* This error occurs when attempting to open a port that is already connected.
|
|
50
|
+
* Only one connection can be active at a time per SerialClient instance.
|
|
51
|
+
*
|
|
52
|
+
* **Suggested action**: Disconnect the current port before connecting a new one.
|
|
53
|
+
*/
|
|
54
|
+
PORT_ALREADY_OPEN: "PORT_ALREADY_OPEN",
|
|
55
|
+
/**
|
|
56
|
+
* Serial port is not open.
|
|
57
|
+
*
|
|
58
|
+
* This error occurs when attempting to read from or write to a port that hasn't
|
|
59
|
+
* been opened yet. The port must be connected before performing I/O operations.
|
|
60
|
+
*
|
|
61
|
+
* **Suggested action**: Call {@link SerialClient.connect} before reading or writing.
|
|
62
|
+
*/
|
|
63
|
+
PORT_NOT_OPEN: "PORT_NOT_OPEN",
|
|
64
|
+
/**
|
|
65
|
+
* Failed to read from the serial port.
|
|
66
|
+
*
|
|
67
|
+
* This error occurs when reading data from the port fails, typically due to
|
|
68
|
+
* connection loss, hardware issues, or stream errors.
|
|
69
|
+
*
|
|
70
|
+
* **Suggested action**: Check the connection and hardware, then retry the read operation.
|
|
71
|
+
*/
|
|
72
|
+
READ_FAILED: "READ_FAILED",
|
|
73
|
+
/**
|
|
74
|
+
* Failed to write to the serial port.
|
|
75
|
+
*
|
|
76
|
+
* This error occurs when writing data to the port fails, typically due to
|
|
77
|
+
* connection loss, hardware issues, or stream errors.
|
|
78
|
+
*
|
|
79
|
+
* **Suggested action**: Check the connection and hardware, then retry the write operation.
|
|
80
|
+
*/
|
|
81
|
+
WRITE_FAILED: "WRITE_FAILED",
|
|
82
|
+
/**
|
|
83
|
+
* Serial port connection was lost.
|
|
84
|
+
*
|
|
85
|
+
* This error occurs when the connection to the serial port is unexpectedly
|
|
86
|
+
* terminated, such as when the device is disconnected or the port is closed
|
|
87
|
+
* by another process.
|
|
88
|
+
*
|
|
89
|
+
* **Suggested action**: Check the physical connection and reconnect if needed.
|
|
90
|
+
*/
|
|
91
|
+
CONNECTION_LOST: "CONNECTION_LOST",
|
|
92
|
+
/**
|
|
93
|
+
* Invalid filter options provided.
|
|
94
|
+
*
|
|
95
|
+
* This error occurs when port filter options are invalid, such as when
|
|
96
|
+
* filter values are out of range or missing required fields.
|
|
97
|
+
*
|
|
98
|
+
* **Suggested action**: Verify filter options match the expected format and value ranges.
|
|
99
|
+
*/
|
|
100
|
+
INVALID_FILTER_OPTIONS: "INVALID_FILTER_OPTIONS",
|
|
101
|
+
/**
|
|
102
|
+
* Operation was cancelled by the user.
|
|
103
|
+
*
|
|
104
|
+
* This error occurs when the user cancels a port selection dialog or aborts
|
|
105
|
+
* an operation before it completes.
|
|
106
|
+
*
|
|
107
|
+
* **Suggested action**: This is a normal condition - no action required, but you may want
|
|
108
|
+
* to inform the user that the operation was cancelled.
|
|
109
|
+
*/
|
|
110
|
+
OPERATION_CANCELLED: "OPERATION_CANCELLED",
|
|
111
|
+
/**
|
|
112
|
+
* Operation timed out before completion.
|
|
113
|
+
*
|
|
114
|
+
* This error occurs when an operation waits for a condition (for example, prompt
|
|
115
|
+
* detection) and the timeout period elapses first.
|
|
116
|
+
*/
|
|
117
|
+
OPERATION_TIMEOUT: "OPERATION_TIMEOUT",
|
|
118
|
+
/**
|
|
119
|
+
* Internal line buffer exceeded its configured size limit.
|
|
120
|
+
*
|
|
121
|
+
* This error occurs when the incomplete line tail held by the {@link SerialSession.lines$}
|
|
122
|
+
* framing buffer grows beyond {@link SerialSessionOptions.lineBuffer} `maxChars`.
|
|
123
|
+
* Leading characters are discarded to bound memory; the session remains connected.
|
|
124
|
+
*
|
|
125
|
+
* **Suggested action**: Increase `lineBuffer.maxChars`, handle framing on `receive$`,
|
|
126
|
+
* or ensure the device sends line terminators.
|
|
127
|
+
*/
|
|
128
|
+
LINE_BUFFER_OVERFLOW: "LINE_BUFFER_OVERFLOW",
|
|
129
|
+
/**
|
|
130
|
+
* Invalid receive replay options provided.
|
|
131
|
+
*
|
|
132
|
+
* This error occurs when {@link SerialSessionOptions.receiveReplay} values are
|
|
133
|
+
* out of range, such as a non-integer `bufferSize` or `maxChars`.
|
|
134
|
+
*
|
|
135
|
+
* **Suggested action**: Verify `receiveReplay` options match the documented
|
|
136
|
+
* ranges and value types.
|
|
137
|
+
*/
|
|
138
|
+
INVALID_RECEIVE_REPLAY_OPTIONS: "INVALID_RECEIVE_REPLAY_OPTIONS",
|
|
139
|
+
/**
|
|
140
|
+
* Invalid terminal buffer options provided.
|
|
141
|
+
*
|
|
142
|
+
* This error occurs when {@link SerialSessionOptions.terminalBuffer} values are
|
|
143
|
+
* out of range, such as a negative or non-integer `maxLines` or `maxChars`.
|
|
144
|
+
*
|
|
145
|
+
* **Suggested action**: Verify `terminalBuffer` options match the documented
|
|
146
|
+
* ranges and value types.
|
|
147
|
+
*/
|
|
148
|
+
INVALID_TERMINAL_BUFFER_OPTIONS: "INVALID_TERMINAL_BUFFER_OPTIONS",
|
|
149
|
+
/**
|
|
150
|
+
* Invalid line buffer options provided.
|
|
151
|
+
*
|
|
152
|
+
* This error occurs when {@link SerialSessionOptions.lineBuffer} values are
|
|
153
|
+
* out of range, such as a negative or non-integer `maxChars`.
|
|
154
|
+
*
|
|
155
|
+
* **Suggested action**: Verify `lineBuffer` options match the documented
|
|
156
|
+
* ranges and value types.
|
|
157
|
+
*/
|
|
158
|
+
INVALID_LINE_BUFFER_OPTIONS: "INVALID_LINE_BUFFER_OPTIONS",
|
|
159
|
+
/**
|
|
160
|
+
* Invalid connection options provided.
|
|
161
|
+
*
|
|
162
|
+
* This error occurs when connection fields such as `baudRate` are out of
|
|
163
|
+
* range at session creation time.
|
|
164
|
+
*
|
|
165
|
+
* **Suggested action**: Verify connection options match the documented
|
|
166
|
+
* ranges and value types.
|
|
167
|
+
*/
|
|
168
|
+
INVALID_CONNECTION_OPTIONS: "INVALID_CONNECTION_OPTIONS",
|
|
169
|
+
/**
|
|
170
|
+
* Receive replay buffer exceeded its configured character limit.
|
|
171
|
+
*
|
|
172
|
+
* This error occurs when buffered chunks on {@link SerialSession.receiveReplay$}
|
|
173
|
+
* exceed {@link SerialSessionOptions.receiveReplay} `maxChars`. Oldest chunks
|
|
174
|
+
* are discarded to bound memory; the session remains connected.
|
|
175
|
+
*
|
|
176
|
+
* **Suggested action**: Increase `receiveReplay.maxChars`, reduce chunk size at
|
|
177
|
+
* the source, or subscribe earlier to avoid relying on a large replay buffer.
|
|
178
|
+
*/
|
|
179
|
+
RECEIVE_REPLAY_BUFFER_OVERFLOW: "RECEIVE_REPLAY_BUFFER_OVERFLOW",
|
|
180
|
+
/**
|
|
181
|
+
* Session has been disposed and can no longer be used.
|
|
182
|
+
*
|
|
183
|
+
* This error occurs when calling {@link SerialSession.connect$} or
|
|
184
|
+
* {@link SerialSession.send$} after {@link SerialSession.dispose$} has
|
|
185
|
+
* completed. Create a new session instead of reusing a disposed instance.
|
|
186
|
+
*
|
|
187
|
+
* **Suggested action**: Call {@link SerialSession.dispose$} only when
|
|
188
|
+
* permanently tearing down a session, then create a new one if needed.
|
|
189
|
+
*/
|
|
190
|
+
SESSION_DISPOSED: "SESSION_DISPOSED",
|
|
191
|
+
/**
|
|
192
|
+
* Unknown error occurred.
|
|
193
|
+
*
|
|
194
|
+
* This error code is used for errors that don't fit into any other category.
|
|
195
|
+
* The original error details may be available in the error's message or originalError property.
|
|
196
|
+
*
|
|
197
|
+
* **Suggested action**: Check the error message and originalError for more details.
|
|
198
|
+
*/
|
|
199
|
+
UNKNOWN: "UNKNOWN"
|
|
67
200
|
};
|
|
68
201
|
|
|
69
202
|
// src/terminal/create-terminal-buffer.ts
|
|
70
203
|
import { map, scan, shareReplay } from "rxjs";
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
204
|
+
|
|
205
|
+
// src/internal/branded-numbers.ts
|
|
206
|
+
function brandBaudRate(value) {
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
function brandSerialPortBufferSize(value) {
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
function brandReceiveReplayBufferSize(value) {
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
function brandMaxChars(value) {
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
function brandMaxLines(value) {
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/internal/newline-tokenizer.ts
|
|
223
|
+
function createNewlineTokenizer(mode) {
|
|
224
|
+
let pending = "";
|
|
225
|
+
let deferredTrailingCr = false;
|
|
226
|
+
const clear = () => {
|
|
227
|
+
pending = "";
|
|
228
|
+
deferredTrailingCr = false;
|
|
229
|
+
};
|
|
230
|
+
const restorePending = (text) => {
|
|
231
|
+
pending = text;
|
|
232
|
+
deferredTrailingCr = false;
|
|
233
|
+
};
|
|
234
|
+
const feed = (chunk) => {
|
|
235
|
+
const events = [];
|
|
236
|
+
let i = 0;
|
|
237
|
+
if (mode === "line" && deferredTrailingCr) {
|
|
238
|
+
deferredTrailingCr = false;
|
|
239
|
+
if (chunk.length > 0 && chunk.charAt(0) === "\n") {
|
|
240
|
+
events.push({ type: "line", content: pending });
|
|
241
|
+
pending = "";
|
|
242
|
+
i = 1;
|
|
82
243
|
} else {
|
|
83
|
-
|
|
244
|
+
events.push({ type: "line", content: pending });
|
|
245
|
+
pending = "";
|
|
84
246
|
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
247
|
+
}
|
|
248
|
+
const len = chunk.length;
|
|
249
|
+
for (; i < len; i++) {
|
|
250
|
+
const c = chunk.charAt(i);
|
|
251
|
+
if (c === "\r") {
|
|
252
|
+
const next = i + 1 < len ? chunk.charAt(i + 1) : "";
|
|
253
|
+
if (next === "\n") {
|
|
254
|
+
events.push({ type: "line", content: pending });
|
|
255
|
+
pending = "";
|
|
256
|
+
i++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (mode === "line") {
|
|
260
|
+
if (next !== "") {
|
|
261
|
+
events.push({ type: "line", content: pending });
|
|
262
|
+
pending = "";
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
deferredTrailingCr = true;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
events.push({ type: "carriage-return" });
|
|
269
|
+
pending = "";
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (c === "\n") {
|
|
273
|
+
events.push({ type: "line", content: pending });
|
|
274
|
+
pending = "";
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
pending += c;
|
|
278
|
+
}
|
|
279
|
+
return events;
|
|
280
|
+
};
|
|
281
|
+
const trimPending = (maxChars) => {
|
|
282
|
+
if (maxChars <= 0) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
const text = deferredTrailingCr ? `${pending}\r` : pending;
|
|
286
|
+
if (text.length <= maxChars) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
const trimmed = text.slice(text.length - maxChars);
|
|
290
|
+
if (trimmed.endsWith("\r")) {
|
|
291
|
+
pending = trimmed.slice(0, -1);
|
|
292
|
+
deferredTrailingCr = true;
|
|
88
293
|
} else {
|
|
89
|
-
|
|
294
|
+
pending = trimmed;
|
|
295
|
+
deferredTrailingCr = false;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
};
|
|
299
|
+
return {
|
|
300
|
+
feed,
|
|
301
|
+
clear,
|
|
302
|
+
restorePending,
|
|
303
|
+
getPendingText: () => deferredTrailingCr ? `${pending}\r` : pending,
|
|
304
|
+
trimPending
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/terminal/create-terminal-buffer.ts
|
|
309
|
+
function applyTerminalChunk(state, chunk) {
|
|
310
|
+
const tokenizer = createNewlineTokenizer("terminal");
|
|
311
|
+
tokenizer.restorePending(state.currentLine);
|
|
312
|
+
const events = tokenizer.feed(chunk);
|
|
313
|
+
let { completed } = state;
|
|
314
|
+
for (const event of events) {
|
|
315
|
+
if (event.type === "line") {
|
|
316
|
+
completed += event.content + "\n";
|
|
90
317
|
}
|
|
91
318
|
}
|
|
92
|
-
return {
|
|
319
|
+
return {
|
|
320
|
+
completed,
|
|
321
|
+
currentLine: tokenizer.getPendingText()
|
|
322
|
+
};
|
|
93
323
|
}
|
|
94
324
|
function terminalDisplayText(state) {
|
|
95
325
|
return state.completed + state.currentLine;
|
|
96
326
|
}
|
|
327
|
+
function countCompletedLines(completed) {
|
|
328
|
+
if (completed.length === 0) {
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
let count = 0;
|
|
332
|
+
for (let i = 0; i < completed.length; i++) {
|
|
333
|
+
if (completed.charAt(i) === "\n") {
|
|
334
|
+
count++;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return count;
|
|
338
|
+
}
|
|
339
|
+
function trimCompletedByMaxLines(completed, maxLines) {
|
|
340
|
+
if (maxLines <= 0) {
|
|
341
|
+
return completed;
|
|
342
|
+
}
|
|
343
|
+
let trimmed = completed;
|
|
344
|
+
while (countCompletedLines(trimmed) > maxLines) {
|
|
345
|
+
const firstNewline = trimmed.indexOf("\n");
|
|
346
|
+
if (firstNewline < 0) {
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
trimmed = trimmed.slice(firstNewline + 1);
|
|
350
|
+
}
|
|
351
|
+
return trimmed;
|
|
352
|
+
}
|
|
353
|
+
function trimTerminalState(state, limits) {
|
|
354
|
+
let { completed, currentLine } = state;
|
|
355
|
+
if (limits.maxLines > 0) {
|
|
356
|
+
completed = trimCompletedByMaxLines(completed, limits.maxLines);
|
|
357
|
+
}
|
|
358
|
+
if (limits.maxChars > 0) {
|
|
359
|
+
let total = completed.length + currentLine.length;
|
|
360
|
+
while (total > limits.maxChars) {
|
|
361
|
+
const excess = total - limits.maxChars;
|
|
362
|
+
if (completed.length >= excess) {
|
|
363
|
+
completed = completed.slice(excess);
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
const removeFromCurrent = excess - completed.length;
|
|
367
|
+
completed = "";
|
|
368
|
+
currentLine = currentLine.slice(removeFromCurrent);
|
|
369
|
+
total = completed.length + currentLine.length;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return { completed, currentLine };
|
|
373
|
+
}
|
|
374
|
+
var DEFAULT_TERMINAL_BUFFER_OPTIONS = {
|
|
375
|
+
maxLines: 1e4,
|
|
376
|
+
maxChars: 1048576
|
|
377
|
+
};
|
|
378
|
+
function resolveTerminalBufferLimits(options) {
|
|
379
|
+
const maxLines = options?.maxLines ?? DEFAULT_TERMINAL_BUFFER_OPTIONS.maxLines;
|
|
380
|
+
const maxChars = options?.maxChars ?? DEFAULT_TERMINAL_BUFFER_OPTIONS.maxChars;
|
|
381
|
+
return {
|
|
382
|
+
maxLines: brandMaxLines(maxLines),
|
|
383
|
+
maxChars: brandMaxChars(maxChars)
|
|
384
|
+
};
|
|
385
|
+
}
|
|
97
386
|
var initialTerminalState = {
|
|
98
387
|
completed: "",
|
|
99
388
|
currentLine: ""
|
|
100
389
|
};
|
|
101
|
-
function createTerminalBuffer(receive
|
|
390
|
+
function createTerminalBuffer(receive$, options) {
|
|
391
|
+
const limits = resolveTerminalBufferLimits(options);
|
|
102
392
|
const text$ = receive$.pipe(
|
|
103
|
-
scan(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
),
|
|
393
|
+
scan((state, chunk) => {
|
|
394
|
+
const next = applyTerminalChunk(state, chunk);
|
|
395
|
+
return trimTerminalState(next, limits);
|
|
396
|
+
}, initialTerminalState),
|
|
107
397
|
map(terminalDisplayText),
|
|
108
398
|
shareReplay({ bufferSize: 1, refCount: true })
|
|
109
399
|
);
|
|
110
400
|
return { text$ };
|
|
111
401
|
}
|
|
112
402
|
|
|
113
|
-
// src/session/internal/build-request-options.ts
|
|
114
|
-
function buildRequestOptions(options) {
|
|
115
|
-
if (!options || !options.filters || options.filters.length === 0) {
|
|
116
|
-
return void 0;
|
|
117
|
-
}
|
|
118
|
-
for (const filter of options.filters) {
|
|
119
|
-
if (!filter.usbVendorId && !filter.usbProductId) {
|
|
120
|
-
throw new SerialError(
|
|
121
|
-
"INVALID_FILTER_OPTIONS" /* INVALID_FILTER_OPTIONS */,
|
|
122
|
-
"Filter must have at least usbVendorId or usbProductId"
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
if (filter.usbVendorId !== void 0) {
|
|
126
|
-
if (!Number.isInteger(filter.usbVendorId) || filter.usbVendorId < 0 || filter.usbVendorId > 65535) {
|
|
127
|
-
throw new SerialError(
|
|
128
|
-
"INVALID_FILTER_OPTIONS" /* INVALID_FILTER_OPTIONS */,
|
|
129
|
-
`Invalid usbVendorId: ${filter.usbVendorId}. Must be an integer between 0 and 65535.`
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (filter.usbProductId !== void 0) {
|
|
134
|
-
if (!Number.isInteger(filter.usbProductId) || filter.usbProductId < 0 || filter.usbProductId > 65535) {
|
|
135
|
-
throw new SerialError(
|
|
136
|
-
"INVALID_FILTER_OPTIONS" /* INVALID_FILTER_OPTIONS */,
|
|
137
|
-
`Invalid usbProductId: ${filter.usbProductId}. Must be an integer between 0 and 65535.`
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return {
|
|
143
|
-
filters: options.filters
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
403
|
// src/session/internal/has-web-serial-support.ts
|
|
148
404
|
function hasWebSerialSupport() {
|
|
149
405
|
return typeof navigator !== "undefined" && "serial" in navigator && navigator.serial !== void 0 && navigator.serial !== null;
|
|
150
406
|
}
|
|
151
407
|
|
|
408
|
+
// src/session/internal/receive-pipeline.ts
|
|
409
|
+
import {
|
|
410
|
+
BehaviorSubject,
|
|
411
|
+
share,
|
|
412
|
+
Subject as Subject2,
|
|
413
|
+
switchMap
|
|
414
|
+
} from "rxjs";
|
|
415
|
+
|
|
416
|
+
// src/errors/serial-error-context.ts
|
|
417
|
+
var CAUSE_CONTEXT_CODES = /* @__PURE__ */ new Set([
|
|
418
|
+
SerialErrorCode.PORT_NOT_AVAILABLE,
|
|
419
|
+
SerialErrorCode.PORT_OPEN_FAILED,
|
|
420
|
+
SerialErrorCode.READ_FAILED,
|
|
421
|
+
SerialErrorCode.WRITE_FAILED,
|
|
422
|
+
SerialErrorCode.CONNECTION_LOST,
|
|
423
|
+
SerialErrorCode.OPERATION_CANCELLED,
|
|
424
|
+
SerialErrorCode.UNKNOWN
|
|
425
|
+
]);
|
|
426
|
+
function isCauseContextCode(code) {
|
|
427
|
+
return CAUSE_CONTEXT_CODES.has(code);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/errors/serial-error.ts
|
|
431
|
+
var ErrorWithCaptureStackTrace = Error;
|
|
432
|
+
var SerialError = class _SerialError extends Error {
|
|
433
|
+
/**
|
|
434
|
+
* Creates a new SerialError instance.
|
|
435
|
+
*
|
|
436
|
+
* @param code - The error code identifying the type of error
|
|
437
|
+
* @param message - A human-readable error message
|
|
438
|
+
* @param originalError - The original error that caused this SerialError, if any
|
|
439
|
+
* @param context - Structured metadata for the error code. When omitted, cause-bearing
|
|
440
|
+
* codes derive `{ cause }` from `originalError`.
|
|
441
|
+
*/
|
|
442
|
+
constructor(code, message, originalError, context) {
|
|
443
|
+
super(message);
|
|
444
|
+
this.name = "SerialError";
|
|
445
|
+
this.code = code;
|
|
446
|
+
if (originalError !== void 0) {
|
|
447
|
+
this.originalError = originalError;
|
|
448
|
+
}
|
|
449
|
+
if (context !== void 0) {
|
|
450
|
+
this.context = context;
|
|
451
|
+
} else if (originalError !== void 0 && isCauseContextCode(code)) {
|
|
452
|
+
this.context = { cause: originalError };
|
|
453
|
+
} else {
|
|
454
|
+
this.context = void 0;
|
|
455
|
+
}
|
|
456
|
+
if (ErrorWithCaptureStackTrace.captureStackTrace) {
|
|
457
|
+
ErrorWithCaptureStackTrace.captureStackTrace(this, _SerialError);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Check if the error matches a specific error code.
|
|
462
|
+
*
|
|
463
|
+
* This is a convenience method for checking the error code without directly
|
|
464
|
+
* comparing the code property. When this method returns `true`, TypeScript
|
|
465
|
+
* narrows `this.code` and `this.context` to the shapes defined for the
|
|
466
|
+
* provided `code` argument.
|
|
467
|
+
*
|
|
468
|
+
* @param code - The error code to check against
|
|
469
|
+
* @returns Type predicate: `true` if this error's code matches the provided
|
|
470
|
+
* code (and `this.code` / `this.context` are narrowed), `false` otherwise
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* ```typescript
|
|
474
|
+
* if (error.is(SerialErrorCode.LINE_BUFFER_OVERFLOW)) {
|
|
475
|
+
* // error.code and error.context.maxChars are narrowed
|
|
476
|
+
* }
|
|
477
|
+
* ```
|
|
478
|
+
*/
|
|
479
|
+
is(code) {
|
|
480
|
+
return this.code === code;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
152
484
|
// src/session/internal/line-buffer.ts
|
|
153
|
-
|
|
154
|
-
|
|
485
|
+
var DEFAULT_LINE_BUFFER_OPTIONS = {
|
|
486
|
+
maxChars: 1048576
|
|
487
|
+
};
|
|
488
|
+
function createLineBuffer(options) {
|
|
489
|
+
const limits = {
|
|
490
|
+
maxChars: brandMaxChars(
|
|
491
|
+
options?.maxChars ?? DEFAULT_LINE_BUFFER_OPTIONS.maxChars
|
|
492
|
+
)
|
|
493
|
+
};
|
|
494
|
+
const tokenizer = createNewlineTokenizer("line");
|
|
155
495
|
const clear = () => {
|
|
156
|
-
|
|
496
|
+
tokenizer.clear();
|
|
157
497
|
};
|
|
158
498
|
const feed = (chunk) => {
|
|
159
|
-
|
|
499
|
+
const events = tokenizer.feed(chunk);
|
|
160
500
|
const out = [];
|
|
161
|
-
for (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
out.push(buffer.slice(0, crlf));
|
|
165
|
-
buffer = buffer.slice(crlf + 2);
|
|
166
|
-
continue;
|
|
501
|
+
for (const event of events) {
|
|
502
|
+
if (event.type === "line") {
|
|
503
|
+
out.push(event.content);
|
|
167
504
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
505
|
+
}
|
|
506
|
+
const overflowed = tokenizer.trimPending(limits.maxChars);
|
|
507
|
+
return { lines: out, overflowed };
|
|
508
|
+
};
|
|
509
|
+
return { feed, clear };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/session/internal/receive-replay-buffer.ts
|
|
513
|
+
import { Observable, Subject } from "rxjs";
|
|
514
|
+
function totalChars(chunks) {
|
|
515
|
+
return chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
516
|
+
}
|
|
517
|
+
function createReceiveReplayBuffer(options) {
|
|
518
|
+
const chunks = [];
|
|
519
|
+
const live$ = new Subject();
|
|
520
|
+
let completed = false;
|
|
521
|
+
const trim = () => {
|
|
522
|
+
let overflowed = false;
|
|
523
|
+
while (chunks.length > options.bufferSize) {
|
|
524
|
+
chunks.shift();
|
|
525
|
+
overflowed = true;
|
|
526
|
+
}
|
|
527
|
+
if (options.maxChars > 0) {
|
|
528
|
+
let chars = totalChars(chunks);
|
|
529
|
+
while (chars > options.maxChars && chunks.length > 1) {
|
|
530
|
+
chars -= chunks.shift().length;
|
|
531
|
+
overflowed = true;
|
|
173
532
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
533
|
+
}
|
|
534
|
+
return overflowed;
|
|
535
|
+
};
|
|
536
|
+
const next = (chunk) => {
|
|
537
|
+
if (completed) {
|
|
538
|
+
return { overflowed: false };
|
|
539
|
+
}
|
|
540
|
+
chunks.push(chunk);
|
|
541
|
+
const overflowed = trim();
|
|
542
|
+
live$.next(chunk);
|
|
543
|
+
return { overflowed };
|
|
544
|
+
};
|
|
545
|
+
const asObservable = () => new Observable((subscriber) => {
|
|
546
|
+
for (const chunk of chunks) {
|
|
547
|
+
subscriber.next(chunk);
|
|
548
|
+
}
|
|
549
|
+
if (completed) {
|
|
550
|
+
subscriber.complete();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
return live$.subscribe(subscriber);
|
|
554
|
+
});
|
|
555
|
+
const complete = () => {
|
|
556
|
+
if (completed) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
completed = true;
|
|
560
|
+
chunks.length = 0;
|
|
561
|
+
live$.complete();
|
|
562
|
+
};
|
|
563
|
+
return { next, asObservable, complete };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/session/internal/receive-pipeline.ts
|
|
567
|
+
function createReceivePipeline(deps) {
|
|
568
|
+
const { resolvedOptions, reportError } = deps;
|
|
569
|
+
const receiveSubject = new Subject2();
|
|
570
|
+
const linesSubject = new Subject2();
|
|
571
|
+
const lineBuffer = createLineBuffer(resolvedOptions.lineBuffer);
|
|
572
|
+
const receive$ = receiveSubject.asObservable();
|
|
573
|
+
const lines$ = linesSubject.asObservable();
|
|
574
|
+
const receiveReplayStream$ = resolvedOptions.receiveReplay.enabled ? new BehaviorSubject(receive$) : null;
|
|
575
|
+
let activeReceiveReplay = null;
|
|
576
|
+
const clearReplay = () => {
|
|
577
|
+
if (receiveReplayStream$) {
|
|
578
|
+
if (activeReceiveReplay) {
|
|
579
|
+
activeReceiveReplay.complete();
|
|
580
|
+
activeReceiveReplay = null;
|
|
179
581
|
}
|
|
180
|
-
|
|
181
|
-
|
|
582
|
+
receiveReplayStream$.next(receive$);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
const startLiveReceiveReplay = () => {
|
|
586
|
+
if (!receiveReplayStream$) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (activeReceiveReplay) {
|
|
590
|
+
activeReceiveReplay.complete();
|
|
591
|
+
activeReceiveReplay = null;
|
|
592
|
+
}
|
|
593
|
+
const buffer = createReceiveReplayBuffer({
|
|
594
|
+
bufferSize: resolvedOptions.receiveReplay.bufferSize,
|
|
595
|
+
maxChars: resolvedOptions.receiveReplay.maxChars
|
|
596
|
+
});
|
|
597
|
+
activeReceiveReplay = buffer;
|
|
598
|
+
receiveReplayStream$.next(buffer.asObservable());
|
|
599
|
+
};
|
|
600
|
+
const receiveReplay$ = receiveReplayStream$ ? receiveReplayStream$.pipe(switchMap((inner) => inner), share()) : receive$;
|
|
601
|
+
const clearLineBuffer = () => {
|
|
602
|
+
lineBuffer.clear();
|
|
603
|
+
};
|
|
604
|
+
const handleChunk = (text) => {
|
|
605
|
+
receiveSubject.next(text);
|
|
606
|
+
if (activeReceiveReplay) {
|
|
607
|
+
const { overflowed: overflowed2 } = activeReceiveReplay.next(text);
|
|
608
|
+
if (overflowed2) {
|
|
609
|
+
reportError(
|
|
610
|
+
new SerialError(
|
|
611
|
+
SerialErrorCode.RECEIVE_REPLAY_BUFFER_OVERFLOW,
|
|
612
|
+
"Receive replay buffer exceeded configured limits; oldest chunks were discarded",
|
|
613
|
+
void 0,
|
|
614
|
+
{
|
|
615
|
+
maxChars: resolvedOptions.receiveReplay.maxChars,
|
|
616
|
+
bufferSize: resolvedOptions.receiveReplay.bufferSize
|
|
617
|
+
}
|
|
618
|
+
),
|
|
619
|
+
{
|
|
620
|
+
fallbackCode: SerialErrorCode.RECEIVE_REPLAY_BUFFER_OVERFLOW
|
|
621
|
+
}
|
|
622
|
+
);
|
|
182
623
|
}
|
|
183
|
-
break;
|
|
184
624
|
}
|
|
185
|
-
|
|
625
|
+
const { lines, overflowed } = lineBuffer.feed(text);
|
|
626
|
+
if (overflowed) {
|
|
627
|
+
reportError(
|
|
628
|
+
new SerialError(
|
|
629
|
+
SerialErrorCode.LINE_BUFFER_OVERFLOW,
|
|
630
|
+
"Line buffer exceeded maxChars; leading data was discarded",
|
|
631
|
+
void 0,
|
|
632
|
+
{ maxChars: resolvedOptions.lineBuffer.maxChars }
|
|
633
|
+
),
|
|
634
|
+
{ fallbackCode: SerialErrorCode.LINE_BUFFER_OVERFLOW }
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
for (const line of lines) {
|
|
638
|
+
linesSubject.next(line);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
const complete = () => {
|
|
642
|
+
receiveSubject.complete();
|
|
643
|
+
linesSubject.complete();
|
|
644
|
+
receiveReplayStream$?.complete();
|
|
645
|
+
};
|
|
646
|
+
return {
|
|
647
|
+
receive$,
|
|
648
|
+
lines$,
|
|
649
|
+
receiveReplay$,
|
|
650
|
+
clearReplay,
|
|
651
|
+
startLiveReceiveReplay,
|
|
652
|
+
clearLineBuffer,
|
|
653
|
+
handleChunk,
|
|
654
|
+
complete
|
|
186
655
|
};
|
|
187
|
-
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/session/internal/error-severity.ts
|
|
659
|
+
var ERROR_SEVERITY = {
|
|
660
|
+
[SerialErrorCode.READ_FAILED]: "fatal",
|
|
661
|
+
[SerialErrorCode.CONNECTION_LOST]: "fatal",
|
|
662
|
+
[SerialErrorCode.PORT_OPEN_FAILED]: "fatal",
|
|
663
|
+
[SerialErrorCode.OPERATION_CANCELLED]: "fatal",
|
|
664
|
+
[SerialErrorCode.UNKNOWN]: "fatal",
|
|
665
|
+
[SerialErrorCode.LINE_BUFFER_OVERFLOW]: "non-fatal",
|
|
666
|
+
[SerialErrorCode.RECEIVE_REPLAY_BUFFER_OVERFLOW]: "non-fatal",
|
|
667
|
+
[SerialErrorCode.BROWSER_NOT_SUPPORTED]: "non-fatal",
|
|
668
|
+
[SerialErrorCode.PORT_ALREADY_OPEN]: "non-fatal",
|
|
669
|
+
[SerialErrorCode.PORT_NOT_OPEN]: "non-fatal",
|
|
670
|
+
[SerialErrorCode.WRITE_FAILED]: "non-fatal"
|
|
671
|
+
};
|
|
672
|
+
function resolveErrorSeverity(code) {
|
|
673
|
+
if (code in ERROR_SEVERITY) {
|
|
674
|
+
return ERROR_SEVERITY[code];
|
|
675
|
+
}
|
|
676
|
+
return "fatal";
|
|
188
677
|
}
|
|
189
678
|
|
|
190
679
|
// src/session/normalize-serial-error.ts
|
|
@@ -197,7 +686,7 @@ function normalizeSerialError(error, options) {
|
|
|
197
686
|
const prefix = options.messagePrefix ?? DEFAULT_MESSAGE_PREFIX;
|
|
198
687
|
if (isDomExceptionWithName(error, "NotFoundError")) {
|
|
199
688
|
return new SerialError(
|
|
200
|
-
|
|
689
|
+
SerialErrorCode.OPERATION_CANCELLED,
|
|
201
690
|
"Port selection was cancelled by the user",
|
|
202
691
|
error
|
|
203
692
|
);
|
|
@@ -210,8 +699,195 @@ function normalizeSerialError(error, options) {
|
|
|
210
699
|
);
|
|
211
700
|
}
|
|
212
701
|
|
|
702
|
+
// src/session/session-runtime.ts
|
|
703
|
+
import { BehaviorSubject as BehaviorSubject2 } from "rxjs";
|
|
704
|
+
|
|
705
|
+
// src/session/serial-session-state.ts
|
|
706
|
+
var SerialSessionStatus = {
|
|
707
|
+
Idle: "idle",
|
|
708
|
+
Connecting: "connecting",
|
|
709
|
+
Connected: "connected",
|
|
710
|
+
Disconnecting: "disconnecting",
|
|
711
|
+
Unsupported: "unsupported",
|
|
712
|
+
Error: "error",
|
|
713
|
+
Disposed: "disposed"
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// src/session/session-runtime.ts
|
|
717
|
+
var S = SerialSessionStatus;
|
|
718
|
+
var ALLOWED_TRANSITIONS = {
|
|
719
|
+
[S.Idle]: [S.Connecting, S.Error, S.Disposed],
|
|
720
|
+
[S.Connecting]: [S.Connected, S.Error, S.Idle, S.Disposed],
|
|
721
|
+
[S.Connected]: [S.Disconnecting, S.Error, S.Disposed],
|
|
722
|
+
[S.Disconnecting]: [S.Idle, S.Error, S.Disposed],
|
|
723
|
+
[S.Error]: [S.Idle, S.Connecting, S.Disposed],
|
|
724
|
+
[S.Unsupported]: [S.Disposed],
|
|
725
|
+
[S.Disposed]: []
|
|
726
|
+
};
|
|
727
|
+
function runtimeToSessionStatus(runtime) {
|
|
728
|
+
return runtime.status;
|
|
729
|
+
}
|
|
730
|
+
function runtimeToPublicState(runtime) {
|
|
731
|
+
switch (runtime.status) {
|
|
732
|
+
case S.Idle:
|
|
733
|
+
return { status: S.Idle };
|
|
734
|
+
case S.Connecting:
|
|
735
|
+
return { status: S.Connecting };
|
|
736
|
+
case S.Connected:
|
|
737
|
+
return { status: S.Connected, portInfo: runtime.port.getInfo() };
|
|
738
|
+
case S.Disconnecting:
|
|
739
|
+
return { status: S.Disconnecting };
|
|
740
|
+
case S.Unsupported:
|
|
741
|
+
return { status: S.Unsupported };
|
|
742
|
+
case S.Error:
|
|
743
|
+
return { status: S.Error, error: runtime.error };
|
|
744
|
+
case S.Disposed:
|
|
745
|
+
return { status: S.Disposed };
|
|
746
|
+
default:
|
|
747
|
+
return assertNeverRuntime(runtime);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function isValidTransition(from, to) {
|
|
751
|
+
if (from === to) {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
return ALLOWED_TRANSITIONS[from].includes(
|
|
755
|
+
to
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
function createIdleRuntime() {
|
|
759
|
+
return { status: S.Idle };
|
|
760
|
+
}
|
|
761
|
+
function createConnectingRuntime(cancel) {
|
|
762
|
+
return { status: S.Connecting, cancel };
|
|
763
|
+
}
|
|
764
|
+
function createConnectedRuntime(port, pump) {
|
|
765
|
+
return { status: S.Connected, port, pump };
|
|
766
|
+
}
|
|
767
|
+
function createDisconnectingRuntime(port) {
|
|
768
|
+
return { status: S.Disconnecting, port };
|
|
769
|
+
}
|
|
770
|
+
function createErrorRuntime(error) {
|
|
771
|
+
return { status: S.Error, error };
|
|
772
|
+
}
|
|
773
|
+
function createUnsupportedRuntime() {
|
|
774
|
+
return { status: S.Unsupported };
|
|
775
|
+
}
|
|
776
|
+
function createDisposedRuntime() {
|
|
777
|
+
return { status: S.Disposed };
|
|
778
|
+
}
|
|
779
|
+
function createInitialRuntime(supported) {
|
|
780
|
+
return supported ? createIdleRuntime() : createUnsupportedRuntime();
|
|
781
|
+
}
|
|
782
|
+
function getRuntimePort(runtime) {
|
|
783
|
+
switch (runtime.status) {
|
|
784
|
+
case S.Connected:
|
|
785
|
+
return runtime.port;
|
|
786
|
+
case S.Disconnecting:
|
|
787
|
+
return runtime.port;
|
|
788
|
+
default:
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function getRuntimePump(runtime) {
|
|
793
|
+
if (runtime.status === S.Connected) {
|
|
794
|
+
return runtime.pump;
|
|
795
|
+
}
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
function createSessionRuntimeController(initial) {
|
|
799
|
+
let runtime = initial;
|
|
800
|
+
const subject = new BehaviorSubject2(
|
|
801
|
+
runtimeToPublicState(initial)
|
|
802
|
+
);
|
|
803
|
+
const transition = (next) => {
|
|
804
|
+
const from = runtimeToSessionStatus(runtime);
|
|
805
|
+
const to = runtimeToSessionStatus(next);
|
|
806
|
+
if (from === to) {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
if (!isValidTransition(from, to)) {
|
|
810
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
811
|
+
console.warn(
|
|
812
|
+
`[web-serial-rxjs] Ignoring invalid SerialSession transition ${from} -> ${to}`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
runtime = next;
|
|
818
|
+
subject.next(runtimeToPublicState(next));
|
|
819
|
+
return true;
|
|
820
|
+
};
|
|
821
|
+
return {
|
|
822
|
+
get runtime() {
|
|
823
|
+
return runtime;
|
|
824
|
+
},
|
|
825
|
+
get status() {
|
|
826
|
+
return runtimeToSessionStatus(runtime);
|
|
827
|
+
},
|
|
828
|
+
get state$() {
|
|
829
|
+
return subject.asObservable();
|
|
830
|
+
},
|
|
831
|
+
transition,
|
|
832
|
+
complete() {
|
|
833
|
+
subject.complete();
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
function assertNeverRuntime(value) {
|
|
838
|
+
return assertNever(value);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/session/internal/session-error-reporter.ts
|
|
842
|
+
function createSessionErrorReporter(deps) {
|
|
843
|
+
const {
|
|
844
|
+
controller,
|
|
845
|
+
errorsSubject,
|
|
846
|
+
sendQueue,
|
|
847
|
+
isDisposed,
|
|
848
|
+
updatePortInfo,
|
|
849
|
+
teardownPump,
|
|
850
|
+
closePortSafely
|
|
851
|
+
} = deps;
|
|
852
|
+
const createDisposedError = () => new SerialError(
|
|
853
|
+
SerialErrorCode.SESSION_DISPOSED,
|
|
854
|
+
"SerialSession has been disposed"
|
|
855
|
+
);
|
|
856
|
+
const reportError = (error, options) => {
|
|
857
|
+
const serialError = normalizeSerialError(error, options);
|
|
858
|
+
if (isDisposed()) {
|
|
859
|
+
return serialError;
|
|
860
|
+
}
|
|
861
|
+
errorsSubject.next(serialError);
|
|
862
|
+
if (resolveErrorSeverity(serialError.code) === "fatal") {
|
|
863
|
+
const runtime = controller.runtime;
|
|
864
|
+
const portToClose = getRuntimePort(runtime);
|
|
865
|
+
const pump = getRuntimePump(runtime);
|
|
866
|
+
controller.transition(createErrorRuntime(serialError));
|
|
867
|
+
sendQueue.clear();
|
|
868
|
+
updatePortInfo(null);
|
|
869
|
+
void teardownPump(pump).then(() => closePortSafely(portToClose));
|
|
870
|
+
}
|
|
871
|
+
return serialError;
|
|
872
|
+
};
|
|
873
|
+
return { reportError, createDisposedError };
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/session/internal/session-lifecycle.ts
|
|
877
|
+
import { Observable as Observable3 } from "rxjs";
|
|
878
|
+
|
|
879
|
+
// src/session/internal/build-request-options.ts
|
|
880
|
+
function buildRequestOptions(options) {
|
|
881
|
+
if (!options?.filters || options.filters.length === 0) {
|
|
882
|
+
return void 0;
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
filters: options.filters
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
213
889
|
// src/session/read-pump.ts
|
|
214
|
-
function createReadPump(port, { onChunk, onError }) {
|
|
890
|
+
function createReadPump(port, { onChunk, onError, onDone }) {
|
|
215
891
|
let reader = null;
|
|
216
892
|
let running = false;
|
|
217
893
|
let stopped = false;
|
|
@@ -233,6 +909,9 @@ function createReadPump(port, { onChunk, onError }) {
|
|
|
233
909
|
while (!stopped) {
|
|
234
910
|
const { done, value } = await reader.read();
|
|
235
911
|
if (done) {
|
|
912
|
+
if (!stopped) {
|
|
913
|
+
onDone?.();
|
|
914
|
+
}
|
|
236
915
|
break;
|
|
237
916
|
}
|
|
238
917
|
if (value && value.byteLength > 0) {
|
|
@@ -252,63 +931,342 @@ function createReadPump(port, { onChunk, onError }) {
|
|
|
252
931
|
if (!stopped) {
|
|
253
932
|
onError(
|
|
254
933
|
normalizeSerialError(error, {
|
|
255
|
-
fallbackCode:
|
|
934
|
+
fallbackCode: SerialErrorCode.READ_FAILED,
|
|
256
935
|
messagePrefix: "Read pump failed"
|
|
257
936
|
})
|
|
258
937
|
);
|
|
259
938
|
}
|
|
260
939
|
} finally {
|
|
261
|
-
running = false;
|
|
262
|
-
releaseReader();
|
|
940
|
+
running = false;
|
|
941
|
+
releaseReader();
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
return {
|
|
945
|
+
start() {
|
|
946
|
+
if (running || stopped) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const stream = port.readable;
|
|
950
|
+
if (!stream) {
|
|
951
|
+
stopped = true;
|
|
952
|
+
onError(
|
|
953
|
+
new SerialError(
|
|
954
|
+
SerialErrorCode.CONNECTION_LOST,
|
|
955
|
+
"Read pump failed: port.readable is not available"
|
|
956
|
+
)
|
|
957
|
+
);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
void pump(stream);
|
|
961
|
+
},
|
|
962
|
+
async stop() {
|
|
963
|
+
if (stopped) {
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
stopped = true;
|
|
967
|
+
if (!reader) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
await reader.cancel();
|
|
972
|
+
} catch {
|
|
973
|
+
} finally {
|
|
974
|
+
releaseReader();
|
|
975
|
+
}
|
|
976
|
+
},
|
|
977
|
+
get isRunning() {
|
|
978
|
+
return running;
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/session/internal/session-lifecycle.ts
|
|
984
|
+
function createSessionLifecycle(deps) {
|
|
985
|
+
const {
|
|
986
|
+
controller,
|
|
987
|
+
resolvedOptions,
|
|
988
|
+
sendQueue,
|
|
989
|
+
receivePipeline,
|
|
990
|
+
portInfoSubject,
|
|
991
|
+
errorsSubject,
|
|
992
|
+
isDisposed,
|
|
993
|
+
reportError,
|
|
994
|
+
createDisposedError
|
|
995
|
+
} = deps;
|
|
996
|
+
const updatePortInfo = (port) => {
|
|
997
|
+
portInfoSubject.next(port ? port.getInfo() : null);
|
|
998
|
+
};
|
|
999
|
+
const teardownPump = async (pump) => {
|
|
1000
|
+
receivePipeline.clearReplay();
|
|
1001
|
+
receivePipeline.clearLineBuffer();
|
|
1002
|
+
if (pump) {
|
|
1003
|
+
await pump.stop();
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
const closePortSafely = async (port) => {
|
|
1007
|
+
if (!port) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
try {
|
|
1011
|
+
await port.close();
|
|
1012
|
+
} catch {
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
const teardownFromSnapshot = async (snapshot) => {
|
|
1016
|
+
if (snapshot.status === SerialSessionStatus.Connecting) {
|
|
1017
|
+
snapshot.cancel();
|
|
1018
|
+
}
|
|
1019
|
+
sendQueue.clear();
|
|
1020
|
+
if (snapshot.status === SerialSessionStatus.Connected || snapshot.status === SerialSessionStatus.Disconnecting || snapshot.status === SerialSessionStatus.Error) {
|
|
1021
|
+
const portToClose = getRuntimePort(snapshot);
|
|
1022
|
+
const pump = getRuntimePump(snapshot);
|
|
1023
|
+
await teardownPump(pump);
|
|
1024
|
+
await closePortSafely(portToClose);
|
|
1025
|
+
updatePortInfo(null);
|
|
1026
|
+
}
|
|
1027
|
+
receivePipeline.clearLineBuffer();
|
|
1028
|
+
};
|
|
1029
|
+
const completeSubjects = () => {
|
|
1030
|
+
controller.complete();
|
|
1031
|
+
errorsSubject.complete();
|
|
1032
|
+
receivePipeline.complete();
|
|
1033
|
+
portInfoSubject.complete();
|
|
1034
|
+
};
|
|
1035
|
+
const dispose$ = () => new Observable3((subscriber) => {
|
|
1036
|
+
if (isDisposed()) {
|
|
1037
|
+
subscriber.next();
|
|
1038
|
+
subscriber.complete();
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const snapshot = controller.runtime;
|
|
1042
|
+
controller.transition(createDisposedRuntime());
|
|
1043
|
+
const run = async () => {
|
|
1044
|
+
try {
|
|
1045
|
+
await teardownFromSnapshot(snapshot);
|
|
1046
|
+
completeSubjects();
|
|
1047
|
+
subscriber.next();
|
|
1048
|
+
subscriber.complete();
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
const serialError = normalizeSerialError(error, {
|
|
1051
|
+
fallbackCode: SerialErrorCode.UNKNOWN,
|
|
1052
|
+
messagePrefix: "Unexpected dispose failure"
|
|
1053
|
+
});
|
|
1054
|
+
subscriber.error(serialError);
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
void run();
|
|
1058
|
+
});
|
|
1059
|
+
const writeToPort = async (payload) => {
|
|
1060
|
+
const runtime = controller.runtime;
|
|
1061
|
+
if (runtime.status !== SerialSessionStatus.Connected || !runtime.port.writable) {
|
|
1062
|
+
throw new SerialError(
|
|
1063
|
+
SerialErrorCode.PORT_NOT_OPEN,
|
|
1064
|
+
"Cannot send data while session is not connected"
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
const writer = runtime.port.writable.getWriter();
|
|
1068
|
+
try {
|
|
1069
|
+
await writer.write(payload);
|
|
1070
|
+
} finally {
|
|
1071
|
+
try {
|
|
1072
|
+
writer.releaseLock();
|
|
1073
|
+
} catch {
|
|
1074
|
+
}
|
|
263
1075
|
}
|
|
264
1076
|
};
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
1077
|
+
const connect$ = () => new Observable3((subscriber) => {
|
|
1078
|
+
if (isDisposed()) {
|
|
1079
|
+
subscriber.error(createDisposedError());
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (!hasWebSerialSupport()) {
|
|
1083
|
+
const error = reportError(
|
|
1084
|
+
new SerialError(
|
|
1085
|
+
SerialErrorCode.BROWSER_NOT_SUPPORTED,
|
|
1086
|
+
"Web Serial API is not supported in this environment"
|
|
1087
|
+
),
|
|
1088
|
+
{ fallbackCode: SerialErrorCode.BROWSER_NOT_SUPPORTED }
|
|
1089
|
+
);
|
|
1090
|
+
subscriber.error(error);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const current = controller.status;
|
|
1094
|
+
if (current !== SerialSessionStatus.Idle && current !== SerialSessionStatus.Error) {
|
|
1095
|
+
const error = reportError(
|
|
1096
|
+
new SerialError(
|
|
1097
|
+
SerialErrorCode.PORT_ALREADY_OPEN,
|
|
1098
|
+
`Cannot connect while session state is '${current}'`
|
|
1099
|
+
),
|
|
1100
|
+
{ fallbackCode: SerialErrorCode.PORT_ALREADY_OPEN }
|
|
1101
|
+
);
|
|
1102
|
+
subscriber.error(error);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
let cancelled = false;
|
|
1106
|
+
const cancelInFlightConnect = () => {
|
|
1107
|
+
cancelled = true;
|
|
1108
|
+
if (controller.status === SerialSessionStatus.Connecting) {
|
|
1109
|
+
controller.transition(createIdleRuntime());
|
|
269
1110
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
1111
|
+
};
|
|
1112
|
+
controller.transition(createConnectingRuntime(cancelInFlightConnect));
|
|
1113
|
+
const run = async () => {
|
|
1114
|
+
let selectedPort = null;
|
|
1115
|
+
try {
|
|
1116
|
+
selectedPort = await navigator.serial.requestPort(
|
|
1117
|
+
buildRequestOptions(resolvedOptions)
|
|
278
1118
|
);
|
|
1119
|
+
await selectedPort.open({
|
|
1120
|
+
baudRate: resolvedOptions.baudRate,
|
|
1121
|
+
dataBits: resolvedOptions.dataBits,
|
|
1122
|
+
stopBits: resolvedOptions.stopBits,
|
|
1123
|
+
parity: resolvedOptions.parity,
|
|
1124
|
+
bufferSize: resolvedOptions.bufferSize,
|
|
1125
|
+
flowControl: resolvedOptions.flowControl
|
|
1126
|
+
});
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
if (selectedPort) {
|
|
1129
|
+
await closePortSafely(selectedPort);
|
|
1130
|
+
}
|
|
1131
|
+
const serialError = reportError(error, {
|
|
1132
|
+
fallbackCode: SerialErrorCode.PORT_OPEN_FAILED,
|
|
1133
|
+
messagePrefix: "Failed to open port"
|
|
1134
|
+
});
|
|
1135
|
+
if (!cancelled) {
|
|
1136
|
+
subscriber.error(serialError);
|
|
1137
|
+
}
|
|
279
1138
|
return;
|
|
280
1139
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
async stop() {
|
|
284
|
-
if (stopped) {
|
|
1140
|
+
if (cancelled || isDisposed()) {
|
|
1141
|
+
await closePortSafely(selectedPort);
|
|
285
1142
|
return;
|
|
286
1143
|
}
|
|
287
|
-
|
|
288
|
-
if (
|
|
1144
|
+
receivePipeline.clearLineBuffer();
|
|
1145
|
+
if (resolvedOptions.receiveReplay.enabled) {
|
|
1146
|
+
receivePipeline.startLiveReceiveReplay();
|
|
1147
|
+
}
|
|
1148
|
+
const pump = createReadPump(selectedPort, {
|
|
1149
|
+
onChunk: receivePipeline.handleChunk,
|
|
1150
|
+
onError: (pumpError) => reportError(pumpError, {
|
|
1151
|
+
fallbackCode: SerialErrorCode.READ_FAILED,
|
|
1152
|
+
messagePrefix: "Read pump failed"
|
|
1153
|
+
}),
|
|
1154
|
+
onDone: () => {
|
|
1155
|
+
if (controller.status !== SerialSessionStatus.Connected) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
reportError(
|
|
1159
|
+
new SerialError(
|
|
1160
|
+
SerialErrorCode.CONNECTION_LOST,
|
|
1161
|
+
"Read pump ended unexpectedly: stream closed while connected"
|
|
1162
|
+
),
|
|
1163
|
+
{
|
|
1164
|
+
fallbackCode: SerialErrorCode.CONNECTION_LOST
|
|
1165
|
+
}
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
pump.start();
|
|
1170
|
+
sendQueue.clear();
|
|
1171
|
+
if (isDisposed()) {
|
|
1172
|
+
await teardownPump(pump);
|
|
1173
|
+
await closePortSafely(selectedPort);
|
|
289
1174
|
return;
|
|
290
1175
|
}
|
|
1176
|
+
controller.transition(createConnectedRuntime(selectedPort, pump));
|
|
1177
|
+
updatePortInfo(selectedPort);
|
|
1178
|
+
subscriber.next();
|
|
1179
|
+
subscriber.complete();
|
|
1180
|
+
};
|
|
1181
|
+
void run();
|
|
1182
|
+
return () => {
|
|
1183
|
+
cancelInFlightConnect();
|
|
1184
|
+
};
|
|
1185
|
+
});
|
|
1186
|
+
const disconnect$ = () => new Observable3((subscriber) => {
|
|
1187
|
+
if (isDisposed()) {
|
|
1188
|
+
subscriber.next();
|
|
1189
|
+
subscriber.complete();
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
const runtime = controller.runtime;
|
|
1193
|
+
if (runtime.status === SerialSessionStatus.Idle || runtime.status === SerialSessionStatus.Unsupported || runtime.status === SerialSessionStatus.Disconnecting) {
|
|
1194
|
+
subscriber.next();
|
|
1195
|
+
subscriber.complete();
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
if (runtime.status === SerialSessionStatus.Connecting) {
|
|
1199
|
+
runtime.cancel();
|
|
1200
|
+
subscriber.next();
|
|
1201
|
+
subscriber.complete();
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
if (runtime.status !== SerialSessionStatus.Connected && runtime.status !== SerialSessionStatus.Error) {
|
|
1205
|
+
const error = reportError(
|
|
1206
|
+
new SerialError(
|
|
1207
|
+
SerialErrorCode.PORT_NOT_OPEN,
|
|
1208
|
+
`Cannot disconnect while session state is '${runtime.status}'`
|
|
1209
|
+
),
|
|
1210
|
+
{ fallbackCode: SerialErrorCode.PORT_NOT_OPEN }
|
|
1211
|
+
);
|
|
1212
|
+
subscriber.error(error);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
const portToClose = getRuntimePort(runtime);
|
|
1216
|
+
controller.transition(createDisconnectingRuntime(portToClose));
|
|
1217
|
+
sendQueue.clear();
|
|
1218
|
+
const run = async () => {
|
|
291
1219
|
try {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
1220
|
+
const pump = getRuntimePump(controller.runtime);
|
|
1221
|
+
await teardownPump(pump);
|
|
1222
|
+
if (portToClose) {
|
|
1223
|
+
try {
|
|
1224
|
+
await portToClose.close();
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
updatePortInfo(null);
|
|
1227
|
+
const serialError = reportError(error, {
|
|
1228
|
+
fallbackCode: SerialErrorCode.CONNECTION_LOST,
|
|
1229
|
+
messagePrefix: "Failed to close port"
|
|
1230
|
+
});
|
|
1231
|
+
subscriber.error(serialError);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
updatePortInfo(null);
|
|
1236
|
+
if (!isDisposed()) {
|
|
1237
|
+
controller.transition(createIdleRuntime());
|
|
1238
|
+
}
|
|
1239
|
+
subscriber.next();
|
|
1240
|
+
subscriber.complete();
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
const serialError = reportError(error, {
|
|
1243
|
+
fallbackCode: SerialErrorCode.UNKNOWN,
|
|
1244
|
+
messagePrefix: "Unexpected disconnect failure"
|
|
1245
|
+
});
|
|
1246
|
+
subscriber.error(serialError);
|
|
296
1247
|
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
1248
|
+
};
|
|
1249
|
+
void run();
|
|
1250
|
+
});
|
|
1251
|
+
return {
|
|
1252
|
+
connect$,
|
|
1253
|
+
disconnect$,
|
|
1254
|
+
dispose$,
|
|
1255
|
+
writeToPort,
|
|
1256
|
+
teardownPump,
|
|
1257
|
+
closePortSafely,
|
|
1258
|
+
updatePortInfo
|
|
301
1259
|
};
|
|
302
1260
|
}
|
|
303
1261
|
|
|
304
1262
|
// src/session/send-queue.ts
|
|
305
|
-
import { Observable, defer } from "rxjs";
|
|
1263
|
+
import { Observable as Observable4, defer } from "rxjs";
|
|
306
1264
|
function createSendQueue() {
|
|
307
1265
|
let chain = Promise.resolve();
|
|
308
1266
|
return {
|
|
309
1267
|
enqueue(operation) {
|
|
310
1268
|
return defer(
|
|
311
|
-
() => new
|
|
1269
|
+
() => new Observable4((subscriber) => {
|
|
312
1270
|
let cancelled = false;
|
|
313
1271
|
const run = async () => {
|
|
314
1272
|
try {
|
|
@@ -340,373 +1298,243 @@ function createSendQueue() {
|
|
|
340
1298
|
};
|
|
341
1299
|
}
|
|
342
1300
|
|
|
1301
|
+
// src/session/internal/validate-serial-port-filters.ts
|
|
1302
|
+
function validateSerialPortFilters(filters) {
|
|
1303
|
+
if (!filters || filters.length === 0) {
|
|
1304
|
+
return filters;
|
|
1305
|
+
}
|
|
1306
|
+
for (const filter of filters) {
|
|
1307
|
+
if (!filter.usbVendorId && !filter.usbProductId) {
|
|
1308
|
+
throw new SerialError(
|
|
1309
|
+
SerialErrorCode.INVALID_FILTER_OPTIONS,
|
|
1310
|
+
"Filter must have at least usbVendorId or usbProductId"
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
if (filter.usbVendorId !== void 0) {
|
|
1314
|
+
if (!Number.isInteger(filter.usbVendorId) || filter.usbVendorId < 0 || filter.usbVendorId > 65535) {
|
|
1315
|
+
throw new SerialError(
|
|
1316
|
+
SerialErrorCode.INVALID_FILTER_OPTIONS,
|
|
1317
|
+
`Invalid usbVendorId: ${filter.usbVendorId}. Must be an integer between 0 and 65535.`
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (filter.usbProductId !== void 0) {
|
|
1322
|
+
if (!Number.isInteger(filter.usbProductId) || filter.usbProductId < 0 || filter.usbProductId > 65535) {
|
|
1323
|
+
throw new SerialError(
|
|
1324
|
+
SerialErrorCode.INVALID_FILTER_OPTIONS,
|
|
1325
|
+
`Invalid usbProductId: ${filter.usbProductId}. Must be an integer between 0 and 65535.`
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return filters;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
343
1333
|
// src/session/serial-session-options.ts
|
|
1334
|
+
var MAX_RECEIVE_REPLAY_BUFFER_SIZE = 65536;
|
|
1335
|
+
var MAX_RECEIVE_REPLAY_MAX_CHARS = 1048576;
|
|
344
1336
|
var DEFAULT_RECEIVE_REPLAY = {
|
|
345
1337
|
enabled: false,
|
|
346
|
-
bufferSize: 512
|
|
1338
|
+
bufferSize: 512,
|
|
1339
|
+
maxChars: 0
|
|
347
1340
|
};
|
|
1341
|
+
function resolveReceiveReplayOptions(options) {
|
|
1342
|
+
const merged = {
|
|
1343
|
+
...DEFAULT_RECEIVE_REPLAY,
|
|
1344
|
+
...options
|
|
1345
|
+
};
|
|
1346
|
+
const { bufferSize, maxChars } = merged;
|
|
1347
|
+
if (!Number.isSafeInteger(bufferSize) || bufferSize < 1 || bufferSize > MAX_RECEIVE_REPLAY_BUFFER_SIZE) {
|
|
1348
|
+
throw new SerialError(
|
|
1349
|
+
SerialErrorCode.INVALID_RECEIVE_REPLAY_OPTIONS,
|
|
1350
|
+
`Invalid receiveReplay.bufferSize: ${bufferSize}. Must be a safe integer between 1 and ${MAX_RECEIVE_REPLAY_BUFFER_SIZE}.`
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
if (!Number.isSafeInteger(maxChars) || maxChars < 0 || maxChars > MAX_RECEIVE_REPLAY_MAX_CHARS) {
|
|
1354
|
+
throw new SerialError(
|
|
1355
|
+
SerialErrorCode.INVALID_RECEIVE_REPLAY_OPTIONS,
|
|
1356
|
+
`Invalid receiveReplay.maxChars: ${maxChars}. Must be a safe integer between 0 and ${MAX_RECEIVE_REPLAY_MAX_CHARS}.`
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
return {
|
|
1360
|
+
enabled: merged.enabled,
|
|
1361
|
+
bufferSize: brandReceiveReplayBufferSize(bufferSize),
|
|
1362
|
+
maxChars: brandMaxChars(maxChars)
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
function resolveTerminalBufferOptions(options) {
|
|
1366
|
+
const merged = {
|
|
1367
|
+
...DEFAULT_TERMINAL_BUFFER_OPTIONS,
|
|
1368
|
+
...options
|
|
1369
|
+
};
|
|
1370
|
+
const { maxLines, maxChars } = merged;
|
|
1371
|
+
if (!Number.isSafeInteger(maxLines) || maxLines < 0) {
|
|
1372
|
+
throw new SerialError(
|
|
1373
|
+
SerialErrorCode.INVALID_TERMINAL_BUFFER_OPTIONS,
|
|
1374
|
+
`Invalid terminalBuffer.maxLines: ${maxLines}. Must be a safe integer >= 0.`
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
if (!Number.isSafeInteger(maxChars) || maxChars < 0) {
|
|
1378
|
+
throw new SerialError(
|
|
1379
|
+
SerialErrorCode.INVALID_TERMINAL_BUFFER_OPTIONS,
|
|
1380
|
+
`Invalid terminalBuffer.maxChars: ${maxChars}. Must be a safe integer >= 0.`
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
return {
|
|
1384
|
+
maxLines: brandMaxLines(maxLines),
|
|
1385
|
+
maxChars: brandMaxChars(maxChars)
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function resolveLineBufferOptions(options) {
|
|
1389
|
+
const merged = {
|
|
1390
|
+
...DEFAULT_LINE_BUFFER_OPTIONS,
|
|
1391
|
+
...options
|
|
1392
|
+
};
|
|
1393
|
+
const { maxChars } = merged;
|
|
1394
|
+
if (!Number.isSafeInteger(maxChars) || maxChars < 0) {
|
|
1395
|
+
throw new SerialError(
|
|
1396
|
+
SerialErrorCode.INVALID_LINE_BUFFER_OPTIONS,
|
|
1397
|
+
`Invalid lineBuffer.maxChars: ${maxChars}. Must be a safe integer >= 0.`
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
return {
|
|
1401
|
+
maxChars: brandMaxChars(maxChars)
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
348
1404
|
var DEFAULT_SERIAL_SESSION_OPTIONS = {
|
|
349
|
-
baudRate: 9600,
|
|
1405
|
+
baudRate: brandBaudRate(9600),
|
|
350
1406
|
dataBits: 8,
|
|
351
1407
|
stopBits: 1,
|
|
352
1408
|
parity: "none",
|
|
353
|
-
bufferSize: 255,
|
|
1409
|
+
bufferSize: brandSerialPortBufferSize(255),
|
|
354
1410
|
flowControl: "none",
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
// src/session/serial-session-state.ts
|
|
360
|
-
var SerialSessionState = {
|
|
361
|
-
Idle: "idle",
|
|
362
|
-
Connecting: "connecting",
|
|
363
|
-
Connected: "connected",
|
|
364
|
-
Disconnecting: "disconnecting",
|
|
365
|
-
Unsupported: "unsupported",
|
|
366
|
-
Error: "error"
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
// src/session/session-state-machine.ts
|
|
370
|
-
import { BehaviorSubject } from "rxjs";
|
|
371
|
-
var S = SerialSessionState;
|
|
372
|
-
var ALLOWED_TRANSITIONS = {
|
|
373
|
-
[S.Idle]: [S.Connecting, S.Error],
|
|
374
|
-
[S.Connecting]: [S.Connected, S.Error, S.Idle],
|
|
375
|
-
[S.Connected]: [S.Disconnecting, S.Error],
|
|
376
|
-
[S.Disconnecting]: [S.Idle, S.Error],
|
|
377
|
-
[S.Error]: [S.Idle, S.Connecting],
|
|
378
|
-
[S.Unsupported]: []
|
|
1411
|
+
receiveReplay: resolveReceiveReplayOptions(),
|
|
1412
|
+
terminalBuffer: resolveTerminalBufferOptions(),
|
|
1413
|
+
lineBuffer: resolveLineBufferOptions()
|
|
379
1414
|
};
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
toDisconnecting() {
|
|
397
|
-
return this.transition(S.Disconnecting);
|
|
398
|
-
}
|
|
399
|
-
toIdle() {
|
|
400
|
-
return this.transition(S.Idle);
|
|
401
|
-
}
|
|
402
|
-
toError() {
|
|
403
|
-
return this.transition(S.Error);
|
|
404
|
-
}
|
|
405
|
-
toUnsupported() {
|
|
406
|
-
return this.transition(S.Unsupported);
|
|
407
|
-
}
|
|
408
|
-
complete() {
|
|
409
|
-
this.subject.complete();
|
|
410
|
-
}
|
|
411
|
-
transition(next) {
|
|
412
|
-
const current = this.subject.getValue();
|
|
413
|
-
if (current === next) {
|
|
414
|
-
return false;
|
|
415
|
-
}
|
|
416
|
-
const allowed = ALLOWED_TRANSITIONS[current];
|
|
417
|
-
if (!allowed.includes(next)) {
|
|
418
|
-
if (typeof console !== "undefined" && console.warn) {
|
|
419
|
-
console.warn(
|
|
420
|
-
`[web-serial-rxjs] Ignoring invalid SerialSession transition ${current} -> ${next}`
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
this.subject.next(next);
|
|
426
|
-
return true;
|
|
1415
|
+
function resolveConnectionOptions(options) {
|
|
1416
|
+
const merged = {
|
|
1417
|
+
baudRate: DEFAULT_SERIAL_SESSION_OPTIONS.baudRate,
|
|
1418
|
+
dataBits: DEFAULT_SERIAL_SESSION_OPTIONS.dataBits,
|
|
1419
|
+
stopBits: DEFAULT_SERIAL_SESSION_OPTIONS.stopBits,
|
|
1420
|
+
parity: DEFAULT_SERIAL_SESSION_OPTIONS.parity,
|
|
1421
|
+
bufferSize: DEFAULT_SERIAL_SESSION_OPTIONS.bufferSize,
|
|
1422
|
+
flowControl: DEFAULT_SERIAL_SESSION_OPTIONS.flowControl,
|
|
1423
|
+
...options
|
|
1424
|
+
};
|
|
1425
|
+
const { baudRate, bufferSize } = merged;
|
|
1426
|
+
if (!Number.isSafeInteger(baudRate) || baudRate <= 0) {
|
|
1427
|
+
throw new SerialError(
|
|
1428
|
+
SerialErrorCode.INVALID_CONNECTION_OPTIONS,
|
|
1429
|
+
`Invalid baudRate: ${baudRate}. Must be a safe integer > 0.`
|
|
1430
|
+
);
|
|
427
1431
|
}
|
|
428
|
-
|
|
1432
|
+
return {
|
|
1433
|
+
dataBits: merged.dataBits ?? DEFAULT_SERIAL_SESSION_OPTIONS.dataBits,
|
|
1434
|
+
stopBits: merged.stopBits ?? DEFAULT_SERIAL_SESSION_OPTIONS.stopBits,
|
|
1435
|
+
parity: merged.parity ?? DEFAULT_SERIAL_SESSION_OPTIONS.parity,
|
|
1436
|
+
flowControl: merged.flowControl ?? DEFAULT_SERIAL_SESSION_OPTIONS.flowControl,
|
|
1437
|
+
baudRate: brandBaudRate(baudRate),
|
|
1438
|
+
bufferSize: brandSerialPortBufferSize(
|
|
1439
|
+
bufferSize ?? DEFAULT_SERIAL_SESSION_OPTIONS.bufferSize
|
|
1440
|
+
)
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
function resolveSerialSessionOptions(options) {
|
|
1444
|
+
const connection = resolveConnectionOptions(options);
|
|
1445
|
+
const filters = validateSerialPortFilters(options?.filters);
|
|
1446
|
+
return {
|
|
1447
|
+
...connection,
|
|
1448
|
+
...filters !== void 0 ? { filters } : {},
|
|
1449
|
+
receiveReplay: resolveReceiveReplayOptions(options?.receiveReplay),
|
|
1450
|
+
terminalBuffer: resolveTerminalBufferOptions(options?.terminalBuffer),
|
|
1451
|
+
lineBuffer: resolveLineBufferOptions(options?.lineBuffer)
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
429
1454
|
|
|
430
1455
|
// src/session/create-serial-session.ts
|
|
431
1456
|
function createSerialSession(options) {
|
|
432
|
-
const resolvedOptions =
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
filters: options?.filters,
|
|
436
|
-
receiveReplay: {
|
|
437
|
-
...DEFAULT_SERIAL_SESSION_OPTIONS.receiveReplay,
|
|
438
|
-
...options?.receiveReplay
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
const supported = hasWebSerialSupport();
|
|
442
|
-
const machine = new SessionStateMachine(
|
|
443
|
-
supported ? SerialSessionState.Idle : SerialSessionState.Unsupported
|
|
1457
|
+
const resolvedOptions = resolveSerialSessionOptions(options);
|
|
1458
|
+
const controller = createSessionRuntimeController(
|
|
1459
|
+
createInitialRuntime(hasWebSerialSupport())
|
|
444
1460
|
);
|
|
445
|
-
const errorsSubject = new
|
|
446
|
-
const receiveSubject = new Subject();
|
|
447
|
-
const linesSubject = new Subject();
|
|
1461
|
+
const errorsSubject = new Subject3();
|
|
448
1462
|
const sendQueue = createSendQueue();
|
|
449
1463
|
const textEncoder = new TextEncoder();
|
|
450
|
-
const
|
|
1464
|
+
const portInfoSubject = new BehaviorSubject3(null);
|
|
1465
|
+
const isDisposed = () => controller.status === SerialSessionStatus.Disposed;
|
|
1466
|
+
const errorReporterRef = {};
|
|
1467
|
+
const receivePipeline = createReceivePipeline({
|
|
1468
|
+
resolvedOptions,
|
|
1469
|
+
reportError: (error, options2) => errorReporterRef.reportError(error, options2)
|
|
1470
|
+
});
|
|
1471
|
+
const lifecycle = createSessionLifecycle({
|
|
1472
|
+
controller,
|
|
1473
|
+
resolvedOptions,
|
|
1474
|
+
sendQueue,
|
|
1475
|
+
receivePipeline,
|
|
1476
|
+
portInfoSubject,
|
|
1477
|
+
errorsSubject,
|
|
1478
|
+
isDisposed,
|
|
1479
|
+
reportError: (error, options2) => errorReporterRef.reportError(error, options2),
|
|
1480
|
+
createDisposedError: () => errorReporterRef.createDisposedError()
|
|
1481
|
+
});
|
|
1482
|
+
const { reportError, createDisposedError } = createSessionErrorReporter({
|
|
1483
|
+
controller,
|
|
1484
|
+
errorsSubject,
|
|
1485
|
+
sendQueue,
|
|
1486
|
+
isDisposed,
|
|
1487
|
+
updatePortInfo: lifecycle.updatePortInfo,
|
|
1488
|
+
teardownPump: lifecycle.teardownPump,
|
|
1489
|
+
closePortSafely: lifecycle.closePortSafely
|
|
1490
|
+
});
|
|
1491
|
+
errorReporterRef.reportError = reportError;
|
|
1492
|
+
errorReporterRef.createDisposedError = createDisposedError;
|
|
1493
|
+
const { receive$, lines$, receiveReplay$ } = receivePipeline;
|
|
451
1494
|
const errors$ = errorsSubject.asObservable();
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
1495
|
+
const terminalText$ = createTerminalBuffer(
|
|
1496
|
+
receive$,
|
|
1497
|
+
resolvedOptions.terminalBuffer
|
|
1498
|
+
).text$;
|
|
1499
|
+
const isConnected$ = controller.state$.pipe(
|
|
1500
|
+
map2((state) => state.status === SerialSessionStatus.Connected),
|
|
457
1501
|
distinctUntilChanged()
|
|
458
1502
|
);
|
|
459
|
-
const portInfoSubject = new BehaviorSubject2(null);
|
|
460
1503
|
const portInfo$ = portInfoSubject.asObservable();
|
|
461
|
-
const receiveReplayStream$ = resolvedOptions.receiveReplay.enabled ? new BehaviorSubject2(receive$) : null;
|
|
462
|
-
let activeReceiveReplay = null;
|
|
463
|
-
const clearLiveReceiveReplay = () => {
|
|
464
|
-
if (receiveReplayStream$) {
|
|
465
|
-
if (activeReceiveReplay) {
|
|
466
|
-
activeReceiveReplay.complete();
|
|
467
|
-
activeReceiveReplay = null;
|
|
468
|
-
}
|
|
469
|
-
receiveReplayStream$.next(receive$);
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
const startLiveReceiveReplay = () => {
|
|
473
|
-
if (!receiveReplayStream$) {
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
if (activeReceiveReplay) {
|
|
477
|
-
activeReceiveReplay.complete();
|
|
478
|
-
activeReceiveReplay = null;
|
|
479
|
-
}
|
|
480
|
-
const rs = new ReplaySubject(resolvedOptions.receiveReplay.bufferSize);
|
|
481
|
-
activeReceiveReplay = rs;
|
|
482
|
-
receiveReplayStream$.next(rs.asObservable());
|
|
483
|
-
};
|
|
484
|
-
const receiveReplay$ = receiveReplayStream$ ? receiveReplayStream$.pipe(switchMap((inner) => inner), share()) : receive$;
|
|
485
|
-
let activePort = null;
|
|
486
|
-
let activePump = null;
|
|
487
|
-
const setActivePort = (port) => {
|
|
488
|
-
activePort = port;
|
|
489
|
-
portInfoSubject.next(port ? port.getInfo() : null);
|
|
490
|
-
};
|
|
491
|
-
const teardownPump = async () => {
|
|
492
|
-
clearLiveReceiveReplay();
|
|
493
|
-
const pump = activePump;
|
|
494
|
-
activePump = null;
|
|
495
|
-
lineBuffer.clear();
|
|
496
|
-
if (pump) {
|
|
497
|
-
await pump.stop();
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
const closePortSafely = async (port) => {
|
|
501
|
-
if (!port) {
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
try {
|
|
505
|
-
await port.close();
|
|
506
|
-
} catch {
|
|
507
|
-
}
|
|
508
|
-
};
|
|
509
|
-
const reportError = (error, severity, options2) => {
|
|
510
|
-
const serialError = normalizeSerialError(error, options2);
|
|
511
|
-
errorsSubject.next(serialError);
|
|
512
|
-
if (severity === "fatal") {
|
|
513
|
-
machine.toError();
|
|
514
|
-
sendQueue.clear();
|
|
515
|
-
const portToClose = activePort;
|
|
516
|
-
setActivePort(null);
|
|
517
|
-
void teardownPump().then(() => closePortSafely(portToClose));
|
|
518
|
-
}
|
|
519
|
-
return serialError;
|
|
520
|
-
};
|
|
521
|
-
const writeToPort = async (payload) => {
|
|
522
|
-
const port = activePort;
|
|
523
|
-
if (machine.current !== SerialSessionState.Connected || !port || !port.writable) {
|
|
524
|
-
throw new SerialError(
|
|
525
|
-
"PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
|
|
526
|
-
"Cannot send data while session is not connected"
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
const writer = port.writable.getWriter();
|
|
530
|
-
try {
|
|
531
|
-
await writer.write(payload);
|
|
532
|
-
} finally {
|
|
533
|
-
try {
|
|
534
|
-
writer.releaseLock();
|
|
535
|
-
} catch {
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
1504
|
return {
|
|
540
1505
|
isBrowserSupported() {
|
|
541
1506
|
return hasWebSerialSupport();
|
|
542
1507
|
},
|
|
543
|
-
connect
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
new SerialError(
|
|
548
|
-
"BROWSER_NOT_SUPPORTED" /* BROWSER_NOT_SUPPORTED */,
|
|
549
|
-
"Web Serial API is not supported in this environment"
|
|
550
|
-
),
|
|
551
|
-
"non-fatal",
|
|
552
|
-
{ fallbackCode: "BROWSER_NOT_SUPPORTED" /* BROWSER_NOT_SUPPORTED */ }
|
|
553
|
-
);
|
|
554
|
-
subscriber.error(error);
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
const current = machine.current;
|
|
558
|
-
if (current !== SerialSessionState.Idle && current !== SerialSessionState.Error) {
|
|
559
|
-
const error = reportError(
|
|
560
|
-
new SerialError(
|
|
561
|
-
"PORT_ALREADY_OPEN" /* PORT_ALREADY_OPEN */,
|
|
562
|
-
`Cannot connect while session state is '${current}'`
|
|
563
|
-
),
|
|
564
|
-
"non-fatal",
|
|
565
|
-
{ fallbackCode: "PORT_ALREADY_OPEN" /* PORT_ALREADY_OPEN */ }
|
|
566
|
-
);
|
|
567
|
-
subscriber.error(error);
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
let cancelled = false;
|
|
571
|
-
machine.toConnecting();
|
|
572
|
-
const run = async () => {
|
|
573
|
-
let selectedPort = null;
|
|
574
|
-
try {
|
|
575
|
-
selectedPort = await navigator.serial.requestPort(
|
|
576
|
-
buildRequestOptions(resolvedOptions)
|
|
577
|
-
);
|
|
578
|
-
await selectedPort.open({
|
|
579
|
-
baudRate: resolvedOptions.baudRate,
|
|
580
|
-
dataBits: resolvedOptions.dataBits,
|
|
581
|
-
stopBits: resolvedOptions.stopBits,
|
|
582
|
-
parity: resolvedOptions.parity,
|
|
583
|
-
bufferSize: resolvedOptions.bufferSize,
|
|
584
|
-
flowControl: resolvedOptions.flowControl
|
|
585
|
-
});
|
|
586
|
-
} catch (error) {
|
|
587
|
-
if (selectedPort) {
|
|
588
|
-
await closePortSafely(selectedPort);
|
|
589
|
-
}
|
|
590
|
-
const serialError = reportError(error, "fatal", {
|
|
591
|
-
fallbackCode: "PORT_OPEN_FAILED" /* PORT_OPEN_FAILED */,
|
|
592
|
-
messagePrefix: "Failed to open port"
|
|
593
|
-
});
|
|
594
|
-
if (!cancelled) {
|
|
595
|
-
subscriber.error(serialError);
|
|
596
|
-
}
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
if (cancelled) {
|
|
600
|
-
await closePortSafely(selectedPort);
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
setActivePort(selectedPort);
|
|
604
|
-
lineBuffer.clear();
|
|
605
|
-
if (resolvedOptions.receiveReplay.enabled) {
|
|
606
|
-
startLiveReceiveReplay();
|
|
607
|
-
}
|
|
608
|
-
activePump = createReadPump(selectedPort, {
|
|
609
|
-
onChunk: (text) => {
|
|
610
|
-
receiveSubject.next(text);
|
|
611
|
-
if (activeReceiveReplay) {
|
|
612
|
-
activeReceiveReplay.next(text);
|
|
613
|
-
}
|
|
614
|
-
for (const line of lineBuffer.feed(text)) {
|
|
615
|
-
linesSubject.next(line);
|
|
616
|
-
}
|
|
617
|
-
},
|
|
618
|
-
onError: (pumpError) => reportError(pumpError, "fatal", {
|
|
619
|
-
fallbackCode: "READ_FAILED" /* READ_FAILED */,
|
|
620
|
-
messagePrefix: "Read pump failed"
|
|
621
|
-
})
|
|
622
|
-
});
|
|
623
|
-
activePump.start();
|
|
624
|
-
sendQueue.clear();
|
|
625
|
-
machine.toConnected();
|
|
626
|
-
subscriber.next();
|
|
627
|
-
subscriber.complete();
|
|
628
|
-
};
|
|
629
|
-
void run();
|
|
630
|
-
return () => {
|
|
631
|
-
cancelled = true;
|
|
632
|
-
};
|
|
633
|
-
});
|
|
634
|
-
},
|
|
635
|
-
disconnect$() {
|
|
636
|
-
return new Observable3((subscriber) => {
|
|
637
|
-
const current = machine.current;
|
|
638
|
-
if (current === SerialSessionState.Idle || current === SerialSessionState.Unsupported) {
|
|
639
|
-
subscriber.next();
|
|
640
|
-
subscriber.complete();
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
if (current !== SerialSessionState.Connected && current !== SerialSessionState.Error) {
|
|
644
|
-
const error = reportError(
|
|
645
|
-
new SerialError(
|
|
646
|
-
"PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
|
|
647
|
-
`Cannot disconnect while session state is '${current}'`
|
|
648
|
-
),
|
|
649
|
-
"non-fatal",
|
|
650
|
-
{ fallbackCode: "PORT_NOT_OPEN" /* PORT_NOT_OPEN */ }
|
|
651
|
-
);
|
|
652
|
-
subscriber.error(error);
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
machine.toDisconnecting();
|
|
656
|
-
sendQueue.clear();
|
|
657
|
-
const portToClose = activePort;
|
|
658
|
-
const run = async () => {
|
|
659
|
-
try {
|
|
660
|
-
await teardownPump();
|
|
661
|
-
if (portToClose) {
|
|
662
|
-
try {
|
|
663
|
-
await portToClose.close();
|
|
664
|
-
} catch (error) {
|
|
665
|
-
setActivePort(null);
|
|
666
|
-
const serialError = reportError(error, "fatal", {
|
|
667
|
-
fallbackCode: "CONNECTION_LOST" /* CONNECTION_LOST */,
|
|
668
|
-
messagePrefix: "Failed to close port"
|
|
669
|
-
});
|
|
670
|
-
subscriber.error(serialError);
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
setActivePort(null);
|
|
675
|
-
machine.toIdle();
|
|
676
|
-
subscriber.next();
|
|
677
|
-
subscriber.complete();
|
|
678
|
-
} catch (error) {
|
|
679
|
-
const serialError = reportError(error, "fatal", {
|
|
680
|
-
fallbackCode: "UNKNOWN" /* UNKNOWN */,
|
|
681
|
-
messagePrefix: "Unexpected disconnect failure"
|
|
682
|
-
});
|
|
683
|
-
subscriber.error(serialError);
|
|
684
|
-
}
|
|
685
|
-
};
|
|
686
|
-
void run();
|
|
687
|
-
});
|
|
688
|
-
},
|
|
1508
|
+
connect$: lifecycle.connect$,
|
|
1509
|
+
disconnect$: lifecycle.disconnect$,
|
|
1510
|
+
dispose$: lifecycle.dispose$,
|
|
1511
|
+
destroy$: lifecycle.dispose$,
|
|
689
1512
|
send$(data) {
|
|
1513
|
+
if (isDisposed()) {
|
|
1514
|
+
return new Observable5((subscriber) => {
|
|
1515
|
+
subscriber.error(createDisposedError());
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
690
1518
|
return sendQueue.enqueue(async () => {
|
|
691
1519
|
const payload = typeof data === "string" ? textEncoder.encode(data) : data;
|
|
692
1520
|
try {
|
|
693
|
-
await writeToPort(payload);
|
|
1521
|
+
await lifecycle.writeToPort(payload);
|
|
694
1522
|
} catch (error) {
|
|
695
|
-
throw reportError(error,
|
|
696
|
-
fallbackCode:
|
|
1523
|
+
throw reportError(error, {
|
|
1524
|
+
fallbackCode: SerialErrorCode.WRITE_FAILED,
|
|
697
1525
|
messagePrefix: "Failed to write data"
|
|
698
1526
|
});
|
|
699
1527
|
}
|
|
700
1528
|
});
|
|
701
1529
|
},
|
|
702
|
-
state$:
|
|
1530
|
+
state$: controller.state$,
|
|
703
1531
|
isConnected$,
|
|
704
1532
|
portInfo$,
|
|
705
1533
|
getPortInfo() {
|
|
706
1534
|
return portInfoSubject.getValue();
|
|
707
1535
|
},
|
|
708
1536
|
getCurrentPort() {
|
|
709
|
-
return
|
|
1537
|
+
return getRuntimePort(controller.runtime);
|
|
710
1538
|
},
|
|
711
1539
|
errors$,
|
|
712
1540
|
receive$,
|
|
@@ -716,10 +1544,16 @@ function createSerialSession(options) {
|
|
|
716
1544
|
};
|
|
717
1545
|
}
|
|
718
1546
|
export {
|
|
1547
|
+
DEFAULT_LINE_BUFFER_OPTIONS,
|
|
1548
|
+
DEFAULT_TERMINAL_BUFFER_OPTIONS,
|
|
1549
|
+
MAX_RECEIVE_REPLAY_BUFFER_SIZE,
|
|
1550
|
+
MAX_RECEIVE_REPLAY_MAX_CHARS,
|
|
719
1551
|
SerialError,
|
|
720
1552
|
SerialErrorCode,
|
|
721
|
-
|
|
1553
|
+
SerialSessionStatus,
|
|
1554
|
+
assertNever,
|
|
722
1555
|
createSerialSession,
|
|
723
|
-
createTerminalBuffer
|
|
1556
|
+
createTerminalBuffer,
|
|
1557
|
+
resolveSerialSessionOptions
|
|
724
1558
|
};
|
|
725
1559
|
//# sourceMappingURL=index.mjs.map
|