webserial-core 2.0.0 → 2.0.2

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.
@@ -43,6 +43,23 @@ export declare abstract class AbstractSerialDevice<T> extends SerialEventEmitter
43
43
  protected handshake(): Promise<boolean>;
44
44
  connect(): Promise<void>;
45
45
  disconnect(): Promise<void>;
46
+ /**
47
+ * Returns `true` if the device is currently connected and the port, reader,
48
+ * and writer are all valid. Note that a port may be "connected" at the
49
+ * Web Serial API level but still have an invalid reader/writer if the device
50
+ * was unplugged or is in a bad state. This method is useful for checking
51
+ * overall health of the connection before attempting to send data.
52
+ * @returns `true` if the device is connected and ready for communication, `false` otherwise.
53
+ */
54
+ isConnected(): boolean;
55
+ /**
56
+ * Returns `true` if the device is not connected or in the process of connecting.
57
+ * This is a convenience method equivalent to `!isConnected()`, but may be more
58
+ * semantically clear in certain contexts (e.g. when checking for disconnection
59
+ * in a read loop catch block).
60
+ * @returns `true` if the device is disconnected or not ready, `false` if it is currently connected.
61
+ */
62
+ isDisconnected(): boolean;
46
63
  /**
47
64
  * Internal cleanup: tears down the port, reader, writer without
48
65
  * marking it as user-initiated. This allows auto-reconnect to trigger.
@@ -0,0 +1,255 @@
1
+ /* Shared styles for all webserial-core demos.
2
+ * Each demo HTML sets its own CSS custom properties in an inline <style> block:
3
+ * --accent, --accent-dark, --accent-badge, --log-event
4
+ * --notice-bg, --notice-border, --notice-text, --notice-code-bg (optional)
5
+ */
6
+
7
+ *,
8
+ *::before,
9
+ *::after {
10
+ box-sizing: border-box;
11
+ margin: 0;
12
+ padding: 0;
13
+ }
14
+
15
+ body {
16
+ font-family:
17
+ "Segoe UI",
18
+ system-ui,
19
+ -apple-system,
20
+ sans-serif;
21
+ background: #0f0f12;
22
+ color: #e4e4e7;
23
+ min-height: 100vh;
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ }
28
+
29
+ #app {
30
+ width: min(640px, 94vw);
31
+ background: #18181b;
32
+ border: 1px solid #27272a;
33
+ border-radius: 16px;
34
+ overflow: hidden;
35
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
36
+ }
37
+
38
+ header {
39
+ padding: 20px 24px;
40
+ border-bottom: 1px solid #27272a;
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 12px;
44
+ }
45
+
46
+ header h1 {
47
+ font-size: 1.15rem;
48
+ font-weight: 600;
49
+ color: var(--accent);
50
+ }
51
+
52
+ header span {
53
+ font-size: 0.8rem;
54
+ color: #71717a;
55
+ }
56
+
57
+ .badge {
58
+ background: var(--accent);
59
+ color: var(--accent-badge);
60
+ font-size: 0.65rem;
61
+ font-weight: 700;
62
+ padding: 2px 8px;
63
+ border-radius: 999px;
64
+ text-transform: uppercase;
65
+ letter-spacing: 0.05em;
66
+ }
67
+
68
+ .controls {
69
+ display: flex;
70
+ gap: 8px;
71
+ padding: 16px 24px;
72
+ border-bottom: 1px solid #27272a;
73
+ flex-wrap: wrap;
74
+ }
75
+
76
+ button {
77
+ padding: 8px 18px;
78
+ border: none;
79
+ border-radius: 8px;
80
+ cursor: pointer;
81
+ font-size: 0.85rem;
82
+ font-weight: 500;
83
+ transition:
84
+ background 0.2s,
85
+ opacity 0.2s;
86
+ }
87
+
88
+ button:disabled {
89
+ opacity: 0.4;
90
+ cursor: not-allowed;
91
+ }
92
+
93
+ #btn-connect {
94
+ background: var(--accent);
95
+ color: var(--accent-badge);
96
+ }
97
+
98
+ #btn-connect:hover:not(:disabled) {
99
+ background: var(--accent-dark);
100
+ }
101
+
102
+ #btn-disconnect {
103
+ background: #ef4444;
104
+ color: #fff;
105
+ }
106
+
107
+ #btn-disconnect:hover:not(:disabled) {
108
+ background: #dc2626;
109
+ }
110
+
111
+ .notice {
112
+ margin: 0 24px 16px;
113
+ padding: 14px 16px;
114
+ background: var(--notice-bg, #161616);
115
+ border: 1px solid var(--notice-border, #3f3f46);
116
+ border-left: 3px solid var(--accent);
117
+ border-radius: 8px;
118
+ font-size: 0.78rem;
119
+ line-height: 1.6;
120
+ color: var(--notice-text, #e4e4e7);
121
+ }
122
+
123
+ .notice code {
124
+ background: var(--notice-code-bg, #0f0f0f);
125
+ padding: 1px 5px;
126
+ border-radius: 4px;
127
+ font-family: monospace;
128
+ }
129
+
130
+ .info-text {
131
+ margin: 0 24px 4px;
132
+ padding: 10px 0;
133
+ font-size: 0.8rem;
134
+ line-height: 1.6;
135
+ color: #a1a1aa;
136
+ border-bottom: 1px solid #27272a;
137
+ }
138
+
139
+ .info-text code {
140
+ background: #09090b;
141
+ color: #e4e4e7;
142
+ padding: 1px 5px;
143
+ border-radius: 4px;
144
+ font-family: monospace;
145
+ font-size: 0.78rem;
146
+ }
147
+
148
+ .send-row {
149
+ display: flex;
150
+ gap: 8px;
151
+ padding: 0 24px 16px;
152
+ }
153
+
154
+ .send-row input {
155
+ flex: 1;
156
+ padding: 8px 12px;
157
+ border-radius: 8px;
158
+ border: 1px solid #3f3f46;
159
+ background: #09090b;
160
+ color: #e4e4e7;
161
+ font-size: 0.85rem;
162
+ outline: none;
163
+ transition: border-color 0.2s;
164
+ }
165
+
166
+ .send-row input:focus {
167
+ border-color: var(--accent);
168
+ }
169
+
170
+ .send-row input:disabled {
171
+ opacity: 0.4;
172
+ }
173
+
174
+ #btn-send {
175
+ background: #6366f1;
176
+ color: #fff;
177
+ }
178
+
179
+ #btn-send:hover:not(:disabled) {
180
+ background: #4f46e5;
181
+ }
182
+
183
+ #mode-toggle {
184
+ background: var(--accent);
185
+ color: var(--accent-badge);
186
+ font-weight: 700;
187
+ min-width: 54px;
188
+ }
189
+
190
+ #mode-toggle:hover {
191
+ background: var(--accent-dark);
192
+ }
193
+
194
+ #log {
195
+ height: 340px;
196
+ overflow-y: auto;
197
+ padding: 16px 24px;
198
+ font-family: "Fira Code", "Cascadia Code", monospace;
199
+ font-size: 0.8rem;
200
+ line-height: 1.7;
201
+ background: #09090b;
202
+ }
203
+
204
+ .log-line {
205
+ white-space: pre-wrap;
206
+ word-break: break-all;
207
+ }
208
+
209
+ .log-info {
210
+ color: #a1a1aa;
211
+ }
212
+
213
+ .log-event {
214
+ color: var(--log-event, #38bdf8);
215
+ }
216
+
217
+ .log-data {
218
+ color: #4ade80;
219
+ }
220
+
221
+ .log-error {
222
+ color: #f87171;
223
+ }
224
+
225
+ #log::-webkit-scrollbar {
226
+ width: 6px;
227
+ }
228
+
229
+ #log::-webkit-scrollbar-track {
230
+ background: transparent;
231
+ }
232
+
233
+ #log::-webkit-scrollbar-thumb {
234
+ background: #3f3f46;
235
+ border-radius: 3px;
236
+ }
237
+
238
+ .nav-link {
239
+ display: block;
240
+ text-align: center;
241
+ padding: 10px;
242
+ color: #71717a;
243
+ font-size: 0.78rem;
244
+ text-decoration: none;
245
+ border-top: 1px solid #27272a;
246
+ }
247
+
248
+ .nav-link:hover {
249
+ color: var(--accent);
250
+ }
251
+
252
+ .nav-link + .nav-link {
253
+ border-top: none;
254
+ padding-top: 0;
255
+ }
@@ -0,0 +1,272 @@
1
+ (function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var e=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}},t=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)}},n=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)}},r=class e extends Error{constructor(t){super(t),this.name=`SerialPermissionError`,Object.setPrototypeOf(this,e.prototype)}},i=class e extends Error{constructor(t){super(t),this.name=`SerialReadError`,Object.setPrototypeOf(this,e.prototype)}},a=class e extends Error{constructor(t){super(t),this.name=`SerialWriteError`,Object.setPrototypeOf(this,e.prototype)}},o=class o extends e{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 n({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()}),t.register(this)}async handshake(){return!0}async connect(){if(!this.isConnecting&&!this.port){this.isConnecting=!0,this.emit(`serial:connecting`,this);try{let e=this.getSerial();if(!e)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 t;try{t=await e.requestPort({filters:this.options.filters},this.options.polyfillOptions??o.polyfillOptions)}catch(e){throw e instanceof DOMException&&(e.name===`NotFoundError`||e.name===`SecurityError`||e.name===`AbortError`)?new r(e instanceof Error?e.message:String(e)):e instanceof Error?e:Error(String(e))}if(!await this.openAndHandshake(t))throw Error(`Handshake failed: the selected device did not respond correctly.`);this.port=t}this.abortController=new AbortController,this.queue.resume(),this.emit(`serial:connected`,this)}catch(e){if(e instanceof r?this.emit(`serial:need-permission`,this):this.emit(`serial:error`,e instanceof Error?e:Error(String(e)),this),this.port){t.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&&t.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(),t.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 a(`Port not writable.`);this.writer=this.port.writable.getWriter();try{await this.writer.write(e)}catch(e){throw new a(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 i(e instanceof Error?e.message:String(e))}finally{if(this.reader){try{this.reader.releaseLock()}catch{}this.reader=null}}}}async openAndHandshake(e){let n=this;if(t.isPortInUse(e,n))return!1;t.lockPort(e,n);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(n){throw t.unlockPort(e),n instanceof Error?n:Error(String(n))}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,n){this.queue.pause(),this.queue.clear(),this.queue.restore(n),await this.stopReader(),this.port=null,this.abortController=null,this.options.parser?.reset?.();try{await e.close()}catch{}t.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 e=this.getSerial();if(!e)return null;let n=await e.getPorts(this.options.polyfillOptions??o.polyfillOptions);if(n.length===0)return null;let r=this.options.filters??[],i=this;for(let e of n)if(!t.isPortInUse(e,i)){if(r.length>0){let t=e.getInfo();if(!r.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&&=(t.unlockPort(this.port),null),this.options.autoReconnect&&this.startReconnecting()}finally{this.isConnecting=!1}}}static getInstances(){return t.getInstances()}static async connectAll(){let e=t.getInstances();for(let t of e)try{await t.connect()}catch{}}static setProvider(e,t){o.customProvider=e,o.polyfillOptions=t}getSerial(){return this.options.provider?this.options.provider:o.customProvider?o.customProvider:typeof navigator<`u`&&navigator.serial?navigator.serial:null}};function s(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 c(){return{parse(e,t){t(e)},reset(){}}}var l=`wsc-demo-theme`;function u(){return window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function d(){let e=localStorage.getItem(l)??u();return document.documentElement.setAttribute(`data-theme`,e),window.matchMedia(`(prefers-color-scheme: dark)`).addEventListener(`change`,e=>{localStorage.getItem(l)||document.documentElement.setAttribute(`data-theme`,e.matches?`dark`:`light`)}),e}function f(){let e=(document.documentElement.getAttribute(`data-theme`)??u())===`dark`?`light`:`dark`;return localStorage.setItem(l,e),document.documentElement.setAttribute(`data-theme`,e),e}function p(e,t,n){let r=e.querySelector(`.empty-state`);r&&r.remove();let{kind:i,label:a,time:o=new Date}=n,s=document.createElement(`div`);if(s.className=`msg ${i}`,a&&(i===`sent`||i===`received`)){let e=document.createElement(`div`);e.className=`msg-label`,e.textContent=a,s.appendChild(e)}let c=document.createElement(`div`);c.className=`msg-bubble`,c.textContent=t,s.appendChild(c);let l=document.createElement(`div`);l.className=`msg-time`,l.textContent=o.toLocaleTimeString([],{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}),s.appendChild(l),e.appendChild(s),e.scrollTop=e.scrollHeight}function m(e){e.innerHTML=`
2
+ <div class="empty-state">
3
+ <div class="empty-icon">💬</div>
4
+ <span>Messages cleared</span>
5
+ </div>`}var h=new Set(`import.export.from.default.as.class.extends.implements.constructor.super.new.this.return.const.let.var.async.await.function.protected.public.private.static.abstract.interface.type.enum.namespace.declare.readonly.true.false.null.undefined.void.never.any.unknown.if.else.for.while.do.switch.case.break.continue.try.catch.finally.throw.typeof.instanceof.in.of.keyof.infer.string.number.boolean.object.symbol.bigint.Promise.Array.Set.Map`.split(`.`));function g(e){return e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`)}function _(e){let t=``,n=0,r=e.length;for(;n<r;){if(e[n]===`/`&&e[n+1]===`/`){t+=`<span class="t-cmt">${g(e.slice(n))}</span>`;break}let i=e[n];if(i===`'`||i===`"`||i==="`"){let a=n+1;for(;a<r;){if(e[a]===`\\`&&a+1<r){a+=2;continue}if(e[a]===i){a++;break}a++}t+=`<span class="t-str">${g(e.slice(n,a))}</span>`,n=a;continue}if(/\d/.test(e[n])&&(n===0||!/\w/.test(e[n-1]))){let i=n;for(;i<r&&/[\d.xXa-fA-F_n]/.test(e[i]);)i++;t+=`<span class="t-num">${g(e.slice(n,i))}</span>`,n=i;continue}if(/[a-zA-Z_$]/.test(e[n])){let i=n;for(;i<r&&/[\w$]/.test(e[i]);)i++;let a=e.slice(n,i);h.has(a)?t+=`<span class="t-kw">${g(a)}</span>`:i<r&&e[i]===`(`?t+=`<span class="t-fn">${g(a)}</span>`:/^[A-Z]/.test(a)?t+=`<span class="t-cls">${g(a)}</span>`:t+=`<span class="t-var">${g(a)}</span>`,n=i;continue}t+=g(e[n]),n++}return t}function v(e,t){let n=t.split(`
6
+ `);e.innerHTML=``,n.forEach((t,n)=>{let r=document.createElement(`div`);r.className=`code-line`;let i=document.createElement(`span`);i.className=`cl-num`,i.textContent=String(n+1);let a=document.createElement(`span`);a.className=`cl-txt`,a.innerHTML=_(t)||` `,r.appendChild(i),r.appendChild(a),e.appendChild(r)})}function y(e){return`'${e.replace(/\\(?![nrt0\\'])/g,`\\\\`).replace(/'/g,`\\'`)}'`}function b(e){return e.replace(/\\n/g,`
7
+ `).replace(/\\r/g,`\r`).replace(/\\t/g,` `).replace(/\\0/g,`\0`)}function x(e){return!e||e.length===0?`[]`:`[${e.map(e=>{let t=[];return e.usbVendorId!==void 0&&t.push(`usbVendorId: 0x${e.usbVendorId.toString(16).padStart(4,`0`)}`),e.usbProductId!==void 0&&t.push(`usbProductId: 0x${e.usbProductId.toString(16).padStart(4,`0`)}`),`{ ${t.join(`, `)} }`}).join(`, `)}]`}function S(e){return e.trim().split(/\s+/).filter(Boolean).map(e=>parseInt(e,16)).filter(e=>!isNaN(e))}function C(e){let t=S(e);return t.length===0?`new Uint8Array([])`:`new Uint8Array([${t.map(e=>`0x${e.toString(16).padStart(2,`0`)}`).join(`, `)}])`}function w(e){return e.replace(/[^a-zA-Z0-9 _-]/g,``).split(/[\s_-]+/).filter(Boolean).map((e,t)=>t===0?e.charAt(0).toLowerCase()+e.slice(1).toLowerCase():e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join(``)||`cmd`}function T(e,t){if(e.length===0)return``;let n=t.charAt(0).toLowerCase()+t.slice(1);return`
8
+ declare module 'webserial-core' {
9
+ interface SerialEventMap<T> {
10
+ `+e.map(e=>` '${n}:${w(e.name||`listener`)}': (data: string) => void;`).join(`
11
+ `)+`
12
+ }
13
+ }
14
+ `}function E(e,t){if(e.length===0)return``;let n=t?`: Promise<void>`:``,r=t?`public `:``;return`
15
+ `+e.map(e=>{let t=w(e.name),i=`send`+t.charAt(0).toUpperCase()+t.slice(1);return e.mode===`hex`?` ${r}${i}()${n} { return this.send(${C(e.value)}); } // ${e.name}`:` ${r}${i}()${n} { return this.send('${e.value.replace(/'/g,`\\'`)}'); } // ${e.name}`}).join(`
16
+ `)+`
17
+ `}function D(e,t,n){if(e.length===0)return``;let r=t.charAt(0).toLowerCase()+t.slice(1),i=n?`(data: string)`:`(data)`,a=n?`: void`:``,o=e.map(e=>{let t=e.name||`listener`,n=`${r}:${w(t)}`,i;switch(e.match){case`contains`:i=`String(data).includes('${e.pattern.replace(/'/g,`\\'`)}')`;break;case`startsWith`:i=`String(data).startsWith('${e.pattern.replace(/'/g,`\\'`)}')`;break;case`hex`:i=`(() => { const enc = new TextEncoder().encode(String(data)); const ex = ${`[${S(e.pattern).map(e=>`0x${e.toString(16).padStart(2,`0`)}`).join(`, `)}]`}; return enc.length === ex.length && enc.every((b, i) => b === ex[i]); })()`;break;default:i=`String(data).trim() === '${e.pattern.replace(/'/g,`\\'`)}' `}return` // ${t}\n if (${i}) {\n this.emit('${n}', data);\n }`}).join(`
18
+ `);return`\n ${n?`private `:``}startListening()${a} {\n this.on('serial:data', ${i} => {\n${o}\n });\n }\n`}function O(e,t,n,r,i){if(!e)return` // No handshake configured — accept any device.
19
+ return true;`;let a;if(a=t===`hex`?` await this.send(${C(e)});`:` await this.send('${e.replace(/'/g,`\\'`)}');`,!n)return`${a}\n return true;`;let o=i?`(data: string)`:`(data)`,s;return s=r===`hex`?`(() => { const enc = new TextEncoder().encode(String(data)); const ex = ${`[${S(n).map(e=>`0x${e.toString(16).padStart(2,`0`)}`).join(`, `)}]`}; return enc.length === ex.length && enc.every((b, i) => b === ex[i]); })()`:`String(data).trim() === '${n.replace(/'/g,`\\'`)}'`,`${a}\n return new Promise((resolve) => {\n const _h = ${o} => {\n this.off('serial:data', _h);\n resolve(${s});\n };\n this.on('serial:data', _h);\n });`}function k(e,t,n,r=[],i=[]){let a=n?`ts`:`js`,o=e.delimiter?y(e.delimiter):null,s=o?`delimiter`:`raw`,c=o?`delimiter(${o})`:`raw()`,l=n?`: Promise<boolean>`:``,u=n?`: SerialPortFilter[]`:``,d=n?`
20
+ import type { SerialPortFilter } from 'webserial-core';`:``,f=x(e.filters);return`// device.${a} — Generated by webserial-core demo
21
+ import { AbstractSerialDevice, ${s} } from 'webserial-core';${d}
22
+ ${n?T(i,t):``}
23
+ export class ${t} extends AbstractSerialDevice${n?`<string>`:``} {
24
+ constructor(filters${n?`${u}`:` = []`}) {
25
+ super({
26
+ baudRate: ${e.baudRate},
27
+ dataBits: ${e.dataBits},
28
+ stopBits: ${e.stopBits},
29
+ parity: '${e.parity}',
30
+ flowControl: '${e.flowControl}',
31
+ bufferSize: ${e.bufferSize},
32
+ commandTimeout: ${e.commandTimeout},
33
+ parser: ${c},
34
+ autoReconnect: ${e.autoReconnect},
35
+ autoReconnectInterval: ${e.autoReconnectInterval},
36
+ handshakeTimeout: ${e.handshakeTimeout},
37
+ filters,
38
+ });${i.length>0?`
39
+ this.startListening();`:``}
40
+ }
41
+
42
+ ${n?`protected `:``}async handshake()${l} {
43
+ ${O(e.hsCmd,e.hsCmdMode,e.hsExpect,e.hsExpectMode,n)}
44
+ }
45
+ ${E(r,n)}${D(i,t,n)}}
46
+
47
+ // ── Usage ────────────────────────────────────────────────────────
48
+ const device = new ${t}(${f});
49
+
50
+ device.on('serial:connected', () => console.log('Connected!'));
51
+ device.on('serial:disconnected', () => console.log('Disconnected.'));
52
+ device.on('serial:data', (data) => console.log('← ', data));
53
+ device.on('serial:error', (err) => console.error(err.message));
54
+
55
+ // Must be called from a user-gesture (click handler):
56
+ // await device.connect();
57
+
58
+ // Send a message (prepend/append applied in your UI layer):
59
+ // await device.send('${e.prepend}COMMAND${e.append}');
60
+
61
+ // Disconnect:
62
+ // await device.disconnect();
63
+ `}function A(e,t,n,r=[],i=[]){let a=n?`ts`:`js`,o=e.delimiter?y(e.delimiter):null,s=o?`delimiter`:`raw`,c=o?`delimiter(${o})`:`raw()`,l=n?`: Promise<boolean>`:``;return`// device.${a} — Generated by webserial-core demo
64
+ import { AbstractSerialDevice, ${s}, createBluetoothProvider } from 'webserial-core';
65
+
66
+ // Inject the BLE provider before creating any device instance.
67
+ AbstractSerialDevice.setProvider(createBluetoothProvider());
68
+ ${n?T(i,t):``}
69
+ export class ${t} extends AbstractSerialDevice${n?`<string>`:``} {
70
+ constructor() {
71
+ super({
72
+ baudRate: 9600, // Nominal — not used over BLE GATT
73
+ bufferSize: ${e.bufferSize},
74
+ commandTimeout: ${e.commandTimeout},
75
+ parser: ${c},
76
+ autoReconnect: false, // BLE requires user gesture to reconnect
77
+ handshakeTimeout: ${e.handshakeTimeout},
78
+ });${i.length>0?`
79
+ this.startListening();`:``}
80
+ }
81
+
82
+ ${n?`protected `:``}async handshake()${l} {
83
+ ${O(e.hsCmd,e.hsCmdMode,e.hsExpect,e.hsExpectMode,n)}
84
+ }
85
+ ${E(r,n)}${D(i,t,n)}}
86
+
87
+ // ── Usage ────────────────────────────────────────────────────────
88
+ const device = new ${t}();
89
+
90
+ device.on('serial:connected', () => console.log('BLE connected!'));
91
+ device.on('serial:disconnected', () => console.log('Disconnected.'));
92
+ device.on('serial:data', (data) => console.log('← ', data));
93
+ device.on('serial:error', (err) => console.error(err.message));
94
+
95
+ // Must be called from a user-gesture:
96
+ // await device.connect();
97
+ // await device.send('${e.prepend}COMMAND${e.append}');
98
+ // await device.disconnect();
99
+ `}function j(e,t,n,r=[],i=[]){let a=n?`ts`:`js`,o=e.delimiter?y(e.delimiter):null,s=o?`delimiter`:`raw`,c=o?`delimiter(${o})`:`raw()`,l=n?`: Promise<boolean>`:``,u=n?`: SerialPortFilter[]`:``,d=n?`
100
+ import type { SerialPortFilter } from 'webserial-core';`:``,f=x(e.filters);return`// device.${a} — Generated by webserial-core demo
101
+ import { AbstractSerialDevice, ${s}, WebUsbProvider } from 'webserial-core';${d}
102
+
103
+ // Inject the WebUSB polyfill provider.
104
+ AbstractSerialDevice.setProvider(
105
+ new WebUsbProvider({
106
+ usbControlInterfaceClass: ${e.usbControlInterfaceClass},
107
+ usbTransferInterfaceClass: ${e.usbTransferInterfaceClass},
108
+ })
109
+ );
110
+ ${n?T(i,t):``}
111
+ export class ${t} extends AbstractSerialDevice${n?`<string>`:``} {
112
+ constructor(filters${n?`${u}`:` = []`}) {
113
+ super({
114
+ baudRate: ${e.baudRate},
115
+ dataBits: ${e.dataBits},
116
+ stopBits: ${e.stopBits},
117
+ parity: '${e.parity}',
118
+ flowControl: '${e.flowControl}',
119
+ bufferSize: ${e.bufferSize},
120
+ commandTimeout: ${e.commandTimeout},
121
+ parser: ${c},
122
+ autoReconnect: ${e.autoReconnect},
123
+ autoReconnectInterval: ${e.autoReconnectInterval},
124
+ handshakeTimeout: ${e.handshakeTimeout},
125
+ filters,
126
+ });${i.length>0?`
127
+ this.startListening();`:``}
128
+ }
129
+
130
+ ${n?`protected `:``}async handshake()${l} {
131
+ ${O(e.hsCmd,e.hsCmdMode,e.hsExpect,e.hsExpectMode,n)}
132
+ }
133
+ ${E(r,n)}${D(i,t,n)}}
134
+
135
+ // ── Usage ────────────────────────────────────────────────────────
136
+ // CP2102/ESP32: { usbVendorId: 0x10c4, usbProductId: 0xea60 }
137
+ // CH340/Arduino: { usbVendorId: 0x1a86, usbProductId: 0x7523 }
138
+ const device = new ${t}(${f});
139
+
140
+ device.on('serial:connected', () => console.log('USB connected!'));
141
+ device.on('serial:disconnected', () => console.log('Disconnected.'));
142
+ device.on('serial:data', (data) => console.log('← ', data));
143
+ device.on('serial:error', (err) => console.error(err.message));
144
+
145
+ // await device.connect();
146
+ // await device.send('${e.prepend}COMMAND${e.append}');
147
+ // await device.disconnect();
148
+ `}function M(e,t,n,r=[],i=[]){let a=n?`ts`:`js`,o=e.delimiter?y(e.delimiter):null,s=o?`delimiter`:`raw`,c=o?`delimiter(${o})`:`raw()`,l=n?`: Promise<boolean>`:``;return`// device.${a} — Generated by webserial-core demo
149
+ import { AbstractSerialDevice, ${s}, createWebSocketProvider } from 'webserial-core';
150
+
151
+ // Inject the WebSocket bridge provider.
152
+ // Start the Node.js bridge first: cd demos/websocket && node server.js
153
+ const wsProvider = createWebSocketProvider('${e.wsUrl}');
154
+ AbstractSerialDevice.setProvider(wsProvider);
155
+ ${n?T(i,t):``}
156
+ export class ${t} extends AbstractSerialDevice${n?`<string>`:``} {
157
+ constructor() {
158
+ super({
159
+ baudRate: ${e.baudRate},
160
+ dataBits: ${e.dataBits},
161
+ stopBits: ${e.stopBits},
162
+ parity: '${e.parity}',
163
+ flowControl: '${e.flowControl}',
164
+ bufferSize: ${e.bufferSize},
165
+ commandTimeout: ${e.commandTimeout},
166
+ parser: ${c},
167
+ autoReconnect: ${e.autoReconnect},
168
+ autoReconnectInterval: ${e.autoReconnectInterval},
169
+ handshakeTimeout: ${e.handshakeTimeout},
170
+ });${i.length>0?`
171
+ this.startListening();`:``}
172
+ }
173
+
174
+ ${n?`protected `:``}async handshake()${l} {
175
+ ${O(e.hsCmd,e.hsCmdMode,e.hsExpect,e.hsExpectMode,n)}
176
+ }
177
+ ${E(r,n)}${D(i,t,n)}}
178
+
179
+ // ── Usage ────────────────────────────────────────────────────────
180
+ const device = new ${t}();
181
+
182
+ device.on('serial:connected', () => console.log('WS connected!'));
183
+ device.on('serial:disconnected', () => console.log('Disconnected.'));
184
+ device.on('serial:data', (data) => console.log('← ', data));
185
+ device.on('serial:error', (err) => console.error(err.message));
186
+
187
+ // await device.connect();
188
+ // await device.send('${e.prepend}COMMAND${e.append}');
189
+ // await device.disconnect();
190
+ `}function N(e,t,n){let r=n===`ws`?{ws:`^8.0.0`}:{},i={vite:`^8.0.0`,...t?{typescript:`~5.9.3`}:{}};return JSON.stringify({name:e.toLowerCase().replace(/\s+/g,`-`).replace(/[^a-z0-9-]/g,``),version:`1.0.0`,type:`module`,scripts:{dev:`vite`,build:`vite build`,preview:`vite preview`},dependencies:{"webserial-core":`^2.0.0`,...r},devDependencies:i},null,2)}function P(){return JSON.stringify({compilerOptions:{target:`ES2022`,useDefineForClassFields:!0,lib:[`ES2022`,`DOM`,`DOM.Iterable`],module:`ESNext`,skipLibCheck:!0,moduleResolution:`bundler`,allowImportingTsExtensions:!0,strict:!0,noEmit:!0},include:[`**/*.ts`]},null,2)}function F(e,t,n){return`<!doctype html>
191
+ <html lang="en">
192
+ <head>
193
+ <meta charset="UTF-8" />
194
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
195
+ <title>${e} — ${n}</title>
196
+ <style>
197
+ body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; padding: 0 16px; }
198
+ pre { background: #0f0f0f; color: #4ade80; padding: 12px; border-radius: 8px; font-size: 0.82rem; min-height: 120px; overflow-y: auto; }
199
+ .controls { display: flex; gap: 8px; margin-bottom: 12px; }
200
+ button { padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer; }
201
+ #btn-connect { background: #22c55e; color: #fff; }
202
+ #btn-disconnect { background: #ef4444; color: #fff; }
203
+ #btn-send { background: #6366f1; color: #fff; }
204
+ input { flex: 1; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; }
205
+ </style>
206
+ </head>
207
+ <body>
208
+ <h2>${e}</h2>
209
+ <div class="controls">
210
+ <button id="btn-connect">Connect</button>
211
+ <button id="btn-disconnect" disabled>Disconnect</button>
212
+ </div>
213
+ <div class="controls">
214
+ <input id="input-send" type="text" placeholder="Command, e.g. LED_ON" disabled />
215
+ <button id="btn-send" disabled>Send</button>
216
+ </div>
217
+ <pre id="log">Waiting for connection...</pre>
218
+
219
+ <script type="module">
220
+ import { ${e} } from './device.${t}';
221
+ const device = new ${e}();
222
+ const log = (msg) => {
223
+ document.getElementById('log').textContent += msg + '\\n';
224
+ };
225
+ device.on('serial:connected', () => { log('✓ Connected'); document.getElementById('btn-disconnect').disabled = false; document.getElementById('btn-send').disabled = false; document.getElementById('input-send').disabled = false; });
226
+ device.on('serial:disconnected', () => { log('✗ Disconnected'); document.getElementById('btn-disconnect').disabled = true; document.getElementById('btn-send').disabled = true; document.getElementById('input-send').disabled = true; });
227
+ device.on('serial:data', (data) => log('← ' + data));
228
+ device.on('serial:error', (err) => log('⚠ ' + err.message));
229
+ document.getElementById('btn-connect').onclick = () => device.connect();
230
+ document.getElementById('btn-disconnect').onclick = () => device.disconnect();
231
+ document.getElementById('btn-send').onclick = () => {
232
+ const v = document.getElementById('input-send').value.trim();
233
+ if (v) { device.send(v + '\\n'); document.getElementById('input-send').value = ''; }
234
+ };
235
+ document.getElementById('input-send').onkeydown = (e) => { if (e.key === 'Enter') document.getElementById('btn-send').click(); };
236
+ <\/script>
237
+ </body>
238
+ </html>
239
+ `}function I(e,t,n,r){return`# ${e}
240
+
241
+ A ${n} device using [webserial-core](https://github.com/danidoble/webserial-core).
242
+
243
+ ${r}
244
+
245
+ ## Setup
246
+
247
+ \`\`\`bash
248
+ npm install
249
+ npm run dev
250
+ \`\`\`
251
+
252
+ ## Usage
253
+
254
+ \`\`\`typescript
255
+ import { ${e} } from './device.${t}';
256
+
257
+ const device = new ${e}();
258
+
259
+ device.on('serial:data', (data) => {
260
+ console.log('Received:', data);
261
+ });
262
+
263
+ // Must be called from a user-gesture (button click):
264
+ await device.connect();
265
+
266
+ // Send data:
267
+ await device.send('LED_ON\\n');
268
+
269
+ // Disconnect:
270
+ await device.disconnect();
271
+ \`\`\`
272
+ `}function L(e,t){let n=new Blob([t],{type:`text/plain;charset=utf-8`}),r=URL.createObjectURL(n),i=document.createElement(`a`);i.href=r,i.download=e,document.body.appendChild(i),i.click(),document.body.removeChild(i),URL.revokeObjectURL(r)}function R(e,t){let n=JSON.stringify(e,null,2),r=new Blob([n],{type:`application/json;charset=utf-8`}),i=URL.createObjectURL(r),a=document.createElement(`a`);a.href=i,a.download=t.endsWith(`.json`)?t:t+`.json`,document.body.appendChild(a),a.click(),document.body.removeChild(a),URL.revokeObjectURL(i)}function z(e,t){let n=new TextEncoder,r=G(e.map(e=>({name:e.name,data:n.encode(e.content)}))),i=new Blob([r.buffer],{type:`application/zip`}),a=URL.createObjectURL(i),o=document.createElement(`a`);o.href=a,o.download=t.endsWith(`.zip`)?t:t+`.zip`,document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(a)}var B=null;function V(){if(B)return B;B=new Uint32Array(256);for(let e=0;e<256;e++){let t=e;for(let e=0;e<8;e++)t=t&1?3988292384^t>>>1:t>>>1;B[e]=t>>>0}return B}function H(e){let t=V(),n=4294967295;for(let r=0;r<e.length;r++)n=n>>>8^t[(n^e[r])&255];return(n^4294967295)>>>0}function U(e,t,n){new DataView(e.buffer,e.byteOffset+t).setUint16(0,n,!0)}function W(e,t,n){new DataView(e.buffer,e.byteOffset+t).setUint32(0,n,!0)}function G(e){let t=new TextEncoder,n=[],r=0;for(let i of e){let e=t.encode(i.name),a=H(i.data),o=new Uint8Array(30+e.length);W(o,0,67324752),U(o,4,20),U(o,6,0),U(o,8,0),U(o,10,0),U(o,12,0),W(o,14,a),W(o,18,i.data.length),W(o,22,i.data.length),U(o,26,e.length),U(o,28,0),o.set(e,30);let s=new Uint8Array(46+e.length);W(s,0,33639248),U(s,4,20),U(s,6,20),U(s,8,0),U(s,10,0),U(s,12,0),U(s,14,0),W(s,16,a),W(s,20,i.data.length),W(s,24,i.data.length),U(s,28,e.length),U(s,30,0),U(s,32,0),U(s,34,0),U(s,36,0),W(s,38,0),W(s,42,r),s.set(e,46),n.push({localHdr:o,data:i.data,cdHdr:s,offset:r}),r+=o.length+i.data.length}let i=n.reduce((e,t)=>e+t.cdHdr.length,0),a=new Uint8Array(22);W(a,0,101010256),U(a,4,0),U(a,6,0),U(a,8,n.length),U(a,10,n.length),W(a,12,i),W(a,16,r),U(a,20,0);let o=new Uint8Array(r+i+a.length),s=0;for(let e of n)o.set(e.localHdr,s),s+=e.localHdr.length,o.set(e.data,s),s+=e.data.length;for(let e of n)o.set(e.cdHdr,s),s+=e.cdHdr.length;return o.set(a,s),o}function K(){let e=document.createElement(`canvas`);e.style.cssText=`position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:0;opacity:0.55;`,document.body.insertBefore(e,document.body.firstChild);let t=e.getContext(`2d`),n=window.devicePixelRatio||1,r=[],i=[],a=[],o=0,s=0;function c(){o=Math.max(window.innerWidth,1),s=Math.max(window.innerHeight,1),e.width=Math.round(o*n),e.height=Math.round(s*n),e.style.width=`${o}px`,e.style.height=`${s}px`,t.setTransform(n,0,0,n,0,0);let c=Math.ceil(o/52),l=Math.ceil(s/52);r=[];let u=new Map,d=(e,t)=>{u.set(`${e},${t}`,(u.get(`${e},${t}`)??0)+1)};for(let e=0;e<=l;e++)for(let t=0;t<=c;t++){let n=t*52,i=e*52;t<c&&Math.random()>.22&&(r.push({x1:n,y1:i,x2:n+52,y2:i}),d(n,i),d(n+52,i)),e<l&&Math.random()>.22&&(r.push({x1:n,y1:i,x2:n,y2:i+52}),d(n,i),d(n,i+52))}i=Array.from(u.keys()).map(e=>{let[t,n]=e.split(`,`).map(Number);return[t,n]});let f=Math.max(10,Math.min(35,Math.floor(o*s/11e3)));a=Array.from({length:f},()=>({seg:Math.floor(Math.random()*Math.max(1,r.length)),t:Math.random(),speed:.004+Math.random()*.009,sz:1.5+Math.random()*2}))}function l(){let e=getComputedStyle(document.documentElement).getPropertyValue(`--accent`).trim();if(e.startsWith(`#`)){let t=e.length===4?e.slice(1).split(``).map(e=>e+e).join(``):e.slice(1);return[0,2,4].map(e=>parseInt(t.slice(e,e+2),16)).join(`,`)}return`34,197,94`}let u=0;function d(){t.clearRect(0,0,o,s);let e=l();t.strokeStyle=`rgba(${e},0.07)`,t.lineWidth=.8;for(let e of r)t.beginPath(),t.moveTo(e.x1,e.y1),t.lineTo(e.x2,e.y2),t.stroke();for(let[n,r]of i)t.beginPath(),t.arc(n,r,3.8,0,Math.PI*2),t.fillStyle=`rgba(${e},0.05)`,t.fill(),t.strokeStyle=`rgba(${e},0.2)`,t.lineWidth=.7,t.stroke(),t.beginPath(),t.arc(n,r,1.4,0,Math.PI*2),t.fillStyle=`rgba(${e},0.25)`,t.fill();for(let n of a){let i=r[n.seg];if(!i)continue;let a=i.x1+(i.x2-i.x1)*n.t,o=i.y1+(i.y2-i.y1)*n.t,s=Math.max(0,n.t-.24),c=i.x1+(i.x2-i.x1)*s,l=i.y1+(i.y2-i.y1)*s,u=t.createLinearGradient(c,l,a,o);u.addColorStop(0,`rgba(${e},0)`),u.addColorStop(1,`rgba(${e},0.42)`),t.strokeStyle=u,t.lineWidth=2,t.beginPath(),t.moveTo(c,l),t.lineTo(a,o),t.stroke();let d=t.createRadialGradient(a,o,0,a,o,n.sz*7);d.addColorStop(0,`rgba(${e},0.5)`),d.addColorStop(.4,`rgba(${e},0.15)`),d.addColorStop(1,`rgba(${e},0)`),t.fillStyle=d,t.beginPath(),t.arc(a,o,n.sz*7,0,Math.PI*2),t.fill(),t.fillStyle=`rgba(${e},1)`,t.beginPath(),t.arc(a,o,n.sz,0,Math.PI*2),t.fill(),t.fillStyle=`rgba(255,255,255,0.75)`,t.beginPath(),t.arc(a,o,n.sz*.4,0,Math.PI*2),t.fill(),n.t+=n.speed,n.t>=1&&(n.t=0,n.seg=Math.floor(Math.random()*r.length))}u=requestAnimationFrame(d)}c(),d(),new ResizeObserver(()=>{cancelAnimationFrame(u),c(),d()}).observe(document.body)}function q(){let e=document.querySelector(`.topbar`);if(!e)return;let t=document.createElement(`button`);t.className=`icon-btn mob-toggle`,t.title=`Navigation menu`,t.innerHTML=`<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2.2" fill="none" stroke-linecap="round"><circle cx="12" cy="5" r="1.3"/><circle cx="12" cy="12" r="1.3"/><circle cx="12" cy="19" r="1.3"/></svg>`,e.appendChild(t);let n=document.createElement(`div`);n.className=`mob-nav-drawer`;let r=document.createElement(`div`);r.className=`mob-nav-inner`;let i=e.querySelector(`.topbar-nav`);if(i){let e=document.createElement(`div`);e.className=`mob-nav-links`,i.querySelectorAll(`.nav-item`).forEach(t=>{e.appendChild(t.cloneNode(!0))}),r.appendChild(e)}let a=document.createElement(`div`);a.className=`mob-nav-sep`,r.appendChild(a);let o=document.createElement(`div`);o.className=`mob-nav-actions`;let s=document.getElementById(`btn-connect`),c=document.getElementById(`btn-disconnect`),l=(e,t,r)=>{let i=document.createElement(`button`);return i.className=`btn ${t}`,i.textContent=r,e&&(i.disabled=e.disabled,new MutationObserver(()=>{i.disabled=e.disabled}).observe(e,{attributes:!0,attributeFilter:[`disabled`]}),i.addEventListener(`click`,()=>{e.click(),n.classList.remove(`open`)})),i};o.appendChild(l(s,`btn-connect`,`Connect`)),o.appendChild(l(c,`btn-disconnect`,`Disconnect`)),r.appendChild(o),n.appendChild(r),document.body.appendChild(n),t.addEventListener(`click`,e=>{e.stopPropagation(),n.classList.toggle(`open`)}),document.addEventListener(`click`,()=>n.classList.remove(`open`)),n.addEventListener(`click`,e=>e.stopPropagation())}export{o as S,P as _,L as a,c as b,k as c,K as d,q as f,I as g,N as h,R as i,j as l,F as m,m as n,z as o,d as p,b as r,A as s,p as t,M as u,v,s as x,f as y};
@@ -0,0 +1 @@
1
+ *,:before,:after{box-sizing:border-box;margin:0;padding:0}html,body{height:100%;overflow:hidden}:root{--bg:#f4f4f5;--surface:#fff;--surface-2:#f4f4f5;--surface-3:#e4e4e7;--border:#e4e4e7;--border-hi:#d1d5db;--text:#18181b;--text-muted:#71717a;--text-faint:#a1a1aa;--accent:#22c55e;--accent-dark:#16a34a;--accent-glow:#22c55e1f;--accent-badge:#fff;--sent-bg:var(--accent);--sent-text:#fff;--recv-bg:#e4e4e7;--recv-text:#18181b;--sys-bg:#fef9c3;--sys-text:#713f12;--sys-border:#fbbf24;--err-bg:#fee2e2;--err-text:#991b1b;--err-border:#f87171;--topbar-h:56px;--sidebar-w:320px;--code-w:370px;--code-bg:#1e1e1e;--code-text:#d4d4d4;--code-gutter:#858585;--code-hover:#ffffff0a;--code-border:#3f3f46;--scrollbar:#d1d5db}[data-theme=dark]{--bg:#0f0f12;--surface:#18181b;--surface-2:#1c1c1f;--surface-3:#27272a;--border:#27272a;--border-hi:#3f3f46;--text:#e4e4e7;--text-muted:#71717a;--text-faint:#52525b;--recv-bg:#27272a;--recv-text:#e4e4e7;--sys-bg:#1c1a07;--sys-text:#fcd34d;--sys-border:#854d0e;--err-bg:#1f0707;--err-text:#f87171;--err-border:#7f1d1d;--scrollbar:#3f3f46}body{background:var(--bg);color:var(--text);flex-direction:column;font-family:Segoe UI,system-ui,-apple-system,sans-serif;transition:background .2s,color .2s;display:flex}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--scrollbar);border-radius:3px}.topbar{height:var(--topbar-h);background:var(--surface);border-bottom:1px solid var(--border);z-index:100;flex-shrink:0;align-items:center;gap:10px;padding:0 14px;display:flex}.topbar-brand{flex-shrink:0;align-items:center;gap:7px;text-decoration:none;display:flex}.brand-icon{font-size:1.1rem}.brand-name{color:var(--text);white-space:nowrap;font-size:.88rem;font-weight:700}.brand-ver{color:var(--text-faint);border:1px solid var(--border);border-radius:4px;padding:1px 6px;font-size:.6rem}.provider-pill{background:var(--accent);color:var(--accent-badge);text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;border-radius:999px;padding:2px 8px;font-size:.6rem;font-weight:700}.topbar-sep{background:var(--border);flex-shrink:0;width:1px;height:22px}.flex-1{flex:1}.topbar-nav{scrollbar-width:none;flex-shrink:0;gap:2px;display:flex;overflow-x:auto}.topbar-nav::-webkit-scrollbar{display:none}.nav-item{color:var(--text-muted);white-space:nowrap;border-radius:7px;align-items:center;gap:5px;padding:5px 10px;font-size:.77rem;font-weight:500;text-decoration:none;transition:background .15s,color .15s;display:flex}.nav-item:hover{background:var(--surface-2);color:var(--text)}.nav-item.active{background:var(--accent-glow);color:var(--accent)}.topbar-actions{flex-shrink:0;align-items:center;gap:6px;display:flex}.status-pill{border:1px solid var(--border);color:var(--text-muted);white-space:nowrap;border-radius:999px;align-items:center;gap:5px;padding:4px 9px;font-size:.7rem;display:flex}.status-dot{background:var(--text-faint);border-radius:50%;flex-shrink:0;width:7px;height:7px;transition:background .2s,box-shadow .2s}.status-dot.connected{background:var(--accent);box-shadow:0 0 5px var(--accent)}.status-dot.connecting{background:#f59e0b;animation:1s infinite blink}.status-dot.error{background:#ef4444}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.btn{cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none;border:1px solid #0000;border-radius:8px;justify-content:center;align-items:center;gap:5px;padding:6px 13px;font-size:.77rem;font-weight:500;line-height:1;transition:background .15s,opacity .15s,transform .1s,border-color .15s;display:inline-flex}.btn:active:not(:disabled){transform:scale(.97)}.btn:disabled{opacity:.4;cursor:not-allowed}.btn-connect{background:var(--accent);color:#fff}.btn-connect:hover:not(:disabled){background:var(--accent-dark)}.btn-disconnect{color:#fff;background:#ef4444}.btn-disconnect:hover:not(:disabled){background:#dc2626}.btn-send{color:#fff;background:#6366f1}.btn-send:hover:not(:disabled){background:#4f46e5}.btn-ghost{border-color:var(--border);color:var(--text-muted);background:0 0}.btn-ghost:hover:not(:disabled){background:var(--surface-2);color:var(--text)}.btn-mode{background:var(--accent);color:var(--accent-badge);border-color:#0000;min-width:46px;font-weight:700}.btn-mode:hover{background:var(--accent-dark)}.btn-dl{background:var(--surface);border-color:var(--border);color:var(--text)}.btn-dl:hover:not(:disabled){background:var(--surface-2)}.icon-btn{border:1px solid var(--border);cursor:pointer;width:32px;height:32px;color:var(--text-muted);background:0 0;border-radius:7px;flex-shrink:0;justify-content:center;align-items:center;font-size:.88rem;transition:background .15s,color .15s;display:flex}.icon-btn:hover{background:var(--surface-2);color:var(--text)}.app-layout{flex:1;display:flex;overflow:hidden}.sidebar{width:var(--sidebar-w);background:var(--surface);border-right:1px solid var(--border);flex-direction:column;flex-shrink:0;transition:width .22s;display:flex;overflow:hidden}.sidebar.collapsed{width:0}.sidebar-scroll{flex:1;overflow-y:auto}.sb-section{padding:12px 13px 9px}.sb-section+.sb-section{border-top:1px solid var(--border)}.sb-title{text-transform:uppercase;letter-spacing:.08em;color:var(--text-faint);margin-bottom:9px;font-size:.59rem;font-weight:700}.field{flex-direction:column;gap:3px;margin-bottom:8px;display:flex}.field:last-child{margin-bottom:0}.field-row{gap:7px;display:flex}.field-row .field{flex:1;min-width:0}.field label{color:var(--text-muted);font-size:.68rem;font-weight:500}.field select,.field input[type=text],.field input[type=number],.field input[type=url]{border:1px solid var(--border-hi);background:var(--surface-2);width:100%;color:var(--text);appearance:textfield;border-radius:6px;outline:none;padding:5px 8px;font-size:.75rem;transition:border-color .15s}.field input::-webkit-inner-spin-button{-webkit-appearance:none}.field select:focus,.field input:focus{border-color:var(--accent)}.field-toggle{justify-content:space-between;align-items:center;margin-bottom:8px;padding:1px 0;display:flex}.field-toggle label{color:var(--text-muted);cursor:pointer;font-size:.74rem}.sw{width:34px;height:18px;display:inline-block;position:relative}.sw input{opacity:0;width:0;height:0}.sw-knob{background:var(--surface-3);cursor:pointer;border-radius:999px;transition:background .2s;position:absolute;inset:0}.sw-knob:before{content:"";background:#fff;border-radius:50%;width:12px;height:12px;transition:transform .2s;position:absolute;bottom:3px;left:3px;box-shadow:0 1px 3px #00000040}.sw input:checked+.sw-knob{background:var(--accent)}.sw input:checked+.sw-knob:before{transform:translate(16px)}.dl-lang-row,.dl-type-row{flex-wrap:wrap;gap:5px;margin-bottom:7px;display:flex}.dl-opt{border:1px solid var(--border);background:var(--surface-2);color:var(--text-muted);cursor:pointer;border-radius:6px;align-items:center;gap:4px;padding:3px 9px;font-size:.71rem;transition:all .15s;display:inline-flex}.dl-opt:has(input:checked){border-color:var(--accent);color:var(--accent);background:var(--accent-glow)}.dl-opt:hover{background:var(--surface-3)}.dl-opt input{accent-color:var(--accent)}.dl-btns{flex-wrap:wrap;gap:5px;margin-top:7px;display:flex}.dl-btns .btn{flex:1;font-size:.72rem}.console-area{flex-direction:column;flex:1;min-width:0;display:flex;overflow:hidden}.console-hdr{background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0;align-items:center;gap:8px;padding:7px 13px;display:flex}.console-hdr .status-pill{font-size:.7rem}.messages{flex-direction:column;flex:1;gap:3px;padding:12px 13px;display:flex;position:relative;overflow-y:auto}.msg{flex-direction:column;max-width:78%;animation:.15s both msgIn;display:flex}@keyframes msgIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.msg.sent{align-self:flex-end;align-items:flex-end}.msg.received{align-self:flex-start;align-items:flex-start}.msg.system,.msg.error{align-self:center;max-width:88%}.msg-label{text-transform:uppercase;letter-spacing:.04em;color:var(--text-faint);margin-bottom:2px;padding:0 4px;font-size:.59rem;font-weight:700}.msg-bubble{word-break:break-word;white-space:pre-wrap;border-radius:14px;padding:7px 12px;font-family:Cascadia Code,Fira Code,monospace;font-size:.79rem;line-height:1.5}.msg.sent .msg-bubble{background:var(--sent-bg);color:var(--sent-text);border-bottom-right-radius:3px}.msg.received .msg-bubble{background:var(--recv-bg);color:var(--recv-text);border-bottom-left-radius:3px}.msg.system .msg-bubble{background:var(--sys-bg);color:var(--sys-text);border-left:3px solid var(--sys-border);border-radius:8px;font-family:inherit;font-size:.73rem}.msg.error .msg-bubble{background:var(--err-bg);color:var(--err-text);border-left:3px solid var(--err-border);border-radius:8px;font-family:inherit;font-size:.73rem}.msg-time{color:var(--text-faint);margin-top:2px;padding:0 4px;font-size:.59rem}.empty-state{color:var(--text-faint);-webkit-user-select:none;user-select:none;pointer-events:none;flex-direction:column;flex:1;justify-content:center;align-items:center;gap:8px;font-size:.78rem;display:flex}.empty-icon{opacity:.3;font-size:2.4rem}.input-bar{background:var(--surface);border-top:1px solid var(--border);flex-shrink:0;align-items:center;gap:7px;padding:10px 12px;display:flex}.msg-input{border:1px solid var(--border-hi);background:var(--surface-2);color:var(--text);border-radius:22px;outline:none;flex:1;padding:8px 14px;font-size:.82rem;transition:border-color .15s}.msg-input:focus{border-color:var(--accent)}.msg-input:disabled{opacity:.4}.code-panel{width:var(--code-w);background:var(--code-bg);border-left:1px solid var(--code-border);flex-direction:column;flex-shrink:0;transition:width .22s;display:flex;overflow:hidden}.code-panel.collapsed{width:0}.cp-hdr{border-bottom:1px solid var(--code-border);background:#252526;flex-shrink:0;justify-content:space-between;align-items:center;gap:8px;padding:7px 11px;display:flex}.cp-title{color:#ccc;align-items:center;gap:6px;font-size:.68rem;font-weight:600;display:flex}.cp-tab{color:#bbb;cursor:default;background:#37373d;border:none;border-radius:4px;padding:2px 8px;font-size:.67rem}.cp-actions{gap:4px;display:flex}.cp-btn{color:#888;cursor:pointer;background:#37373d;border:none;border-radius:4px;padding:3px 8px;font-size:.67rem;transition:all .15s}.cp-btn:hover{color:#ccc;background:#4a4a4f}.cp-btn.copied{color:#4ade80;background:#1e4d2b}.code-view{background:var(--code-bg);flex:1;padding:6px 0;font-family:Cascadia Code,Fira Code,Consolas,monospace;font-size:.72rem;line-height:1.65;overflow:auto}.code-line{min-height:1.65em;display:flex}.code-line:hover{background:var(--code-hover)}.cl-num{text-align:right;width:42px;color:var(--code-gutter);-webkit-user-select:none;user-select:none;flex-shrink:0;padding-right:14px;font-size:.67rem;line-height:1.65}.cl-txt{color:var(--code-text);white-space:pre;flex:1;padding:0 14px}.t-kw{color:#569cd6}.t-str{color:#ce9178}.t-num{color:#b5cea8}.t-cmt{color:#6a9955;font-style:italic}.t-fn{color:#dcdcaa}.t-cls{color:#4ec9b0}.t-var{color:#9cdcfe}.t-typ{color:#4ec9b0}.notice{background:var(--surface-2);border:1px solid var(--border);border-left:3px solid var(--accent);color:var(--text-muted);border-radius:7px;margin-bottom:3px;padding:8px 11px;font-size:.71rem;line-height:1.6}.notice code{background:var(--surface-3);border-radius:3px;padding:1px 5px;font-family:monospace;font-size:.69rem}.mob-toggle{display:none!important}.mob-nav-drawer{top:var(--topbar-h);background:var(--surface);border-bottom:1px solid var(--border);z-index:85;max-height:0;transition:max-height .26s,box-shadow .26s;position:fixed;left:0;right:0;overflow:hidden;box-shadow:0 6px 28px #00000038}.mob-nav-drawer.open{max-height:340px;box-shadow:0 6px 28px #0000004d}.mob-nav-inner{flex-direction:column;gap:9px;padding:10px 12px 12px;display:flex}.mob-nav-links{flex-wrap:wrap;gap:6px;display:flex}.mob-nav-links .nav-item{border:1px solid var(--border);background:var(--surface-2);border-radius:8px;flex:1;justify-content:center;min-width:72px;padding:8px 10px;font-size:.78rem}.mob-nav-links .nav-lbl{display:inline!important}.mob-nav-sep{background:var(--border);height:1px}.mob-nav-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.mob-nav-actions .btn{flex:1;min-width:90px}@media (width<=1200px){:root{--code-w:320px}.mob-toggle{display:flex!important}.topbar-nav{display:none!important}.topbar-sep{display:none}}@media (width<=960px){:root{--sidebar-w:290px}.code-panel{right:0;top:var(--topbar-h);z-index:70;width:min(90vw,400px);transition:transform .28s;position:fixed;bottom:0;transform:translate(calc(100% + 1px));box-shadow:-4px 0 24px #0006}.code-panel:not(.collapsed){transform:translate(0)}.code-toggle{display:flex!important}.resize-handle{display:none!important}}@media (width<=720px){.brand-ver,.topbar-brand{display:none}}@media (width<=640px){.topbar-actions .btn-connect,.topbar-actions .btn-disconnect{display:none}.sidebar{top:var(--topbar-h);z-index:50;position:fixed;bottom:0;left:0;box-shadow:4px 0 24px #0000004d}.sidebar.collapsed{box-shadow:none}}.resize-handle,.sidebar-resize-handle{cursor:col-resize;z-index:10;background:0 0;flex-shrink:0;width:4px;transition:background .15s}.resize-handle:hover,.resize-handle.dragging,.sidebar-resize-handle:hover,.sidebar-resize-handle.dragging{background:var(--accent)}.chip-list{flex-direction:column;gap:5px;margin-top:5px;display:flex}.chip{background:var(--surface-2);border:1px solid var(--border);border-radius:8px;align-items:center;gap:5px;min-width:0;padding:5px 8px;font-size:.71rem;display:flex}.chip-badge{background:var(--accent-glow);color:var(--accent);text-transform:uppercase;letter-spacing:.03em;border-radius:4px;flex-shrink:0;padding:1px 5px;font-size:.57rem;font-weight:700}.chip-name{color:var(--text);text-overflow:ellipsis;white-space:nowrap;flex-shrink:0;max-width:72px;font-weight:600;overflow:hidden}.chip-val{color:var(--text-muted);text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-family:Cascadia Code,Fira Code,monospace;font-size:.68rem;overflow:hidden}.chip-send,.chip-del{cursor:pointer;background:var(--surface-3);width:20px;height:20px;color:var(--text-muted);border:none;border-radius:5px;flex-shrink:0;justify-content:center;align-items:center;padding:0;font-size:.68rem;line-height:1;transition:background .15s,color .15s;display:flex}.chip-send:hover{background:var(--accent-glow);color:var(--accent)}.chip-del:hover{color:#dc2626;background:#fee2e2}[data-theme=dark] .chip-del:hover{color:#f87171;background:#3f0707}[data-lucide]{stroke-width:2px;vertical-align:-2px;flex-shrink:0;width:14px;height:14px;display:inline-block}.icon-btn [data-lucide]{width:16px;height:16px}.app-footer{color:var(--text-faint);text-align:center;border-top:1px solid var(--border);background:var(--surface);letter-spacing:.02em;flex-shrink:0;padding:5px 16px;font-size:.63rem}
@@ -0,0 +1 @@
1
+ import{S as e,_ as t,a as n,b as r,d as i,f as a,g as o,h as ee,i as s,m as te,n as c,o as ne,p as l,r as u,s as d,t as f,v as re,x as ie,y as ae}from"./demo-shared-BVQKG6NC.js";var p=`6e400001-b5a3-f393-e0a9-e50e24dcca9e`,oe=`6e400003-b5a3-f393-e0a9-e50e24dcca9e`,se=`6e400002-b5a3-f393-e0a9-e50e24dcca9e`,m=20,ce=10;function le(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(p),a=await i.getCharacteristic(oe),o=await i.getCharacteristic(se);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+=m){let n=e.slice(t,t+m);await o.writeValueWithoutResponse(n),t+m<e.length&&await new Promise(e=>setTimeout(e,ce))}}})},async close(){r?.connected&&r.disconnect(),t=null,n=null}}}function ue(){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 le(await navigator.bluetooth.requestDevice({filters:[{services:[p]}]}))},async getPorts(){return[]}}}e.setProvider(ue());var h=[],g=[],de=class extends e{_hsCmd;_hsCmdMode;_hsExpect;_hsExpectMode;constructor(e,t,n,r,i){super(e),this._hsCmd=t,this._hsCmdMode=n,this._hsExpect=r,this._hsExpectMode=i}async handshake(){if(!this._hsCmd||(this._hsCmdMode===`hex`?await this.send(I(this._hsCmd)):await this.send(u(this._hsCmd)),!this._hsExpect))return!0;let e=this._hsExpect.trim();return new Promise(t=>{let n=r=>{if(this.off(`serial:data`,n),this._hsExpectMode===`hex`){let e=new TextEncoder().encode(String(r)),n=I(this._hsExpect);t(e.length===n.length&&e.every((e,t)=>e===n[t]))}else t(String(r).trim()===e)};this.on(`serial:data`,n)})}},_=e=>document.getElementById(e),v=_(`messages`),y=_(`btn-connect`),b=_(`btn-disconnect`),x=_(`btn-send`),S=_(`input-send`),C=_(`mode-toggle`),fe=_(`status-dot`),w=_(`status-text`),pe=_(`console-dot`),T=_(`console-text`),E=_(`sidebar`),D=_(`code-panel`),O=_(`code-view`),k=_(`code-tab`),me=_(`menu-btn`),he=_(`code-toggle-btn`),A=_(`theme-btn`),ge=_(`clear-btn`),j=_(`copy-btn`),_e=_(`dl-btn`),ve=_(`cfg-export-btn`),M=_(`cfg-import-input`);function N(){let e=e=>(_(e)?.value??``).trim(),t=(t,n)=>parseInt(e(t))||n,n=e=>_(e)?.value??``;return{bufferSize:t(`cfg-bufsize`,255),commandTimeout:t(`cfg-timeout`,3e3),handshakeTimeout:t(`cfg-handshake`,2e3),delimiter:e(`cfg-delim`),prepend:e(`cfg-prepend`),append:e(`cfg-append`),hsCmd:e(`cfg-hs-cmd`),hsCmdMode:n(`cfg-hs-cmd-mode`)||`text`,hsExpect:e(`cfg-hs-expect`),hsExpectMode:n(`cfg-hs-expect-mode`)||`text`}}function ye(e){let t=(e,t)=>{let n=document.getElementById(e);n&&(n instanceof HTMLInputElement&&n.type===`checkbox`?n.checked=!!t:(n instanceof HTMLInputElement||n instanceof HTMLSelectElement)&&(n.value=String(t??``)))};t(`cfg-bufsize`,e.bufferSize??255),t(`cfg-timeout`,e.commandTimeout??3e3),t(`cfg-handshake`,e.handshakeTimeout??2e3),t(`cfg-delim`,e.delimiter??`\\n`),t(`cfg-prepend`,e.prepend??``),t(`cfg-append`,e.append??``),t(`cfg-hs-cmd`,e.hsCmd??``),t(`cfg-hs-cmd-mode`,e.hsCmdMode??`text`),t(`cfg-hs-expect`,e.hsExpect??``),t(`cfg-hs-expect-mode`,e.hsExpectMode??`text`)}function P(e){let t=e.replace(/[^a-zA-Z0-9_$]/g,``).replace(/^[^a-zA-Z_$]/,`C`);return t.charAt(0).toUpperCase()+t.slice(1)||`MyDevice`}function F(e){return Array.from(e).map(e=>e.toString(16).toUpperCase().padStart(2,`0`)).join(` `)}function I(e){let t=e.replace(/\s+/g,``);if(t.length%2!=0)throw Error(`Odd number of hex characters.`);let n=new Uint8Array(t.length/2);for(let e=0;e<t.length;e+=2)n[e/2]=parseInt(t.substring(e,e+2),16);return n}function L(e,t){[fe,pe].forEach(t=>{t&&(t.className=`status-dot`,e!==`disconnected`&&t.classList.add(e))}),w&&(w.textContent=t),T&&(T.textContent=t)}var R=null;function z(){R&&clearTimeout(R),R=setTimeout(()=>{let e=N(),t=P((_(`dl-name`)?.value??`MyBleDevice`).trim()),n=(document.querySelector(`input[name='dl-lang']:checked`)?.value??`ts`)===`ts`,r=n?`ts`:`js`;re(O,d(e,t,n,h,g)),k&&(k.textContent=`${t.substring(0,10).toLowerCase()}.${r}`)},180)}var be=l();A&&(A.textContent=be===`dark`?`☀️`:`🌙`),me?.addEventListener(`click`,()=>E.classList.toggle(`collapsed`)),he?.addEventListener(`click`,()=>D.classList.toggle(`collapsed`));var B=_(`resize-handle`);if(B){let e=0,t=0,n=n=>{let r=Math.max(180,Math.min(700,t+(e-n.clientX)));document.documentElement.style.setProperty(`--code-w`,`${r}px`)},r=()=>{B.classList.remove(`dragging`),document.removeEventListener(`pointermove`,n)};B.addEventListener(`pointerdown`,i=>{e=i.clientX,t=D.getBoundingClientRect().width,B.classList.add(`dragging`),document.addEventListener(`pointermove`,n),document.addEventListener(`pointerup`,r,{once:!0}),i.preventDefault()})}var V=_(`sidebar-resize-handle`);if(V){let e=0,t=0,n=n=>{let r=Math.max(200,Math.min(600,t+(n.clientX-e)));document.documentElement.style.setProperty(`--sidebar-w`,`${r}px`)},r=()=>{V.classList.remove(`dragging`),document.removeEventListener(`pointermove`,n)};V.addEventListener(`pointerdown`,i=>{e=i.clientX,t=E.getBoundingClientRect().width,V.classList.add(`dragging`),document.addEventListener(`pointermove`,n),document.addEventListener(`pointerup`,r,{once:!0}),i.preventDefault()})}window.innerWidth<=960&&D.classList.add(`collapsed`),window.innerWidth<=640&&E.classList.add(`collapsed`),A?.addEventListener(`click`,()=>{let e=ae();A&&(A.textContent=e===`dark`?`☀️`:`🌙`)}),ge?.addEventListener(`click`,()=>c(v)),j?.addEventListener(`click`,async()=>{let e=O?.textContent??``;try{await navigator.clipboard.writeText(e),j&&(j.textContent=`Copied!`,j.classList.add(`copied`),setTimeout(()=>{j.textContent=`Copy`,j.classList.remove(`copied`)},1500))}catch{}}),_e?.addEventListener(`click`,()=>{let e=N(),r=(_(`dl-name`)?.value??`my-ble-device`).trim(),i=P(r),a=(document.querySelector(`input[name='dl-lang']:checked`)?.value??`ts`)===`ts`,s=document.querySelector(`input[name='dl-type']:checked`)?.value??`project`,c=a?`ts`:`js`,l=d(e,i,a,h,g);s===`project`?ne([{name:`device.${c}`,content:l},{name:`package.json`,content:ee(r,a,`ble`)},{name:`index.html`,content:te(i,c,`Web Bluetooth`)},{name:`README.md`,content:o(i,c,`Web Bluetooth`,`Requires a Chromium browser. The device must expose a Nordic UART Service (NUS) via BLE GATT.`)},...a?[{name:`tsconfig.json`,content:t()}]:[]],`${r}-project-${c}`):n(`${r}.${c}`,l)}),ve?.addEventListener(`click`,()=>{let e=(_(`dl-name`)?.value??`my-ble-device`).trim(),t=P(e);s({$version:1,provider:`ble`,className:e,language:document.querySelector(`input[name='dl-lang']:checked`)?.value??`ts`,dlType:document.querySelector(`input[name='dl-type']:checked`)?.value??`file`,cfg:N(),commands:h,listeners:g},t+`-config`)}),M?.addEventListener(`change`,()=>{let e=M.files?.[0];if(!e)return;let t=new FileReader;t.onload=e=>{try{let t=JSON.parse(e.target?.result);if(t.$version!==1||t.provider!==`ble`)return;let n=_(`dl-name`);n&&(n.value=t.className),document.querySelectorAll(`input[name='dl-lang']`).forEach(e=>{e.checked=e.value===t.language}),document.querySelectorAll(`input[name='dl-type']`).forEach(e=>{e.checked=e.value===t.dlType}),ye(t.cfg),h=t.commands.map(e=>({...e,id:crypto.randomUUID()})),g=t.listeners.map(e=>({...e,id:crypto.randomUUID()})),Q(),$(),z()}catch{}M.value=``},t.readAsText(e)}),[`cfg-bufsize`,`cfg-timeout`,`cfg-handshake`,`cfg-delim`,`cfg-prepend`,`cfg-append`,`cfg-hs-cmd`,`cfg-hs-cmd-mode`,`cfg-hs-expect`,`cfg-hs-expect-mode`,`dl-name`].forEach(e=>{let t=document.getElementById(e);t?.addEventListener(`change`,z),t?.addEventListener(`input`,z)}),document.querySelectorAll(`input[name='dl-lang']`).forEach(e=>e.addEventListener(`change`,z)),z();var H=`text`;C?.addEventListener(`click`,()=>{H=H===`text`?`hex`:`text`,C.textContent=H===`text`?`TXT`:`HEX`,S.placeholder=H===`text`?`Type a command, e.g. LED_ON`:`Hex bytes, e.g. FF 01 A3`});var U=null;function W(e){x.disabled=!e,S.disabled=!e,b.disabled=!e,y.disabled=e}y?.addEventListener(`click`,async()=>{if(U){try{await U.disconnect()}catch{}U=null}let e=N(),t=e.delimiter?u(e.delimiter):``;U=new de({baudRate:9600,bufferSize:e.bufferSize,commandTimeout:e.commandTimeout,parser:t?ie(t):r(),autoReconnect:!1,handshakeTimeout:e.handshakeTimeout},e.hsCmd,e.hsCmdMode,e.hsExpect,e.hsExpectMode),U.on(`serial:connecting`,()=>{L(`connecting`,`Connecting…`),y.disabled=!0,f(v,`Initiating Web Bluetooth connection…`,{kind:`system`})}),U.on(`serial:connected`,()=>{L(`connected`,`Connected`),W(!0),f(v,`Connected via Web Bluetooth!`,{kind:`system`})}),U.on(`serial:disconnected`,()=>{L(`disconnected`,`Disconnected`),W(!1),f(v,`Disconnected.`,{kind:`system`}),U=null}),U.on(`serial:data`,e=>{f(v,String(e),{kind:`received`,label:`Device`})}),U.on(`serial:error`,e=>{L(`error`,`Error`),f(v,`Error: ${e.message}`,{kind:`error`}),y.disabled=!1}),U.on(`serial:need-permission`,()=>{L(`error`,`Permission denied`),f(v,`Permission denied — select a valid BLE device and allow access.`,{kind:`error`}),y.disabled=!1}),U.on(`serial:timeout`,e=>{f(v,`Timeout: ${F(e)}`,{kind:`error`})});try{await U.connect()}catch{}}),b?.addEventListener(`click`,async()=>{await U?.disconnect()});async function G(){let e=S.value.trim();if(!e||!U)return;let t=N(),n=t.append?u(t.append):t.delimiter?u(t.delimiter):``;try{if(H===`hex`){let t=I(e);f(v,`HEX: ${F(t)}`,{kind:`sent`,label:`You`}),await U.send(t)}else{let r=t.prepend+e+n;f(v,e,{kind:`sent`,label:`You`}),await U.send(r)}S.value=``,S.focus()}catch(e){f(v,`Send error: ${e instanceof Error?e.message:String(e)}`,{kind:`error`})}}x?.addEventListener(`click`,G),S?.addEventListener(`keydown`,e=>{e.key===`Enter`&&G()});var K=_(`cmd-name`),q=_(`cmd-value`),xe=_(`cmd-mode`),Se=_(`cmd-add`),J=_(`cmd-list`),Y=_(`lst-name`),X=_(`lst-pattern`),Ce=_(`lst-match`),we=_(`lst-add`),Z=_(`lst-list`);function Q(){if(J){J.innerHTML=``;for(let e of h){let t=document.createElement(`div`);t.className=`chip`;let n=document.createElement(`span`);n.className=`chip-badge`,n.textContent=e.mode.toUpperCase();let r=document.createElement(`span`);r.className=`chip-name`,r.textContent=e.name;let i=document.createElement(`span`);i.className=`chip-val`,i.textContent=e.value;let a=document.createElement(`button`);a.className=`chip-send`,a.title=`Send now`,a.textContent=`▶`,a.addEventListener(`click`,()=>{if(U)if(e.mode===`hex`){let t=I(e.value);U.send(t).catch(()=>{}),f(v,`HEX: ${F(t)}`,{kind:`sent`,label:`You`})}else{let t=N(),n=t.append?u(t.append):t.delimiter?u(t.delimiter):``;U.send(t.prepend+e.value+n).catch(()=>{}),f(v,e.name,{kind:`sent`,label:`You`})}});let o=document.createElement(`button`);o.className=`chip-del`,o.title=`Remove`,o.textContent=`×`,o.addEventListener(`click`,()=>{h=h.filter(t=>t.id!==e.id),Q(),z()}),t.append(n,r,i,a,o),J.appendChild(t)}}}function $(){if(Z){Z.innerHTML=``;for(let e of g){let t=document.createElement(`div`);t.className=`chip`;let n=document.createElement(`span`);n.className=`chip-badge`,n.textContent=e.match;let r=document.createElement(`span`);r.className=`chip-name`,r.textContent=e.name;let i=document.createElement(`span`);i.className=`chip-val`,i.textContent=e.pattern;let a=document.createElement(`button`);a.className=`chip-del`,a.title=`Remove`,a.textContent=`×`,a.addEventListener(`click`,()=>{g=g.filter(t=>t.id!==e.id),$(),z()}),t.append(n,r,i,a),Z.appendChild(t)}}}Se?.addEventListener(`click`,()=>{let e=K?.value.trim(),t=q?.value.trim();if(!e||!t)return;let n=xe?.value??`text`;h.push({id:crypto.randomUUID(),name:e,value:t,mode:n}),K&&(K.value=``),q&&(q.value=``),Q(),z()}),we?.addEventListener(`click`,()=>{let e=Y?.value.trim(),t=X?.value.trim();if(!e||!t)return;let n=Ce?.value??`exact`;g.push({id:crypto.randomUUID(),name:e,pattern:t,match:n}),Y&&(Y.value=``),X&&(X.value=``),$(),z()}),navigator.bluetooth?f(v,`Web Bluetooth demo ready — configure settings and click Connect.`,{kind:`system`}):(f(v,`Web Bluetooth is NOT supported in this browser or OS.`,{kind:`error`}),y.disabled=!0),i(),a();