@gurezo/web-serial-rxjs 0.1.21 → 2.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 +82 -13
- package/README.md +82 -13
- package/dist/errors/serial-error-code.d.ts +7 -0
- package/dist/errors/serial-error-code.d.ts.map +1 -1
- package/dist/index.d.ts +31 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +469 -449
- package/dist/index.mjs +469 -449
- package/dist/index.mjs.map +4 -4
- package/dist/session/create-serial-session.d.ts +49 -0
- package/dist/session/create-serial-session.d.ts.map +1 -0
- package/dist/session/index.d.ts +5 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/internal/build-request-options.d.ts +18 -0
- package/dist/session/internal/build-request-options.d.ts.map +1 -0
- package/dist/session/internal/has-web-serial-support.d.ts +12 -0
- package/dist/session/internal/has-web-serial-support.d.ts.map +1 -0
- package/dist/session/internal/line-buffer.d.ts +14 -0
- package/dist/session/internal/line-buffer.d.ts.map +1 -0
- package/dist/session/normalize-serial-error.d.ts +55 -0
- package/dist/session/normalize-serial-error.d.ts.map +1 -0
- package/dist/session/read-pump.d.ts +74 -0
- package/dist/session/read-pump.d.ts.map +1 -0
- package/dist/session/send-queue.d.ts +58 -0
- package/dist/session/send-queue.d.ts.map +1 -0
- package/dist/session/serial-session-options.d.ts +80 -0
- package/dist/session/serial-session-options.d.ts.map +1 -0
- package/dist/session/serial-session-state.d.ts +35 -0
- package/dist/session/serial-session-state.d.ts.map +1 -0
- package/dist/session/serial-session.d.ts +143 -0
- package/dist/session/serial-session.d.ts.map +1 -0
- package/dist/session/session-state-machine.d.ts +39 -0
- package/dist/session/session-state-machine.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/browser/browser-detection.d.ts +0 -104
- package/dist/browser/browser-detection.d.ts.map +0 -1
- package/dist/browser/browser-support.d.ts +0 -57
- package/dist/browser/browser-support.d.ts.map +0 -1
- package/dist/client/index.d.ts +0 -250
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/serial-client.d.ts +0 -98
- package/dist/client/serial-client.d.ts.map +0 -1
- package/dist/filters/build-request-options.d.ts +0 -42
- package/dist/filters/build-request-options.d.ts.map +0 -1
- package/dist/io/observable-to-writable.d.ts +0 -65
- package/dist/io/observable-to-writable.d.ts.map +0 -1
- package/dist/io/readable-to-observable.d.ts +0 -44
- package/dist/io/readable-to-observable.d.ts.map +0 -1
- package/dist/lib/web-serial-rxjs.d.ts +0 -7
- package/dist/lib/web-serial-rxjs.d.ts.map +0 -1
- package/dist/types/options.d.ts +0 -107
- package/dist/types/options.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// packages/web-serial-rxjs/src/
|
|
2
|
-
import { Observable as
|
|
1
|
+
// packages/web-serial-rxjs/src/session/create-serial-session.ts
|
|
2
|
+
import { distinctUntilChanged, map, Observable as Observable3, Subject } from "rxjs";
|
|
3
3
|
|
|
4
4
|
// packages/web-serial-rxjs/src/errors/serial-error-code.ts
|
|
5
5
|
var SerialErrorCode = /* @__PURE__ */ ((SerialErrorCode2) => {
|
|
@@ -13,6 +13,7 @@ var SerialErrorCode = /* @__PURE__ */ ((SerialErrorCode2) => {
|
|
|
13
13
|
SerialErrorCode2["CONNECTION_LOST"] = "CONNECTION_LOST";
|
|
14
14
|
SerialErrorCode2["INVALID_FILTER_OPTIONS"] = "INVALID_FILTER_OPTIONS";
|
|
15
15
|
SerialErrorCode2["OPERATION_CANCELLED"] = "OPERATION_CANCELLED";
|
|
16
|
+
SerialErrorCode2["OPERATION_TIMEOUT"] = "OPERATION_TIMEOUT";
|
|
16
17
|
SerialErrorCode2["UNKNOWN"] = "UNKNOWN";
|
|
17
18
|
return SerialErrorCode2;
|
|
18
19
|
})(SerialErrorCode || {});
|
|
@@ -56,54 +57,7 @@ var SerialError = class _SerialError extends Error {
|
|
|
56
57
|
}
|
|
57
58
|
};
|
|
58
59
|
|
|
59
|
-
// packages/web-serial-rxjs/src/
|
|
60
|
-
var BrowserType = /* @__PURE__ */ ((BrowserType2) => {
|
|
61
|
-
BrowserType2["CHROME"] = "chrome";
|
|
62
|
-
BrowserType2["EDGE"] = "edge";
|
|
63
|
-
BrowserType2["OPERA"] = "opera";
|
|
64
|
-
BrowserType2["UNKNOWN"] = "unknown";
|
|
65
|
-
return BrowserType2;
|
|
66
|
-
})(BrowserType || {});
|
|
67
|
-
function hasWebSerialSupport() {
|
|
68
|
-
return typeof navigator !== "undefined" && "serial" in navigator && navigator.serial !== void 0 && navigator.serial !== null;
|
|
69
|
-
}
|
|
70
|
-
function detectBrowserType() {
|
|
71
|
-
if (typeof navigator === "undefined" || !navigator.userAgent) {
|
|
72
|
-
return "unknown" /* UNKNOWN */;
|
|
73
|
-
}
|
|
74
|
-
const ua = navigator.userAgent.toLowerCase();
|
|
75
|
-
if (ua.includes("edg/")) {
|
|
76
|
-
return "edge" /* EDGE */;
|
|
77
|
-
}
|
|
78
|
-
if (ua.includes("opr/") || ua.includes("opera/")) {
|
|
79
|
-
return "opera" /* OPERA */;
|
|
80
|
-
}
|
|
81
|
-
if (ua.includes("chrome/")) {
|
|
82
|
-
return "chrome" /* CHROME */;
|
|
83
|
-
}
|
|
84
|
-
return "unknown" /* UNKNOWN */;
|
|
85
|
-
}
|
|
86
|
-
function isChromiumBased() {
|
|
87
|
-
const browserType = detectBrowserType();
|
|
88
|
-
return browserType === "chrome" /* CHROME */ || browserType === "edge" /* EDGE */ || browserType === "opera" /* OPERA */;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// packages/web-serial-rxjs/src/browser/browser-support.ts
|
|
92
|
-
function checkBrowserSupport() {
|
|
93
|
-
if (!hasWebSerialSupport()) {
|
|
94
|
-
const browserType = detectBrowserType();
|
|
95
|
-
const browserName = browserType === "unknown" /* UNKNOWN */ ? "your browser" : browserType.toUpperCase();
|
|
96
|
-
throw new SerialError(
|
|
97
|
-
"BROWSER_NOT_SUPPORTED" /* BROWSER_NOT_SUPPORTED */,
|
|
98
|
-
`Web Serial API is not supported in ${browserName}. Please use a Chromium-based browser (Chrome, Edge, or Opera).`
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
function isBrowserSupported() {
|
|
103
|
-
return hasWebSerialSupport();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// packages/web-serial-rxjs/src/filters/build-request-options.ts
|
|
60
|
+
// packages/web-serial-rxjs/src/session/internal/build-request-options.ts
|
|
107
61
|
function buildRequestOptions(options) {
|
|
108
62
|
if (!options || !options.filters || options.filters.length === 0) {
|
|
109
63
|
return void 0;
|
|
@@ -137,165 +91,204 @@ function buildRequestOptions(options) {
|
|
|
137
91
|
};
|
|
138
92
|
}
|
|
139
93
|
|
|
140
|
-
// packages/web-serial-rxjs/src/
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
94
|
+
// packages/web-serial-rxjs/src/session/internal/has-web-serial-support.ts
|
|
95
|
+
function hasWebSerialSupport() {
|
|
96
|
+
return typeof navigator !== "undefined" && "serial" in navigator && navigator.serial !== void 0 && navigator.serial !== null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// packages/web-serial-rxjs/src/session/internal/line-buffer.ts
|
|
100
|
+
function createLineBuffer() {
|
|
101
|
+
let buffer = "";
|
|
102
|
+
const clear = () => {
|
|
103
|
+
buffer = "";
|
|
104
|
+
};
|
|
105
|
+
const feed = (chunk) => {
|
|
106
|
+
buffer += chunk;
|
|
107
|
+
const out = [];
|
|
108
|
+
for (; ; ) {
|
|
109
|
+
const crlf = buffer.indexOf("\r\n");
|
|
110
|
+
if (crlf >= 0) {
|
|
111
|
+
out.push(buffer.slice(0, crlf));
|
|
112
|
+
buffer = buffer.slice(crlf + 2);
|
|
113
|
+
continue;
|
|
149
114
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
await writer.write(chunk);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
subscription?.unsubscribe();
|
|
158
|
-
if (writer) {
|
|
159
|
-
writer.releaseLock();
|
|
160
|
-
}
|
|
161
|
-
throw error;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
error: async (error) => {
|
|
166
|
-
if (writer) {
|
|
167
|
-
try {
|
|
168
|
-
await writer.abort(error);
|
|
169
|
-
} catch {
|
|
170
|
-
} finally {
|
|
171
|
-
writer.releaseLock();
|
|
172
|
-
writer = null;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
complete: async () => {
|
|
177
|
-
if (writer) {
|
|
178
|
-
try {
|
|
179
|
-
await writer.close();
|
|
180
|
-
} catch {
|
|
181
|
-
} finally {
|
|
182
|
-
writer.releaseLock();
|
|
183
|
-
writer = null;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
},
|
|
189
|
-
abort(reason) {
|
|
190
|
-
if (subscription) {
|
|
191
|
-
subscription.unsubscribe();
|
|
192
|
-
subscription = null;
|
|
115
|
+
const cr = buffer.indexOf("\r");
|
|
116
|
+
if (cr >= 0 && cr + 1 < buffer.length && buffer[cr + 1] !== "\n") {
|
|
117
|
+
out.push(buffer.slice(0, cr));
|
|
118
|
+
buffer = buffer.slice(cr + 1);
|
|
119
|
+
continue;
|
|
193
120
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
121
|
+
const nl = buffer.indexOf("\n");
|
|
122
|
+
if (nl >= 0) {
|
|
123
|
+
out.push(buffer.slice(0, nl));
|
|
124
|
+
buffer = buffer.slice(nl + 1);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (cr >= 0 && cr + 1 === buffer.length) {
|
|
128
|
+
break;
|
|
199
129
|
}
|
|
130
|
+
break;
|
|
200
131
|
}
|
|
201
|
-
|
|
202
|
-
|
|
132
|
+
return out;
|
|
133
|
+
};
|
|
134
|
+
return { feed, clear };
|
|
203
135
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
136
|
+
|
|
137
|
+
// packages/web-serial-rxjs/src/session/normalize-serial-error.ts
|
|
138
|
+
var DEFAULT_MESSAGE_PREFIX = "Serial operation failed";
|
|
139
|
+
var isDomExceptionWithName = (error, name) => typeof DOMException !== "undefined" && error instanceof DOMException && error.name === name;
|
|
140
|
+
function normalizeSerialError(error, options) {
|
|
141
|
+
if (error instanceof SerialError) {
|
|
142
|
+
return error;
|
|
143
|
+
}
|
|
144
|
+
const prefix = options.messagePrefix ?? DEFAULT_MESSAGE_PREFIX;
|
|
145
|
+
if (isDomExceptionWithName(error, "NotFoundError")) {
|
|
146
|
+
return new SerialError(
|
|
147
|
+
"OPERATION_CANCELLED" /* OPERATION_CANCELLED */,
|
|
148
|
+
"Port selection was cancelled by the user",
|
|
149
|
+
error
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
153
|
+
return new SerialError(
|
|
154
|
+
options.fallbackCode,
|
|
155
|
+
`${prefix}: ${cause.message}`,
|
|
156
|
+
cause
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// packages/web-serial-rxjs/src/session/read-pump.ts
|
|
161
|
+
function createReadPump(port, { onChunk, onError }) {
|
|
162
|
+
let reader = null;
|
|
163
|
+
let running = false;
|
|
164
|
+
let stopped = false;
|
|
165
|
+
const decoder = new TextDecoder(void 0, { fatal: false });
|
|
166
|
+
const releaseReader = () => {
|
|
167
|
+
if (!reader) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
207
170
|
try {
|
|
208
|
-
|
|
171
|
+
reader.releaseLock();
|
|
209
172
|
} catch {
|
|
173
|
+
}
|
|
174
|
+
reader = null;
|
|
175
|
+
};
|
|
176
|
+
const pump = async (stream) => {
|
|
177
|
+
reader = stream.getReader();
|
|
178
|
+
running = true;
|
|
179
|
+
try {
|
|
180
|
+
while (!stopped) {
|
|
181
|
+
const { done, value } = await reader.read();
|
|
182
|
+
if (done) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
if (value && value.byteLength > 0) {
|
|
186
|
+
const text = decoder.decode(value, { stream: true });
|
|
187
|
+
if (text.length > 0) {
|
|
188
|
+
onChunk(text);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!stopped) {
|
|
193
|
+
const tail = decoder.decode();
|
|
194
|
+
if (tail.length > 0) {
|
|
195
|
+
onChunk(tail);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (!stopped) {
|
|
200
|
+
onError(
|
|
201
|
+
normalizeSerialError(error, {
|
|
202
|
+
fallbackCode: "READ_FAILED" /* READ_FAILED */,
|
|
203
|
+
messagePrefix: "Read pump failed"
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
}
|
|
210
207
|
} finally {
|
|
211
|
-
|
|
208
|
+
running = false;
|
|
209
|
+
releaseReader();
|
|
212
210
|
}
|
|
213
211
|
};
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
212
|
+
return {
|
|
213
|
+
start() {
|
|
214
|
+
if (running || stopped) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const stream = port.readable;
|
|
218
|
+
if (!stream) {
|
|
219
|
+
stopped = true;
|
|
220
|
+
onError(
|
|
221
|
+
new SerialError(
|
|
222
|
+
"CONNECTION_LOST" /* CONNECTION_LOST */,
|
|
223
|
+
"Read pump failed: port.readable is not available"
|
|
224
|
+
)
|
|
225
225
|
);
|
|
226
|
-
|
|
226
|
+
return;
|
|
227
227
|
}
|
|
228
|
+
void pump(stream);
|
|
228
229
|
},
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
async stop() {
|
|
231
|
+
if (stopped) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
stopped = true;
|
|
235
|
+
if (!reader) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
231
238
|
try {
|
|
232
|
-
await
|
|
239
|
+
await reader.cancel();
|
|
233
240
|
} catch {
|
|
234
241
|
} finally {
|
|
235
|
-
|
|
242
|
+
releaseReader();
|
|
236
243
|
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
unsubscribe: () => {
|
|
241
|
-
subscription.unsubscribe();
|
|
242
|
-
writer.releaseLock();
|
|
244
|
+
},
|
|
245
|
+
get isRunning() {
|
|
246
|
+
return running;
|
|
243
247
|
}
|
|
244
248
|
};
|
|
245
249
|
}
|
|
246
250
|
|
|
247
|
-
// packages/web-serial-rxjs/src/
|
|
248
|
-
import { Observable } from "rxjs";
|
|
249
|
-
function
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
} else {
|
|
274
|
-
subscriber.error(
|
|
275
|
-
new SerialError(
|
|
276
|
-
"READ_FAILED" /* READ_FAILED */,
|
|
277
|
-
"Failed to read from stream: Unknown error",
|
|
278
|
-
error
|
|
279
|
-
)
|
|
251
|
+
// packages/web-serial-rxjs/src/session/send-queue.ts
|
|
252
|
+
import { Observable, defer } from "rxjs";
|
|
253
|
+
function createSendQueue() {
|
|
254
|
+
let chain = Promise.resolve();
|
|
255
|
+
return {
|
|
256
|
+
enqueue(operation) {
|
|
257
|
+
return defer(
|
|
258
|
+
() => new Observable((subscriber) => {
|
|
259
|
+
let cancelled = false;
|
|
260
|
+
const run = async () => {
|
|
261
|
+
try {
|
|
262
|
+
const value = await operation();
|
|
263
|
+
if (!cancelled) {
|
|
264
|
+
subscriber.next(value);
|
|
265
|
+
subscriber.complete();
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (!cancelled) {
|
|
269
|
+
subscriber.error(error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const scheduled = chain.then(run, run);
|
|
274
|
+
chain = scheduled.then(
|
|
275
|
+
() => void 0,
|
|
276
|
+
() => void 0
|
|
280
277
|
);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return () => {
|
|
292
|
-
reader.releaseLock();
|
|
293
|
-
};
|
|
294
|
-
});
|
|
278
|
+
return () => {
|
|
279
|
+
cancelled = true;
|
|
280
|
+
};
|
|
281
|
+
})
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
clear() {
|
|
285
|
+
chain = Promise.resolve();
|
|
286
|
+
}
|
|
287
|
+
};
|
|
295
288
|
}
|
|
296
289
|
|
|
297
|
-
// packages/web-serial-rxjs/src/
|
|
298
|
-
var
|
|
290
|
+
// packages/web-serial-rxjs/src/session/serial-session-options.ts
|
|
291
|
+
var DEFAULT_SERIAL_SESSION_OPTIONS = {
|
|
299
292
|
baudRate: 9600,
|
|
300
293
|
dataBits: 8,
|
|
301
294
|
stopBits: 1,
|
|
@@ -305,291 +298,318 @@ var DEFAULT_SERIAL_CLIENT_OPTIONS = {
|
|
|
305
298
|
filters: void 0
|
|
306
299
|
};
|
|
307
300
|
|
|
308
|
-
// packages/web-serial-rxjs/src/
|
|
309
|
-
var
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
301
|
+
// packages/web-serial-rxjs/src/session/serial-session-state.ts
|
|
302
|
+
var SerialSessionState = {
|
|
303
|
+
Idle: "idle",
|
|
304
|
+
Connecting: "connecting",
|
|
305
|
+
Connected: "connected",
|
|
306
|
+
Disconnecting: "disconnecting",
|
|
307
|
+
Unsupported: "unsupported",
|
|
308
|
+
Error: "error"
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// packages/web-serial-rxjs/src/session/session-state-machine.ts
|
|
312
|
+
import { BehaviorSubject } from "rxjs";
|
|
313
|
+
var S = SerialSessionState;
|
|
314
|
+
var ALLOWED_TRANSITIONS = {
|
|
315
|
+
[S.Idle]: [S.Connecting, S.Error],
|
|
316
|
+
[S.Connecting]: [S.Connected, S.Error, S.Idle],
|
|
317
|
+
[S.Connected]: [S.Disconnecting, S.Error],
|
|
318
|
+
[S.Disconnecting]: [S.Idle, S.Error],
|
|
319
|
+
[S.Error]: [S.Idle, S.Connecting],
|
|
320
|
+
[S.Unsupported]: []
|
|
321
|
+
};
|
|
322
|
+
var SessionStateMachine = class {
|
|
323
|
+
constructor(initial = SerialSessionState.Idle) {
|
|
324
|
+
this.subject = new BehaviorSubject(initial);
|
|
332
325
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
*
|
|
336
|
-
* @returns Observable that emits the selected SerialPort
|
|
337
|
-
* @internal
|
|
338
|
-
*/
|
|
339
|
-
requestPort() {
|
|
340
|
-
return defer(() => {
|
|
341
|
-
checkBrowserSupport();
|
|
342
|
-
return navigator.serial.requestPort(buildRequestOptions(this.options)).catch((error) => {
|
|
343
|
-
if (error instanceof DOMException && error.name === "NotFoundError") {
|
|
344
|
-
throw new SerialError(
|
|
345
|
-
"OPERATION_CANCELLED" /* OPERATION_CANCELLED */,
|
|
346
|
-
"Port selection was cancelled by the user",
|
|
347
|
-
error
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
throw new SerialError(
|
|
351
|
-
"PORT_NOT_AVAILABLE" /* PORT_NOT_AVAILABLE */,
|
|
352
|
-
`Failed to request port: ${error instanceof Error ? error.message : String(error)}`,
|
|
353
|
-
error instanceof Error ? error : new Error(String(error))
|
|
354
|
-
);
|
|
355
|
-
});
|
|
356
|
-
});
|
|
326
|
+
get current() {
|
|
327
|
+
return this.subject.getValue();
|
|
357
328
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
*
|
|
361
|
-
* @returns Observable that emits an array of available SerialPorts
|
|
362
|
-
* @internal
|
|
363
|
-
*/
|
|
364
|
-
getPorts() {
|
|
365
|
-
return defer(() => {
|
|
366
|
-
checkBrowserSupport();
|
|
367
|
-
return navigator.serial.getPorts().catch((error) => {
|
|
368
|
-
throw new SerialError(
|
|
369
|
-
"PORT_NOT_AVAILABLE" /* PORT_NOT_AVAILABLE */,
|
|
370
|
-
`Failed to get ports: ${error instanceof Error ? error.message : String(error)}`,
|
|
371
|
-
error instanceof Error ? error : new Error(String(error))
|
|
372
|
-
);
|
|
373
|
-
});
|
|
374
|
-
});
|
|
329
|
+
get state$() {
|
|
330
|
+
return this.subject.asObservable();
|
|
375
331
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
*
|
|
379
|
-
* @param port - Optional SerialPort to connect to. If not provided, will request one.
|
|
380
|
-
* @returns Observable that completes when the port is opened
|
|
381
|
-
* @internal
|
|
382
|
-
*/
|
|
383
|
-
connect(port) {
|
|
384
|
-
checkBrowserSupport();
|
|
385
|
-
if (this.isOpen) {
|
|
386
|
-
return new Observable2((subscriber) => {
|
|
387
|
-
subscriber.error(
|
|
388
|
-
new SerialError(
|
|
389
|
-
"PORT_ALREADY_OPEN" /* PORT_ALREADY_OPEN */,
|
|
390
|
-
"Port is already open"
|
|
391
|
-
)
|
|
392
|
-
);
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
const port$ = port ? new Observable2((subscriber) => {
|
|
396
|
-
subscriber.next(port);
|
|
397
|
-
subscriber.complete();
|
|
398
|
-
}) : this.requestPort();
|
|
399
|
-
return port$.pipe(
|
|
400
|
-
switchMap((selectedPort) => {
|
|
401
|
-
return defer(() => {
|
|
402
|
-
this.port = selectedPort;
|
|
403
|
-
return this.port.open({
|
|
404
|
-
baudRate: this.options.baudRate,
|
|
405
|
-
dataBits: this.options.dataBits,
|
|
406
|
-
stopBits: this.options.stopBits,
|
|
407
|
-
parity: this.options.parity,
|
|
408
|
-
bufferSize: this.options.bufferSize,
|
|
409
|
-
flowControl: this.options.flowControl
|
|
410
|
-
}).then(() => {
|
|
411
|
-
this.isOpen = true;
|
|
412
|
-
}).catch((error) => {
|
|
413
|
-
this.port = null;
|
|
414
|
-
this.isOpen = false;
|
|
415
|
-
if (error instanceof SerialError) {
|
|
416
|
-
throw error;
|
|
417
|
-
}
|
|
418
|
-
throw new SerialError(
|
|
419
|
-
"PORT_OPEN_FAILED" /* PORT_OPEN_FAILED */,
|
|
420
|
-
`Failed to open port: ${error instanceof Error ? error.message : String(error)}`,
|
|
421
|
-
error instanceof Error ? error : new Error(String(error))
|
|
422
|
-
);
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
})
|
|
426
|
-
);
|
|
332
|
+
toConnecting() {
|
|
333
|
+
return this.transition(S.Connecting);
|
|
427
334
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
*
|
|
431
|
-
* @returns Observable that completes when the port is closed
|
|
432
|
-
* @internal
|
|
433
|
-
*/
|
|
434
|
-
disconnect() {
|
|
435
|
-
return defer(() => {
|
|
436
|
-
if (!this.isOpen || !this.port) {
|
|
437
|
-
return Promise.resolve();
|
|
438
|
-
}
|
|
439
|
-
if (this.readSubscription) {
|
|
440
|
-
this.readSubscription.unsubscribe();
|
|
441
|
-
this.readSubscription = null;
|
|
442
|
-
}
|
|
443
|
-
if (this.writeSubscription) {
|
|
444
|
-
this.writeSubscription.unsubscribe();
|
|
445
|
-
this.writeSubscription = null;
|
|
446
|
-
}
|
|
447
|
-
return this.port.close().then(() => {
|
|
448
|
-
this.port = null;
|
|
449
|
-
this.isOpen = false;
|
|
450
|
-
}).catch((error) => {
|
|
451
|
-
this.port = null;
|
|
452
|
-
this.isOpen = false;
|
|
453
|
-
throw new SerialError(
|
|
454
|
-
"CONNECTION_LOST" /* CONNECTION_LOST */,
|
|
455
|
-
`Failed to close port: ${error instanceof Error ? error.message : String(error)}`,
|
|
456
|
-
error instanceof Error ? error : new Error(String(error))
|
|
457
|
-
);
|
|
458
|
-
});
|
|
459
|
-
});
|
|
335
|
+
toConnected() {
|
|
336
|
+
return this.transition(S.Connected);
|
|
460
337
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
338
|
+
toDisconnecting() {
|
|
339
|
+
return this.transition(S.Disconnecting);
|
|
340
|
+
}
|
|
341
|
+
toIdle() {
|
|
342
|
+
return this.transition(S.Idle);
|
|
343
|
+
}
|
|
344
|
+
toError() {
|
|
345
|
+
return this.transition(S.Error);
|
|
346
|
+
}
|
|
347
|
+
toUnsupported() {
|
|
348
|
+
return this.transition(S.Unsupported);
|
|
349
|
+
}
|
|
350
|
+
complete() {
|
|
351
|
+
this.subject.complete();
|
|
352
|
+
}
|
|
353
|
+
transition(next) {
|
|
354
|
+
const current = this.subject.getValue();
|
|
355
|
+
if (current === next) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
const allowed = ALLOWED_TRANSITIONS[current];
|
|
359
|
+
if (!allowed.includes(next)) {
|
|
360
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
361
|
+
console.warn(
|
|
362
|
+
`[web-serial-rxjs] Ignoring invalid SerialSession transition ${current} -> ${next}`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return false;
|
|
473
366
|
}
|
|
474
|
-
|
|
367
|
+
this.subject.next(next);
|
|
368
|
+
return true;
|
|
475
369
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// packages/web-serial-rxjs/src/session/create-serial-session.ts
|
|
373
|
+
function createSerialSession(options) {
|
|
374
|
+
const resolvedOptions = {
|
|
375
|
+
...DEFAULT_SERIAL_SESSION_OPTIONS,
|
|
376
|
+
...options,
|
|
377
|
+
filters: options?.filters
|
|
378
|
+
};
|
|
379
|
+
const supported = hasWebSerialSupport();
|
|
380
|
+
const machine = new SessionStateMachine(
|
|
381
|
+
supported ? SerialSessionState.Idle : SerialSessionState.Unsupported
|
|
382
|
+
);
|
|
383
|
+
const errorsSubject = new Subject();
|
|
384
|
+
const receiveSubject = new Subject();
|
|
385
|
+
const linesSubject = new Subject();
|
|
386
|
+
const sendQueue = createSendQueue();
|
|
387
|
+
const textEncoder = new TextEncoder();
|
|
388
|
+
const lineBuffer = createLineBuffer();
|
|
389
|
+
const errors$ = errorsSubject.asObservable();
|
|
390
|
+
const receive$ = receiveSubject.asObservable();
|
|
391
|
+
const lines$ = linesSubject.asObservable();
|
|
392
|
+
const isConnected$ = machine.state$.pipe(
|
|
393
|
+
map((state) => state === SerialSessionState.Connected),
|
|
394
|
+
distinctUntilChanged()
|
|
395
|
+
);
|
|
396
|
+
let activePort = null;
|
|
397
|
+
let activePump = null;
|
|
398
|
+
const teardownPump = async () => {
|
|
399
|
+
const pump = activePump;
|
|
400
|
+
activePump = null;
|
|
401
|
+
lineBuffer.clear();
|
|
402
|
+
if (pump) {
|
|
403
|
+
await pump.stop();
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const closePortSafely = async (port) => {
|
|
407
|
+
if (!port) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
await port.close();
|
|
412
|
+
} catch {
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const reportError = (error, severity, options2) => {
|
|
416
|
+
const serialError = normalizeSerialError(error, options2);
|
|
417
|
+
errorsSubject.next(serialError);
|
|
418
|
+
if (severity === "fatal") {
|
|
419
|
+
machine.toError();
|
|
420
|
+
sendQueue.clear();
|
|
421
|
+
const portToClose = activePort;
|
|
422
|
+
activePort = null;
|
|
423
|
+
void teardownPump().then(() => closePortSafely(portToClose));
|
|
424
|
+
}
|
|
425
|
+
return serialError;
|
|
426
|
+
};
|
|
427
|
+
const writeToPort = async (payload) => {
|
|
428
|
+
const port = activePort;
|
|
429
|
+
if (machine.current !== SerialSessionState.Connected || !port || !port.writable) {
|
|
485
430
|
throw new SerialError(
|
|
486
431
|
"PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
|
|
487
|
-
"
|
|
432
|
+
"Cannot send data while session is not connected"
|
|
488
433
|
);
|
|
489
434
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
new SerialError(
|
|
498
|
-
"WRITE_FAILED" /* WRITE_FAILED */,
|
|
499
|
-
"Write subscription is not available"
|
|
500
|
-
)
|
|
501
|
-
);
|
|
502
|
-
return;
|
|
435
|
+
const writer = port.writable.getWriter();
|
|
436
|
+
try {
|
|
437
|
+
await writer.write(payload);
|
|
438
|
+
} finally {
|
|
439
|
+
try {
|
|
440
|
+
writer.releaseLock();
|
|
441
|
+
} catch {
|
|
503
442
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
return {
|
|
446
|
+
isBrowserSupported() {
|
|
447
|
+
return hasWebSerialSupport();
|
|
448
|
+
},
|
|
449
|
+
connect$() {
|
|
450
|
+
return new Observable3((subscriber) => {
|
|
451
|
+
if (!hasWebSerialSupport()) {
|
|
452
|
+
const error = reportError(
|
|
453
|
+
new SerialError(
|
|
454
|
+
"BROWSER_NOT_SUPPORTED" /* BROWSER_NOT_SUPPORTED */,
|
|
455
|
+
"Web Serial API is not supported in this environment"
|
|
456
|
+
),
|
|
457
|
+
"non-fatal",
|
|
458
|
+
{ fallbackCode: "BROWSER_NOT_SUPPORTED" /* BROWSER_NOT_SUPPORTED */ }
|
|
459
|
+
);
|
|
460
|
+
subscriber.error(error);
|
|
461
|
+
return;
|
|
509
462
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
463
|
+
const current = machine.current;
|
|
464
|
+
if (current !== SerialSessionState.Idle && current !== SerialSessionState.Error) {
|
|
465
|
+
const error = reportError(
|
|
466
|
+
new SerialError(
|
|
467
|
+
"PORT_ALREADY_OPEN" /* PORT_ALREADY_OPEN */,
|
|
468
|
+
`Cannot connect while session state is '${current}'`
|
|
469
|
+
),
|
|
470
|
+
"non-fatal",
|
|
471
|
+
{ fallbackCode: "PORT_ALREADY_OPEN" /* PORT_ALREADY_OPEN */ }
|
|
472
|
+
);
|
|
473
|
+
subscriber.error(error);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
let cancelled = false;
|
|
477
|
+
machine.toConnecting();
|
|
478
|
+
const run = async () => {
|
|
479
|
+
let selectedPort = null;
|
|
480
|
+
try {
|
|
481
|
+
selectedPort = await navigator.serial.requestPort(
|
|
482
|
+
buildRequestOptions(resolvedOptions)
|
|
483
|
+
);
|
|
484
|
+
await selectedPort.open({
|
|
485
|
+
baudRate: resolvedOptions.baudRate,
|
|
486
|
+
dataBits: resolvedOptions.dataBits,
|
|
487
|
+
stopBits: resolvedOptions.stopBits,
|
|
488
|
+
parity: resolvedOptions.parity,
|
|
489
|
+
bufferSize: resolvedOptions.bufferSize,
|
|
490
|
+
flowControl: resolvedOptions.flowControl
|
|
491
|
+
});
|
|
492
|
+
} catch (error) {
|
|
493
|
+
if (selectedPort) {
|
|
494
|
+
await closePortSafely(selectedPort);
|
|
495
|
+
}
|
|
496
|
+
activePort = null;
|
|
497
|
+
const serialError = reportError(error, "fatal", {
|
|
498
|
+
fallbackCode: "PORT_OPEN_FAILED" /* PORT_OPEN_FAILED */,
|
|
499
|
+
messagePrefix: "Failed to open port"
|
|
500
|
+
});
|
|
501
|
+
if (!cancelled) {
|
|
502
|
+
subscriber.error(serialError);
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
516
505
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (this.writeSubscription) {
|
|
521
|
-
this.writeSubscription.unsubscribe();
|
|
522
|
-
this.writeSubscription = null;
|
|
506
|
+
if (cancelled) {
|
|
507
|
+
await closePortSafely(selectedPort);
|
|
508
|
+
return;
|
|
523
509
|
}
|
|
510
|
+
activePort = selectedPort;
|
|
511
|
+
lineBuffer.clear();
|
|
512
|
+
activePump = createReadPump(selectedPort, {
|
|
513
|
+
onChunk: (text) => {
|
|
514
|
+
receiveSubject.next(text);
|
|
515
|
+
for (const line of lineBuffer.feed(text)) {
|
|
516
|
+
linesSubject.next(line);
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
onError: (pumpError) => reportError(pumpError, "fatal", {
|
|
520
|
+
fallbackCode: "READ_FAILED" /* READ_FAILED */,
|
|
521
|
+
messagePrefix: "Read pump failed"
|
|
522
|
+
})
|
|
523
|
+
});
|
|
524
|
+
activePump.start();
|
|
525
|
+
sendQueue.clear();
|
|
526
|
+
machine.toConnected();
|
|
527
|
+
subscriber.next();
|
|
528
|
+
subscriber.complete();
|
|
529
|
+
};
|
|
530
|
+
void run();
|
|
531
|
+
return () => {
|
|
532
|
+
cancelled = true;
|
|
533
|
+
};
|
|
534
|
+
});
|
|
535
|
+
},
|
|
536
|
+
disconnect$() {
|
|
537
|
+
return new Observable3((subscriber) => {
|
|
538
|
+
const current = machine.current;
|
|
539
|
+
if (current === SerialSessionState.Idle || current === SerialSessionState.Unsupported) {
|
|
540
|
+
subscriber.next();
|
|
541
|
+
subscriber.complete();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (current !== SerialSessionState.Connected && current !== SerialSessionState.Error) {
|
|
545
|
+
const error = reportError(
|
|
546
|
+
new SerialError(
|
|
547
|
+
"PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
|
|
548
|
+
`Cannot disconnect while session state is '${current}'`
|
|
549
|
+
),
|
|
550
|
+
"non-fatal",
|
|
551
|
+
{ fallbackCode: "PORT_NOT_OPEN" /* PORT_NOT_OPEN */ }
|
|
552
|
+
);
|
|
524
553
|
subscriber.error(error);
|
|
554
|
+
return;
|
|
525
555
|
}
|
|
556
|
+
machine.toDisconnecting();
|
|
557
|
+
sendQueue.clear();
|
|
558
|
+
const portToClose = activePort;
|
|
559
|
+
const run = async () => {
|
|
560
|
+
try {
|
|
561
|
+
await teardownPump();
|
|
562
|
+
if (portToClose) {
|
|
563
|
+
try {
|
|
564
|
+
await portToClose.close();
|
|
565
|
+
} catch (error) {
|
|
566
|
+
activePort = null;
|
|
567
|
+
const serialError = reportError(error, "fatal", {
|
|
568
|
+
fallbackCode: "CONNECTION_LOST" /* CONNECTION_LOST */,
|
|
569
|
+
messagePrefix: "Failed to close port"
|
|
570
|
+
});
|
|
571
|
+
subscriber.error(serialError);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
activePort = null;
|
|
576
|
+
machine.toIdle();
|
|
577
|
+
subscriber.next();
|
|
578
|
+
subscriber.complete();
|
|
579
|
+
} catch (error) {
|
|
580
|
+
const serialError = reportError(error, "fatal", {
|
|
581
|
+
fallbackCode: "UNKNOWN" /* UNKNOWN */,
|
|
582
|
+
messagePrefix: "Unexpected disconnect failure"
|
|
583
|
+
});
|
|
584
|
+
subscriber.error(serialError);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
void run();
|
|
526
588
|
});
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
throw new SerialError(
|
|
540
|
-
"PORT_NOT_OPEN" /* PORT_NOT_OPEN */,
|
|
541
|
-
"Port is not open or writable stream is not available"
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
const writer = this.port.writable.getWriter();
|
|
545
|
-
return writer.write(data).then(() => {
|
|
546
|
-
writer.releaseLock();
|
|
547
|
-
}).catch((error) => {
|
|
548
|
-
writer.releaseLock();
|
|
549
|
-
throw new SerialError(
|
|
550
|
-
"WRITE_FAILED" /* WRITE_FAILED */,
|
|
551
|
-
`Failed to write data: ${error instanceof Error ? error.message : String(error)}`,
|
|
552
|
-
error instanceof Error ? error : new Error(String(error))
|
|
553
|
-
);
|
|
589
|
+
},
|
|
590
|
+
send$(data) {
|
|
591
|
+
return sendQueue.enqueue(async () => {
|
|
592
|
+
const payload = typeof data === "string" ? textEncoder.encode(data) : data;
|
|
593
|
+
try {
|
|
594
|
+
await writeToPort(payload);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
throw reportError(error, "non-fatal", {
|
|
597
|
+
fallbackCode: "WRITE_FAILED" /* WRITE_FAILED */,
|
|
598
|
+
messagePrefix: "Failed to write data"
|
|
599
|
+
});
|
|
600
|
+
}
|
|
554
601
|
});
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
*/
|
|
563
|
-
get connected() {
|
|
564
|
-
return this.isOpen;
|
|
565
|
-
}
|
|
566
|
-
/**
|
|
567
|
-
* Get the current SerialPort instance.
|
|
568
|
-
*
|
|
569
|
-
* @returns The current SerialPort instance, or `null` if no port is open
|
|
570
|
-
* @internal
|
|
571
|
-
*/
|
|
572
|
-
get currentPort() {
|
|
573
|
-
return this.port;
|
|
574
|
-
}
|
|
575
|
-
};
|
|
576
|
-
|
|
577
|
-
// packages/web-serial-rxjs/src/client/index.ts
|
|
578
|
-
function createSerialClient(options) {
|
|
579
|
-
return new SerialClientImpl(options);
|
|
602
|
+
},
|
|
603
|
+
state$: machine.state$,
|
|
604
|
+
isConnected$,
|
|
605
|
+
errors$,
|
|
606
|
+
receive$,
|
|
607
|
+
lines$
|
|
608
|
+
};
|
|
580
609
|
}
|
|
581
610
|
export {
|
|
582
|
-
BrowserType,
|
|
583
611
|
SerialError,
|
|
584
612
|
SerialErrorCode,
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
createSerialClient,
|
|
588
|
-
detectBrowserType,
|
|
589
|
-
hasWebSerialSupport,
|
|
590
|
-
isBrowserSupported,
|
|
591
|
-
isChromiumBased,
|
|
592
|
-
observableToWritable,
|
|
593
|
-
readableToObservable,
|
|
594
|
-
subscribeToWritable
|
|
613
|
+
SerialSessionState,
|
|
614
|
+
createSerialSession
|
|
595
615
|
};
|