webserial-core 2.0.0-dev.1 → 2.0.1

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.
@@ -0,0 +1,498 @@
1
+ <!doctype html>
2
+ <html lang="en" data-theme="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
+ <title>WebSocket — webserial-core demo</title>
8
+ <style>
9
+ :root {
10
+ --accent: #f97316;
11
+ --accent-dark: #ea580c;
12
+ --accent-glow: rgba(249, 115, 22, 0.12);
13
+ --accent-badge: #fff7ed;
14
+ }
15
+ </style>
16
+ <script
17
+ type="module"
18
+ crossorigin
19
+ src="/demos/assets/websocket-DREvCVt-.js"
20
+ ></script>
21
+ <link
22
+ rel="modulepreload"
23
+ crossorigin
24
+ href="/demos/assets/demo-shared-DnvFynUr.js"
25
+ />
26
+ <link
27
+ rel="stylesheet"
28
+ crossorigin
29
+ href="/demos/assets/demo-shared-DLFsukQx.css"
30
+ />
31
+ </head>
32
+ <body>
33
+ <!-- ── Topbar ──────────────────────────────────────────── -->
34
+ <header class="topbar">
35
+ <button class="icon-btn" id="menu-btn" title="Toggle sidebar">
36
+ <i data-lucide="menu"></i>
37
+ </button>
38
+ <a class="topbar-brand" href="/demos/web-serial.html">
39
+ <span class="brand-icon"><i data-lucide="globe"></i></span>
40
+ <span class="brand-name">webserial-core</span>
41
+ <span class="brand-ver">v2</span>
42
+ </a>
43
+ <span class="provider-pill">WebSocket</span>
44
+ <span class="flex-1"></span>
45
+
46
+ <nav class="topbar-nav">
47
+ <a class="nav-item" href="/demos/web-serial.html"
48
+ ><i data-lucide="zap"></i><span class="nav-lbl"> Web Serial</span></a
49
+ >
50
+ <a class="nav-item" href="/demos/web-bluetooth.html"
51
+ ><i data-lucide="bluetooth"></i
52
+ ><span class="nav-lbl"> Bluetooth</span></a
53
+ >
54
+ <a class="nav-item" href="/demos/web-usb.html"
55
+ ><i data-lucide="usb"></i><span class="nav-lbl"> WebUSB</span></a
56
+ >
57
+ <a class="nav-item active" href="/demos/websocket.html"
58
+ ><i data-lucide="globe"></i><span class="nav-lbl"> WebSocket</span></a
59
+ >
60
+ </nav>
61
+
62
+ <div class="topbar-actions">
63
+ <div class="status-pill">
64
+ <div class="status-dot" id="status-dot"></div>
65
+ <span id="status-text">Disconnected</span>
66
+ </div>
67
+ <button class="icon-btn" id="theme-btn" title="Toggle theme">🌙</button>
68
+ <button
69
+ class="icon-btn code-toggle"
70
+ id="code-toggle-btn"
71
+ title="Toggle code panel"
72
+ >
73
+ <i data-lucide="code-2"></i>
74
+ </button>
75
+ <button class="btn btn-connect" id="btn-connect">Connect</button>
76
+ <button class="btn btn-disconnect" id="btn-disconnect" disabled>
77
+ Disconnect
78
+ </button>
79
+ </div>
80
+ </header>
81
+
82
+ <!-- ── App layout ───────────────────────────────────────── -->
83
+ <div class="app-layout">
84
+ <!-- ── Sidebar ─────────────────────────────────────────── -->
85
+ <aside class="sidebar" id="sidebar">
86
+ <div class="sidebar-scroll">
87
+ <!-- Info notice -->
88
+ <div class="sb-section">
89
+ <div class="notice">
90
+ Requires the <strong>Node.js bridge server</strong> running
91
+ locally:<br />
92
+ <code>cd demos/websocket &amp;&amp; node server.js</code><br />
93
+ The bridge relays serial I/O over WebSocket JSON messages.
94
+ </div>
95
+ </div>
96
+
97
+ <!-- WebSocket URL -->
98
+ <div class="sb-section">
99
+ <div class="sb-title">Bridge URL</div>
100
+ <div class="field">
101
+ <label for="cfg-wsurl">WebSocket URL</label>
102
+ <input
103
+ type="url"
104
+ id="cfg-wsurl"
105
+ value="ws://localhost:8080"
106
+ placeholder="ws://localhost:8080"
107
+ />
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Connection settings -->
112
+ <div class="sb-section">
113
+ <div class="sb-title">Connection</div>
114
+
115
+ <div class="field">
116
+ <label for="cfg-baud">Baud Rate</label>
117
+ <select id="cfg-baud">
118
+ <option>300</option>
119
+ <option>600</option>
120
+ <option>1200</option>
121
+ <option>2400</option>
122
+ <option>4800</option>
123
+ <option value="9600" selected>9600</option>
124
+ <option>14400</option>
125
+ <option>19200</option>
126
+ <option>38400</option>
127
+ <option>57600</option>
128
+ <option>115200</option>
129
+ <option>230400</option>
130
+ <option>250000</option>
131
+ <option>500000</option>
132
+ <option>1000000</option>
133
+ </select>
134
+ </div>
135
+
136
+ <div class="field-row">
137
+ <div class="field">
138
+ <label for="cfg-databits">Data Bits</label>
139
+ <select id="cfg-databits">
140
+ <option value="7">7</option>
141
+ <option value="8" selected>8</option>
142
+ </select>
143
+ </div>
144
+ <div class="field">
145
+ <label for="cfg-stopbits">Stop Bits</label>
146
+ <select id="cfg-stopbits">
147
+ <option value="1" selected>1</option>
148
+ <option value="2">2</option>
149
+ </select>
150
+ </div>
151
+ </div>
152
+
153
+ <div class="field-row">
154
+ <div class="field">
155
+ <label for="cfg-parity">Parity</label>
156
+ <select id="cfg-parity">
157
+ <option value="none" selected>None</option>
158
+ <option value="even">Even</option>
159
+ <option value="odd">Odd</option>
160
+ </select>
161
+ </div>
162
+ <div class="field">
163
+ <label for="cfg-flow">Flow Control</label>
164
+ <select id="cfg-flow">
165
+ <option value="none" selected>None</option>
166
+ <option value="hardware">Hardware</option>
167
+ </select>
168
+ </div>
169
+ </div>
170
+
171
+ <div class="field-row">
172
+ <div class="field">
173
+ <label for="cfg-bufsize">Buffer Size</label>
174
+ <input
175
+ type="number"
176
+ id="cfg-bufsize"
177
+ value="255"
178
+ min="64"
179
+ max="65536"
180
+ />
181
+ </div>
182
+ <div class="field">
183
+ <label for="cfg-timeout">Cmd Timeout (ms)</label>
184
+ <input type="number" id="cfg-timeout" value="3000" min="0" />
185
+ </div>
186
+ </div>
187
+
188
+ <div class="field-row">
189
+ <div class="field">
190
+ <label for="cfg-handshake">Handshake (ms)</label>
191
+ <input type="number" id="cfg-handshake" value="2000" min="0" />
192
+ </div>
193
+ <div class="field">
194
+ <label for="cfg-reconnect-ms">Reconnect (ms)</label>
195
+ <input
196
+ type="number"
197
+ id="cfg-reconnect-ms"
198
+ value="1500"
199
+ min="500"
200
+ />
201
+ </div>
202
+ </div>
203
+
204
+ <div class="field-toggle">
205
+ <label for="cfg-autoreconnect">Auto Reconnect</label>
206
+ <label class="sw">
207
+ <input type="checkbox" id="cfg-autoreconnect" checked />
208
+ <span class="sw-knob"></span>
209
+ </label>
210
+ </div>
211
+ </div>
212
+
213
+ <!-- Message format -->
214
+ <div class="sb-section">
215
+ <div class="sb-title">Message Format</div>
216
+ <div class="field">
217
+ <label for="cfg-delim">Delimiter</label>
218
+ <input
219
+ type="text"
220
+ id="cfg-delim"
221
+ value="\n"
222
+ placeholder="\n \r\n | etc."
223
+ />
224
+ </div>
225
+ <div class="field-row">
226
+ <div class="field">
227
+ <label for="cfg-prepend">Prepend</label>
228
+ <input
229
+ type="text"
230
+ id="cfg-prepend"
231
+ value=""
232
+ placeholder="e.g. STX"
233
+ />
234
+ </div>
235
+ <div class="field">
236
+ <label for="cfg-append">Append</label>
237
+ <input
238
+ type="text"
239
+ id="cfg-append"
240
+ value="\n"
241
+ placeholder="e.g. \n"
242
+ />
243
+ </div>
244
+ </div>
245
+ </div>
246
+
247
+ <!-- Handshake -->
248
+ <div class="sb-section">
249
+ <div class="sb-title">Handshake on Connect</div>
250
+ <div class="notice" style="margin-bottom: 6px">
251
+ Sent automatically right after the WebSocket bridge connects.
252
+ Leave
253
+ <em>Command</em> empty to skip entirely.
254
+ </div>
255
+ <div class="field-row">
256
+ <div class="field">
257
+ <label for="cfg-hs-cmd">Command to send</label>
258
+ <input
259
+ type="text"
260
+ id="cfg-hs-cmd"
261
+ value=""
262
+ placeholder="e.g. PING\n or FF 01 A3"
263
+ />
264
+ </div>
265
+ <div class="field" style="flex: 0 0 62px">
266
+ <label for="cfg-hs-cmd-mode">Mode</label>
267
+ <select id="cfg-hs-cmd-mode">
268
+ <option value="text" selected>TXT</option>
269
+ <option value="hex">HEX</option>
270
+ </select>
271
+ </div>
272
+ </div>
273
+ <div class="field-row">
274
+ <div class="field">
275
+ <label for="cfg-hs-expect"
276
+ >Expected <small>(empty = no check)</small></label
277
+ >
278
+ <input
279
+ type="text"
280
+ id="cfg-hs-expect"
281
+ value=""
282
+ placeholder="e.g. PONG or OK"
283
+ />
284
+ </div>
285
+ <div class="field" style="flex: 0 0 62px">
286
+ <label for="cfg-hs-expect-mode">Mode</label>
287
+ <select id="cfg-hs-expect-mode">
288
+ <option value="text" selected>TXT</option>
289
+ <option value="hex">HEX</option>
290
+ </select>
291
+ </div>
292
+ </div>
293
+ </div>
294
+
295
+ <!-- Saved Commands -->
296
+ <div class="sb-section">
297
+ <div class="sb-title">Saved Commands</div>
298
+ <div class="notice" style="margin-bottom: 6px">
299
+ Pre-defined commands to send. Use <em>HEX</em> for raw bytes.
300
+ </div>
301
+ <div class="field-row">
302
+ <div class="field" style="flex: 0 0 72px">
303
+ <label for="cmd-name">Name</label>
304
+ <input type="text" id="cmd-name" placeholder="LED ON" />
305
+ </div>
306
+ <div class="field">
307
+ <label for="cmd-value">Command</label>
308
+ <input type="text" id="cmd-value" placeholder="LED_ON\n" />
309
+ </div>
310
+ <div class="field" style="flex: 0 0 62px">
311
+ <label for="cmd-mode">Mode</label>
312
+ <select id="cmd-mode">
313
+ <option value="text" selected>TXT</option>
314
+ <option value="hex">HEX</option>
315
+ </select>
316
+ </div>
317
+ </div>
318
+ <button
319
+ class="btn btn-ghost"
320
+ id="cmd-add"
321
+ style="width: 100%; margin-top: 3px"
322
+ >
323
+ <i data-lucide="plus"></i> Add Command
324
+ </button>
325
+ <div class="chip-list" id="cmd-list"></div>
326
+ </div>
327
+
328
+ <!-- Data Listeners -->
329
+ <div class="sb-section">
330
+ <div class="sb-title">Data Listeners</div>
331
+ <div class="notice" style="margin-bottom: 6px">
332
+ Patterns for <code>on('serial:data')</code> — skeleton code in
333
+ download.
334
+ </div>
335
+ <div class="field-row">
336
+ <div class="field" style="flex: 0 0 64px">
337
+ <label for="lst-name">Name</label>
338
+ <input type="text" id="lst-name" placeholder="Temp" />
339
+ </div>
340
+ <div class="field">
341
+ <label for="lst-pattern">Pattern</label>
342
+ <input type="text" id="lst-pattern" placeholder="TEMP:" />
343
+ </div>
344
+ <div class="field" style="flex: 0 0 62px">
345
+ <label for="lst-match">Match</label>
346
+ <select id="lst-match">
347
+ <option value="exact">exact</option>
348
+ <option value="contains" selected>has</option>
349
+ <option value="startsWith">starts</option>
350
+ <option value="hex">hex</option>
351
+ </select>
352
+ </div>
353
+ </div>
354
+ <button
355
+ class="btn btn-ghost"
356
+ id="lst-add"
357
+ style="width: 100%; margin-top: 3px"
358
+ >
359
+ <i data-lucide="plus"></i> Add Listener
360
+ </button>
361
+ <div class="chip-list" id="lst-list"></div>
362
+ </div>
363
+
364
+ <!-- Download -->
365
+ <div class="sb-section">
366
+ <div class="sb-title">Download Code</div>
367
+ <div class="field">
368
+ <label for="dl-name">Class / File name</label>
369
+ <input
370
+ type="text"
371
+ id="dl-name"
372
+ value="MyWsDevice"
373
+ placeholder="MyWsDevice"
374
+ />
375
+ </div>
376
+ <div class="dl-lang-row">
377
+ <label class="dl-opt"
378
+ ><input type="radio" name="dl-lang" value="ts" checked />
379
+ TypeScript</label
380
+ >
381
+ <label class="dl-opt"
382
+ ><input type="radio" name="dl-lang" value="js" />
383
+ JavaScript</label
384
+ >
385
+ </div>
386
+ <div class="dl-type-row">
387
+ <label class="dl-opt"
388
+ ><input type="radio" name="dl-type" value="file" checked />
389
+ Standalone file</label
390
+ >
391
+ <label class="dl-opt"
392
+ ><input type="radio" name="dl-type" value="project" /> Full
393
+ project (ZIP)</label
394
+ >
395
+ </div>
396
+ <div class="dl-btns">
397
+ <button class="btn btn-dl" id="dl-btn">
398
+ <i data-lucide="download"></i> Download
399
+ </button>
400
+ <button
401
+ class="btn btn-ghost"
402
+ id="cfg-export-btn"
403
+ title="Export configuration as JSON"
404
+ >
405
+ <i data-lucide="share"></i> Export config
406
+ </button>
407
+ <label
408
+ class="btn btn-ghost"
409
+ title="Import configuration from JSON"
410
+ >
411
+ <i data-lucide="upload"></i> Import config
412
+ <input
413
+ type="file"
414
+ id="cfg-import-input"
415
+ accept=".json"
416
+ style="display: none"
417
+ />
418
+ </label>
419
+ </div>
420
+ </div>
421
+ </div>
422
+ <!-- /sidebar-scroll -->
423
+ </aside>
424
+ <div class="sidebar-resize-handle" id="sidebar-resize-handle"></div>
425
+
426
+ <!-- ── Chat console ─────────────────────────────────── -->
427
+ <main class="console-area">
428
+ <div class="console-hdr">
429
+ <div class="status-pill">
430
+ <div class="status-dot" id="console-dot"></div>
431
+ <span id="console-text"
432
+ >Start the Node.js bridge, then click Connect</span
433
+ >
434
+ </div>
435
+ <button
436
+ class="btn btn-ghost"
437
+ id="clear-btn"
438
+ style="margin-left: auto; font-size: 0.7rem; padding: 4px 10px"
439
+ >
440
+ Clear
441
+ </button>
442
+ </div>
443
+
444
+ <div class="messages" id="messages">
445
+ <div class="empty-state">
446
+ <div class="empty-icon">🌐</div>
447
+ <span
448
+ >Run <strong>node server.js</strong> in demos/websocket/ then
449
+ connect</span
450
+ >
451
+ </div>
452
+ </div>
453
+
454
+ <div class="input-bar">
455
+ <button class="btn btn-mode" id="mode-toggle">TXT</button>
456
+ <input
457
+ class="msg-input"
458
+ id="input-send"
459
+ type="text"
460
+ placeholder="Type a command, e.g. LED_ON"
461
+ disabled
462
+ />
463
+ <button class="btn btn-send" id="btn-send" disabled>Send</button>
464
+ </div>
465
+ </main>
466
+
467
+ <!-- ── Code panel ────────────────────────────────────── -->
468
+ <div class="resize-handle" id="resize-handle"></div>
469
+ <aside class="code-panel" id="code-panel">
470
+ <div class="cp-hdr">
471
+ <div class="cp-title">
472
+ <span>Code Preview</span>
473
+ <span class="cp-tab" id="code-tab">device.ts</span>
474
+ </div>
475
+ <div class="cp-actions">
476
+ <button class="cp-btn" id="copy-btn">Copy</button>
477
+ </div>
478
+ </div>
479
+ <pre class="code-view" id="code-view"></pre>
480
+ </aside>
481
+ </div>
482
+ <!-- /app-layout -->
483
+
484
+ <footer class="app-footer">
485
+ © 2025
486
+ <a
487
+ href="https://github.com/danidoble"
488
+ style="color: inherit; text-decoration: none"
489
+ >@danidoble</a
490
+ >
491
+ · webserial-core v2
492
+ </footer>
493
+ <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
494
+ <script>
495
+ lucide.createIcons();
496
+ </script>
497
+ </body>
498
+ </html>
@@ -0,0 +1 @@
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.WebSerialCore={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=class{listeners={};on(e,t){return this.listeners[e]||(this.listeners[e]=new Set),this.listeners[e].add(t),this}off(e,t){return this.listeners[e]&&this.listeners[e].delete(t),this}emit(e,...t){let n=this.listeners[e];if(!n||n.size===0)return!1;for(let e of n)e(...t);return!0}},n=class{static instances=new Set;static portInstanceMap=new WeakMap;static getInstances(){return Array.from(this.instances)}static register(e){this.instances.add(e)}static unregister(e){this.instances.delete(e)}static isPortInUse(e,t){let n=this.portInstanceMap.get(e);return n!==void 0&&n!==t}static lockPort(e,t){this.portInstanceMap.set(e,t)}static unlockPort(e){this.portInstanceMap.delete(e)}},r=class{queue=[];isProcessing=!1;isPaused=!0;timeoutId=null;commandTimeout;onSend;onTimeout;constructor(e){this.commandTimeout=e.commandTimeout,this.onSend=e.onSend,this.onTimeout=e.onTimeout}get queueSize(){return this.queue.length}enqueue(e){this.queue.push(e),this.tryProcessNext()}advance(){this.clearCommandTimeout(),this.isProcessing=!1,this.tryProcessNext()}pause(){this.isPaused=!0,this.clearCommandTimeout(),this.isProcessing=!1}resume(){this.isPaused=!1,this.tryProcessNext()}clear(){this.queue=[],this.clearCommandTimeout(),this.isProcessing=!1}snapshot(){return[...this.queue]}restore(e){this.queue=[...e,...this.queue]}tryProcessNext(){if(this.isPaused||this.isProcessing||this.queue.length===0)return;this.isProcessing=!0;let e=this.queue.shift();this.commandTimeout>0&&(this.timeoutId=setTimeout(()=>{this.timeoutId=null,this.onTimeout(e),this.advance()},this.commandTimeout)),this.onSend(e).catch(()=>{this.advance()})}clearCommandTimeout(){this.timeoutId!==null&&(clearTimeout(this.timeoutId),this.timeoutId=null)}},i=class e extends Error{constructor(t){super(t),this.name=`SerialPortConflictError`,Object.setPrototypeOf(this,e.prototype)}},a=class e extends Error{constructor(t){super(t),this.name=`SerialPermissionError`,Object.setPrototypeOf(this,e.prototype)}},o=class e extends Error{constructor(t){super(t),this.name=`SerialTimeoutError`,Object.setPrototypeOf(this,e.prototype)}},s=class e extends Error{constructor(t){super(t),this.name=`SerialReadError`,Object.setPrototypeOf(this,e.prototype)}},c=class e extends Error{constructor(t){super(t),this.name=`SerialWriteError`,Object.setPrototypeOf(this,e.prototype)}},l=class e extends t{port=null;reader=null;writer=null;queue;options;isConnecting=!1;abortController=null;userInitiatedDisconnect=!1;reconnectTimerId=null;isHandshaking=!1;static customProvider=null;static polyfillOptions;constructor(e){super(),this.options={baudRate:e.baudRate,dataBits:e.dataBits??8,stopBits:e.stopBits??1,parity:e.parity??`none`,bufferSize:e.bufferSize??255,flowControl:e.flowControl??`none`,filters:e.filters??[],commandTimeout:e.commandTimeout??0,parser:e.parser,autoReconnect:e.autoReconnect??!1,autoReconnectInterval:e.autoReconnectInterval??1500,handshakeTimeout:e.handshakeTimeout??2e3,provider:e.provider,polyfillOptions:e.polyfillOptions},this.queue=new r({commandTimeout:this.options.commandTimeout,onSend:async e=>{await this.writeToPort(e),this.emit(`serial:sent`,e,this)},onTimeout:e=>{this.emit(`serial:timeout`,e,this)}}),this.on(`serial:data`,()=>{this.queue.advance()}),n.register(this)}async handshake(){return!0}async connect(){if(!this.isConnecting&&!this.port){this.isConnecting=!0,this.emit(`serial:connecting`,this);try{let t=this.getSerial();if(!t)throw Error(`Web Serial API is not supported in this browser. Use AbstractSerialDevice.setProvider() to set a WebUSB polyfill.`);if(this.port=await this.findAndValidatePort(),!this.port){let n;try{n=await t.requestPort({filters:this.options.filters},this.options.polyfillOptions??e.polyfillOptions)}catch(e){throw e instanceof DOMException&&(e.name===`NotFoundError`||e.name===`SecurityError`||e.name===`AbortError`)?new a(e instanceof Error?e.message:String(e)):e instanceof Error?e:Error(String(e))}if(!await this.openAndHandshake(n))throw Error(`Handshake failed: the selected device did not respond correctly.`);this.port=n}this.abortController=new AbortController,this.queue.resume(),this.emit(`serial:connected`,this)}catch(e){if(e instanceof a?this.emit(`serial:need-permission`,this):this.emit(`serial:error`,e instanceof Error?e:Error(String(e)),this),this.port){n.unlockPort(this.port);try{await this.port.close()}catch{}this.port=null}throw e}finally{this.isConnecting=!1}}}async disconnect(){this.port&&(this.userInitiatedDisconnect=!0,this.stopReconnecting(),await this.cleanupPort())}async cleanupPort(){if(this.port){this.queue.pause(),this.abortController?.abort(),this.abortController=null;try{let e=this.reader,t=this.writer;if(this.reader=null,this.writer=null,e){try{await e.cancel()}catch{}try{e.releaseLock()}catch{}}if(t){try{await t.close()}catch{}try{t.releaseLock()}catch{}}try{await this.port.close()}catch{}}catch(e){this.emit(`serial:error`,e instanceof Error?e:Error(String(e)),this)}finally{this.port&&n.unlockPort(this.port),this.port=null,this.options.parser?.reset?.(),this.emit(`serial:disconnected`,this),!this.userInitiatedDisconnect&&this.options.autoReconnect&&this.startReconnecting(),this.userInitiatedDisconnect=!1}}}async forget(){await this.disconnect(),this.port&&typeof this.port.forget==`function`&&await this.port.forget(),n.unregister(this)}async send(e){let t;t=typeof e==`string`?new TextEncoder().encode(e):e,t.length>0&&this.queue.enqueue(t)}clearQueue(){this.queue.clear(),this.emit(`serial:queue-empty`,this)}async writeToPort(e){if(!this.port||!this.port.writable)throw new c(`Port not writable.`);this.writer=this.port.writable.getWriter();try{await this.writer.write(e)}catch(e){throw new c(e instanceof Error?e.message:String(e))}finally{this.writer.releaseLock(),this.writer=null}}async readLoop(){if(!(!this.port||!this.port.readable)&&!this.reader){this.reader=this.port.readable.getReader();try{for(;;){let{value:e,done:t}=await this.reader.read();if(t)break;e&&(this.options.parser?this.options.parser.parse(e,e=>{this.emit(`serial:data`,e,this)}):this.emit(`serial:data`,e,this))}}catch(e){if(this.port)throw new s(e instanceof Error?e.message:String(e))}finally{if(this.reader){try{this.reader.releaseLock()}catch{}this.reader=null}}}}async openAndHandshake(e){let t=this;if(n.isPortInUse(e,t))return!1;n.lockPort(e,t);try{await e.open({baudRate:this.options.baudRate,dataBits:this.options.dataBits,stopBits:this.options.stopBits,parity:this.options.parity,bufferSize:this.options.bufferSize,flowControl:this.options.flowControl})}catch(t){throw n.unlockPort(e),t instanceof Error?t:Error(String(t))}this.port=e,this.abortController=new AbortController;let r=this.queue.snapshot();this.isHandshaking=!0,this.readLoop().catch(e=>{!this.isHandshaking&&this.port&&(this.emit(`serial:error`,e,this),this.cleanupPort())}),this.queue.resume();try{let t=await this.runHandshakeWithTimeout();return this.isHandshaking=!1,t?(this.queue.pause(),this.queue.clear(),this.queue.restore(r),this.options.parser?.reset?.(),!0):(await this.teardownHandshake(e,r),!1)}catch{return this.isHandshaking=!1,await this.teardownHandshake(e,r),!1}}async teardownHandshake(e,t){this.queue.pause(),this.queue.clear(),this.queue.restore(t),await this.stopReader(),this.port=null,this.abortController=null,this.options.parser?.reset?.();try{await e.close()}catch{}n.unlockPort(e)}async stopReader(){let e=this.reader;if(this.reader=null,e){try{await e.cancel()}catch{}try{e.releaseLock()}catch{}}}async runHandshakeWithTimeout(){let e=this.options.handshakeTimeout??2e3;return Promise.race([this.handshake(),new Promise(t=>setTimeout(()=>t(!1),e))])}async findAndValidatePort(){let t=this.getSerial();if(!t)return null;let r=await t.getPorts(this.options.polyfillOptions??e.polyfillOptions);if(r.length===0)return null;let i=this.options.filters??[],a=this;for(let e of r)if(!n.isPortInUse(e,a)){if(i.length>0){let t=e.getInfo();if(!i.some(e=>{let n=e.usbVendorId===void 0||e.usbVendorId===t.usbVendorId,r=e.usbProductId===void 0||e.usbProductId===t.usbProductId;return n&&r}))continue}try{if(await this.openAndHandshake(e))return e}catch{}}return null}startReconnecting(){this.reconnectTimerId||=(this.emit(`serial:reconnecting`,this),setInterval(async()=>{if(this.port||this.isConnecting){this.stopReconnecting();return}try{let e=await this.findAndValidatePort();e&&(this.stopReconnecting(),await this.reconnect(e))}catch{}},this.options.autoReconnectInterval))}stopReconnecting(){this.reconnectTimerId&&=(clearInterval(this.reconnectTimerId),null)}async reconnect(e){if(!(this.isConnecting||this.port)){this.isConnecting=!0,this.emit(`serial:connecting`,this);try{this.port=e,this.abortController=new AbortController,this.queue.resume(),this.emit(`serial:connected`,this)}catch(e){this.emit(`serial:error`,e instanceof Error?e:Error(String(e)),this),this.port&&=(n.unlockPort(this.port),null),this.options.autoReconnect&&this.startReconnecting()}finally{this.isConnecting=!1}}}static getInstances(){return n.getInstances()}static async connectAll(){let e=n.getInstances();for(let t of e)try{await t.connect()}catch{}}static setProvider(t,n){e.customProvider=t,e.polyfillOptions=n}getSerial(){return this.options.provider?this.options.provider:e.customProvider?e.customProvider:typeof navigator<`u`&&navigator.serial?navigator.serial:null}};function u(e){if(e<=0)throw Error(`FixedLengthParser: length must be greater than 0`);let t=new Uint8Array;return{parse(n,r){let i=new Uint8Array(t.length+n.length);for(i.set(t),i.set(n,t.length),t=i;t.length>=e;)r(t.slice(0,e)),t=t.slice(e)},reset(){t=new Uint8Array}}}function d(e){let t=``,n=new TextDecoder;return{parse(r,i){t+=n.decode(r,{stream:!0});let a;for(;(a=t.indexOf(e))!==-1;)i(t.slice(0,a)),t=t.slice(a+e.length)},reset(){t=``,n=new TextDecoder}}}function f(){return{parse(e,t){t(e)},reset(){}}}var p=32,m=34,h=0,g=30,_=3,v=7,y=1,b=0,x=771,S=768,C=255,w=8,T=`none`,E=1,D=[16,8,7,6,5],O=[1,2],k=[`none`,`even`,`odd`],A=[`none`,`odd`,`even`],j=[1,1.5,2],M={usbControlInterfaceClass:2,usbTransferInterfaceClass:10,protocol:void 0};function N(e,t){let n=e.configurations[0];if(!n)return null;for(let e of n.interfaces)if(e.alternates[0]?.interfaceClass===t)return e;return null}function P(e,t){let n=e.configurations[0];if(!n)return null;for(let e of n.interfaces){let n=e.alternates[0];if(!n||n.interfaceClass!==t)continue;let r=n.endpoints.some(e=>e.direction===`in`),i=n.endpoints.some(e=>e.direction===`out`);if(r&&i)return e}return null}function F(e,t){let n=e.alternates[0];if(n){for(let e of n.endpoints)if(e.direction===t)return e}throw TypeError(`Interface ${e.interfaceNumber} does not have an ${t} endpoint.`)}function I(e,t){return t===2?`cdc_acm`:e.vendorId===4292?`cp210x`:`none`}var L=class{device_;endpoint_;onError_;constructor(e,t,n){this.device_=e,this.endpoint_=t,this.onError_=n}pull(e){(async()=>{let t=this.endpoint_.packetSize;try{let n=await this.device_.transferIn(this.endpoint_.endpointNumber,t);if(n.status!==`ok`){e.error(`USB error: ${n.status}`),this.onError_();return}if(n.data?.buffer&&n.data.byteLength>0){let t=new Uint8Array(n.data.buffer,n.data.byteOffset,n.data.byteLength);t.length>0&&e.enqueue(t)}}catch(t){e.error(String(t)),this.onError_()}})()}},R=class{device_;endpoint_;onError_;constructor(e,t,n){this.device_=e,this.endpoint_=t,this.onError_=n}async write(e,t){try{let n=await this.device_.transferOut(this.endpoint_.endpointNumber,e.buffer);n.status!==`ok`&&(t.error(n.status),this.onError_())}catch(e){t.error(String(e)),this.onError_()}}},z=class{device_;protocol_;controlInterface_;transferInterface_;inEndpoint_;outEndpoint_;serialOptions_;readable_=null;writable_=null;cdcOutputSignals_={dataTerminalReady:!1,requestToSend:!1,break:!1};constructor(e,t){this.device_=e;let n={...M,...t};this.protocol_=n.protocol??I(e,n.usbControlInterfaceClass);let r=n.usbControlInterfaceClass,i=n.usbTransferInterfaceClass;if(r===i){let t=P(e,i);if(!t)throw TypeError(`Unable to find interface with class ${i} that has both IN and OUT endpoints.`);this.controlInterface_=t,this.transferInterface_=t}else{let t=N(e,r);if(!t)throw TypeError(`Unable to find control interface with class ${r}.`);let n=P(e,i)??N(e,i);if(!n)throw TypeError(`Unable to find transfer interface with class ${i}.`);this.controlInterface_=t,this.transferInterface_=n}this.inEndpoint_=F(this.transferInterface_,`in`),this.outEndpoint_=F(this.transferInterface_,`out`)}get readable(){return!this.readable_&&this.device_.opened&&(this.readable_=new ReadableStream(new L(this.device_,this.inEndpoint_,()=>{this.readable_=null}),{highWaterMark:this.serialOptions_?.bufferSize??C})),this.readable_}get writable(){return!this.writable_&&this.device_.opened&&(this.writable_=new WritableStream(new R(this.device_,this.outEndpoint_,()=>{this.writable_=null}),new ByteLengthQueuingStrategy({highWaterMark:this.serialOptions_?.bufferSize??C}))),this.writable_}async open(e){this.serialOptions_=e,this.validateOptions();try{switch(await this.device_.open(),this.device_.configuration===null&&await this.device_.selectConfiguration(1),await this.device_.claimInterface(this.controlInterface_.interfaceNumber),this.controlInterface_!==this.transferInterface_&&await this.device_.claimInterface(this.transferInterface_.interfaceNumber),this.protocol_){case`cdc_acm`:await this.cdcInit();break;case`cp210x`:await this.cp210xInit();break;case`none`:break}}catch(e){throw this.device_.opened&&await this.device_.close(),Error(`Error setting up device: `+(e instanceof Error?e.message:String(e)),{cause:e})}}async close(){let e=[];if(this.readable_&&e.push(this.readable_.cancel()),this.writable_&&e.push(this.writable_.abort()),await Promise.all(e),this.readable_=null,this.writable_=null,this.device_.opened){switch(this.protocol_){case`cdc_acm`:await this.cdcSetSignals({dataTerminalReady:!1,requestToSend:!1});break;case`cp210x`:await this.cp210xDeinit();break}await this.device_.close()}}async forget(){return this.device_.forget()}getInfo(){return{usbVendorId:this.device_.vendorId,usbProductId:this.device_.productId}}async cdcInit(){await this.cdcSetLineCoding(),await this.cdcSetSignals({dataTerminalReady:!0})}async cdcSetSignals(e){if(this.cdcOutputSignals_={...this.cdcOutputSignals_,...e},e.dataTerminalReady!==void 0||e.requestToSend!==void 0){let e=(this.cdcOutputSignals_.dataTerminalReady?1:0)|(this.cdcOutputSignals_.requestToSend?2:0);await this.device_.controlTransferOut({requestType:`class`,recipient:`interface`,request:m,value:e,index:this.controlInterface_.interfaceNumber})}}async cdcSetLineCoding(){let e=new ArrayBuffer(7),t=new DataView(e);if(t.setUint32(0,this.serialOptions_.baudRate,!0),t.setUint8(4,j.indexOf(this.serialOptions_.stopBits??E)),t.setUint8(5,A.indexOf(this.serialOptions_.parity??T)),t.setUint8(6,this.serialOptions_.dataBits??w),(await this.device_.controlTransferOut({requestType:`class`,recipient:`interface`,request:p,value:0,index:this.controlInterface_.interfaceNumber},e)).status!==`ok`)throw new DOMException(`Failed to set line coding.`,`NetworkError`)}async cp210xInit(){let e=this.controlInterface_.interfaceNumber;await this.device_.controlTransferOut({requestType:`vendor`,recipient:`interface`,request:h,value:y,index:e});let t=new ArrayBuffer(4);new DataView(t).setUint32(0,this.serialOptions_.baudRate,!0),await this.device_.controlTransferOut({requestType:`vendor`,recipient:`interface`,request:g,value:0,index:e},t);let n=this.serialOptions_.dataBits??w,r={none:0,odd:16,even:32}[this.serialOptions_.parity??T]??0,i=({1:0,2:2}[this.serialOptions_.stopBits??E]??0)<<8|r|n;await this.device_.controlTransferOut({requestType:`vendor`,recipient:`interface`,request:_,value:i,index:e}),await this.device_.controlTransferOut({requestType:`vendor`,recipient:`interface`,request:v,value:x,index:e})}async cp210xDeinit(){let e=this.controlInterface_.interfaceNumber;await this.device_.controlTransferOut({requestType:`vendor`,recipient:`interface`,request:v,value:S,index:e}),await this.device_.controlTransferOut({requestType:`vendor`,recipient:`interface`,request:h,value:b,index:e})}validateOptions(){if(this.serialOptions_.baudRate%1!=0)throw RangeError(`Invalid baud rate: ${this.serialOptions_.baudRate}`);if(this.serialOptions_.dataBits!==void 0&&!D.includes(this.serialOptions_.dataBits))throw RangeError(`Invalid dataBits: ${this.serialOptions_.dataBits}`);if(this.serialOptions_.stopBits!==void 0&&!O.includes(this.serialOptions_.stopBits))throw RangeError(`Invalid stopBits: ${this.serialOptions_.stopBits}`);if(this.serialOptions_.parity!==void 0&&!k.includes(this.serialOptions_.parity))throw RangeError(`Invalid parity: ${this.serialOptions_.parity}`)}},B=class{options_;constructor(e){this.options_={...M,...e}}async requestPort(e,t){let n={...this.options_,...t},r=[];if(e?.filters&&e.filters.length>0)for(let t of e.filters){let e={};t.usbVendorId!==void 0&&(e.vendorId=t.usbVendorId),t.usbProductId!==void 0&&(e.productId=t.usbProductId),n.usbControlInterfaceClass!==void 0&&n.usbControlInterfaceClass!==255?e.classCode=n.usbControlInterfaceClass:e.vendorId===void 0&&e.productId===void 0&&(e.classCode=n.usbControlInterfaceClass??2),r.push(e)}else r.push({classCode:n.usbControlInterfaceClass??2});return new z(await navigator.usb.requestDevice({filters:r}),n)}async getPorts(e){let t={...this.options_,...e},n=await navigator.usb.getDevices(),r=[];for(let e of n)try{let n=new z(e,t);r.push(n)}catch{}return r}},V=`6e400001-b5a3-f393-e0a9-e50e24dcca9e`,H=`6e400003-b5a3-f393-e0a9-e50e24dcca9e`,U=`6e400002-b5a3-f393-e0a9-e50e24dcca9e`,W=20,G=10;function K(e){let t=null,n=null,r=null;return{get readable(){return t},get writable(){return n},getInfo(){return{}},async open(){if(!e.gatt)throw Error(`GATT not available on this Bluetooth device.`);r=await e.gatt.connect();let i=await r.getPrimaryService(V),a=await i.getCharacteristic(H),o=await i.getCharacteristic(U);await a.startNotifications(),t=new ReadableStream({start(e){a.addEventListener(`characteristicvaluechanged`,t=>{let n=t.target.value.buffer;e.enqueue(new Uint8Array(n))})}}),n=new WritableStream({async write(e){for(let t=0;t<e.length;t+=W){let n=e.slice(t,t+W);await o.writeValueWithoutResponse(n),t+W<e.length&&await new Promise(e=>setTimeout(e,G))}}})},async close(){r?.connected&&r.disconnect(),t=null,n=null}}}function q(){return{async requestPort(){if(!navigator.bluetooth)throw Error(`Web Bluetooth API is not supported in this browser. Use Chrome on Android, macOS, or ChromeOS.`);return K(await navigator.bluetooth.requestDevice({filters:[{services:[V]}]}))},async getPorts(){return[]}}}function J(e){return new Promise((t,n)=>{e.addEventListener(`open`,()=>t(),{once:!0}),e.addEventListener(`error`,e=>n(e),{once:!0})})}function Y(e,t){return new Promise(n=>{let r=i=>{let a=JSON.parse(i.data);a.type===t&&(e.removeEventListener(`message`,r),n(a.payload))};e.addEventListener(`message`,r)})}function X(e,t){let n=null,r=null;return{get readable(){return n},get writable(){return r},getInfo(){return{usbVendorId:t.vendorId,usbProductId:t.productId}},async open(i){e.send(JSON.stringify({type:`open`,path:t.path,baudRate:i.baudRate,dataBits:i.dataBits,stopBits:i.stopBits,parity:i.parity,parser:{type:`delimiter`,value:`\\n`}})),await Y(e,`opened`);let a=[],o=null,s=!1;function c(e){let t=JSON.parse(e.data);if(t.type===`data`&&t.bytes){let e=new Uint8Array(t.bytes);o?o.enqueue(e):a.push(e)}t.type===`closed`&&(s=!0,o&&o.close())}e.addEventListener(`message`,c),n=new ReadableStream({start(e){o=e;for(let t of a)e.enqueue(t);a.length=0,s&&e.close()},cancel(){e.removeEventListener(`message`,c),o=null}}),r=new WritableStream({write(t){e.send(JSON.stringify({type:`write`,bytes:Array.from(t)}))}})},async close(){e.send(JSON.stringify({type:`close`})),n=null,r=null,e.close()}}}function Z(e){return{async requestPort(t){let n=new WebSocket(e);await J(n),n.send(JSON.stringify({type:`list-ports`,filters:t?.filters??[]}));let r=(await Y(n,`port-list`))[0];if(!r)throw Error(`No ports available on the bridge server. Make sure the Node.js server is running and a device is connected.`);return X(n,r)},async getPorts(){let t=new WebSocket(e);return await J(t),t.send(JSON.stringify({type:`list-ports`,filters:[]})),(await Y(t,`port-list`)).map(e=>X(t,e))}}}e.AbstractSerialDevice=l,e.CommandQueue=r,e.SerialEventEmitter=t,e.SerialPermissionError=a,e.SerialPortConflictError=i,e.SerialReadError=s,e.SerialRegistry=n,e.SerialTimeoutError=o,e.SerialWriteError=c,e.WebUsbProvider=B,e.createBluetoothProvider=q,e.createWebSocketProvider=Z,e.delimiter=d,e.fixedLength=u,e.raw=f});
@@ -0,0 +1,10 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="40" height="40" fill="none">
2
+ <!-- Dark circle background -->
3
+ <circle cx="20" cy="20" r="19" fill="#1a1527"/>
4
+ <!-- Left angle bracket -->
5
+ <polyline points="10,13 6,20 10,27" stroke="#a78bfa" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
6
+ <!-- Right angle bracket -->
7
+ <polyline points="30,13 34,20 30,27" stroke="#22c55e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
8
+ <!-- Serial data line (dashes) -->
9
+ <line x1="14" y1="20" x2="26" y2="20" stroke="#a78bfa" stroke-width="2" stroke-dasharray="3 2" stroke-linecap="round"/>
10
+ </svg>