UNPKG

webserial-core

Version:

A strongly-typed, event-driven, abstract TypeScript library for the Web Serial API with custom parsers, command queue, handshake validation, and auto-reconnect.

272 lines (239 loc) 34.5 kB
(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=` <div class="empty-state"> <div class="empty-icon">💬</div> <span>Messages cleared</span> </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(` `);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,` `).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` declare module 'webserial-core' { interface SerialEventMap<T> { `+e.map(e=>` '${n}:${w(e.name||`listener`)}': (data: string) => void;`).join(` `)+` } } `}function E(e,t){if(e.length===0)return``;let n=t?`: Promise<void>`:``,r=t?`public `:``;return` `+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(` `)+` `}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(` `);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. 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?` import type { SerialPortFilter } from 'webserial-core';`:``,f=x(e.filters);return`// device.${a} — Generated by webserial-core demo import { AbstractSerialDevice, ${s} } from 'webserial-core';${d} ${n?T(i,t):``} export class ${t} extends AbstractSerialDevice${n?`<string>`:``} { constructor(filters${n?`${u}`:` = []`}) { super({ baudRate: ${e.baudRate}, dataBits: ${e.dataBits}, stopBits: ${e.stopBits}, parity: '${e.parity}', flowControl: '${e.flowControl}', bufferSize: ${e.bufferSize}, commandTimeout: ${e.commandTimeout}, parser: ${c}, autoReconnect: ${e.autoReconnect}, autoReconnectInterval: ${e.autoReconnectInterval}, handshakeTimeout: ${e.handshakeTimeout}, filters, });${i.length>0?` this.startListening();`:``} } ${n?`protected `:``}async handshake()${l} { ${O(e.hsCmd,e.hsCmdMode,e.hsExpect,e.hsExpectMode,n)} } ${E(r,n)}${D(i,t,n)}} // ── Usage ──────────────────────────────────────────────────────── const device = new ${t}(${f}); device.on('serial:connected', () => console.log('Connected!')); device.on('serial:disconnected', () => console.log('Disconnected.')); device.on('serial:data', (data) => console.log('', data)); device.on('serial:error', (err) => console.error(err.message)); // Must be called from a user-gesture (click handler): // await device.connect(); // Send a message (prepend/append applied in your UI layer): // await device.send('${e.prepend}COMMAND${e.append}'); // Disconnect: // await device.disconnect(); `}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 import { AbstractSerialDevice, ${s}, createBluetoothProvider } from 'webserial-core'; // Inject the BLE provider before creating any device instance. AbstractSerialDevice.setProvider(createBluetoothProvider()); ${n?T(i,t):``} export class ${t} extends AbstractSerialDevice${n?`<string>`:``} { constructor() { super({ baudRate: 9600, // Nominal — not used over BLE GATT bufferSize: ${e.bufferSize}, commandTimeout: ${e.commandTimeout}, parser: ${c}, autoReconnect: false, // BLE requires user gesture to reconnect handshakeTimeout: ${e.handshakeTimeout}, });${i.length>0?` this.startListening();`:``} } ${n?`protected `:``}async handshake()${l} { ${O(e.hsCmd,e.hsCmdMode,e.hsExpect,e.hsExpectMode,n)} } ${E(r,n)}${D(i,t,n)}} // ── Usage ──────────────────────────────────────────────────────── const device = new ${t}(); device.on('serial:connected', () => console.log('BLE connected!')); device.on('serial:disconnected', () => console.log('Disconnected.')); device.on('serial:data', (data) => console.log('', data)); device.on('serial:error', (err) => console.error(err.message)); // Must be called from a user-gesture: // await device.connect(); // await device.send('${e.prepend}COMMAND${e.append}'); // await device.disconnect(); `}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?` import type { SerialPortFilter } from 'webserial-core';`:``,f=x(e.filters);return`// device.${a} — Generated by webserial-core demo import { AbstractSerialDevice, ${s}, WebUsbProvider } from 'webserial-core';${d} // Inject the WebUSB polyfill provider. AbstractSerialDevice.setProvider( new WebUsbProvider({ usbControlInterfaceClass: ${e.usbControlInterfaceClass}, usbTransferInterfaceClass: ${e.usbTransferInterfaceClass}, }) ); ${n?T(i,t):``} export class ${t} extends AbstractSerialDevice${n?`<string>`:``} { constructor(filters${n?`${u}`:` = []`}) { super({ baudRate: ${e.baudRate}, dataBits: ${e.dataBits}, stopBits: ${e.stopBits}, parity: '${e.parity}', flowControl: '${e.flowControl}', bufferSize: ${e.bufferSize}, commandTimeout: ${e.commandTimeout}, parser: ${c}, autoReconnect: ${e.autoReconnect}, autoReconnectInterval: ${e.autoReconnectInterval}, handshakeTimeout: ${e.handshakeTimeout}, filters, });${i.length>0?` this.startListening();`:``} } ${n?`protected `:``}async handshake()${l} { ${O(e.hsCmd,e.hsCmdMode,e.hsExpect,e.hsExpectMode,n)} } ${E(r,n)}${D(i,t,n)}} // ── Usage ──────────────────────────────────────────────────────── // CP2102/ESP32: { usbVendorId: 0x10c4, usbProductId: 0xea60 } // CH340/Arduino: { usbVendorId: 0x1a86, usbProductId: 0x7523 } const device = new ${t}(${f}); device.on('serial:connected', () => console.log('USB connected!')); device.on('serial:disconnected', () => console.log('Disconnected.')); device.on('serial:data', (data) => console.log('', data)); device.on('serial:error', (err) => console.error(err.message)); // await device.connect(); // await device.send('${e.prepend}COMMAND${e.append}'); // await device.disconnect(); `}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 import { AbstractSerialDevice, ${s}, createWebSocketProvider } from 'webserial-core'; // Inject the WebSocket bridge provider. // Start the Node.js bridge first: cd demos/websocket && node server.js const wsProvider = createWebSocketProvider('${e.wsUrl}'); AbstractSerialDevice.setProvider(wsProvider); ${n?T(i,t):``} export class ${t} extends AbstractSerialDevice${n?`<string>`:``} { constructor() { super({ baudRate: ${e.baudRate}, dataBits: ${e.dataBits}, stopBits: ${e.stopBits}, parity: '${e.parity}', flowControl: '${e.flowControl}', bufferSize: ${e.bufferSize}, commandTimeout: ${e.commandTimeout}, parser: ${c}, autoReconnect: ${e.autoReconnect}, autoReconnectInterval: ${e.autoReconnectInterval}, handshakeTimeout: ${e.handshakeTimeout}, });${i.length>0?` this.startListening();`:``} } ${n?`protected `:``}async handshake()${l} { ${O(e.hsCmd,e.hsCmdMode,e.hsExpect,e.hsExpectMode,n)} } ${E(r,n)}${D(i,t,n)}} // ── Usage ──────────────────────────────────────────────────────── const device = new ${t}(); device.on('serial:connected', () => console.log('WS connected!')); device.on('serial:disconnected', () => console.log('Disconnected.')); device.on('serial:data', (data) => console.log('', data)); device.on('serial:error', (err) => console.error(err.message)); // await device.connect(); // await device.send('${e.prepend}COMMAND${e.append}'); // await device.disconnect(); `}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> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>${e} — ${n}</title> <style> body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; padding: 0 16px; } pre { background: #0f0f0f; color: #4ade80; padding: 12px; border-radius: 8px; font-size: 0.82rem; min-height: 120px; overflow-y: auto; } .controls { display: flex; gap: 8px; margin-bottom: 12px; } button { padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer; } #btn-connect { background: #22c55e; color: #fff; } #btn-disconnect { background: #ef4444; color: #fff; } #btn-send { background: #6366f1; color: #fff; } input { flex: 1; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; } </style> </head> <body> <h2>${e}</h2> <div class="controls"> <button id="btn-connect">Connect</button> <button id="btn-disconnect" disabled>Disconnect</button> </div> <div class="controls"> <input id="input-send" type="text" placeholder="Command, e.g. LED_ON" disabled /> <button id="btn-send" disabled>Send</button> </div> <pre id="log">Waiting for connection...</pre> <script type="module"> import { ${e} } from './device.${t}'; const device = new ${e}(); const log = (msg) => { document.getElementById('log').textContent += msg + '\\n'; }; device.on('serial:connected', () => { log('✓ Connected'); document.getElementById('btn-disconnect').disabled = false; document.getElementById('btn-send').disabled = false; document.getElementById('input-send').disabled = false; }); device.on('serial:disconnected', () => { log('✗ Disconnected'); document.getElementById('btn-disconnect').disabled = true; document.getElementById('btn-send').disabled = true; document.getElementById('input-send').disabled = true; }); device.on('serial:data', (data) => log('' + data)); device.on('serial:error', (err) => log('' + err.message)); document.getElementById('btn-connect').onclick = () => device.connect(); document.getElementById('btn-disconnect').onclick = () => device.disconnect(); document.getElementById('btn-send').onclick = () => { const v = document.getElementById('input-send').value.trim(); if (v) { device.send(v + '\\n'); document.getElementById('input-send').value = ''; } }; document.getElementById('input-send').onkeydown = (e) => { if (e.key === 'Enter') document.getElementById('btn-send').click(); }; <\/script> </body> </html> `}function I(e,t,n,r){return`# ${e} A ${n} device using [webserial-core](https://github.com/danidoble/webserial-core). ${r} ## Setup \`\`\`bash npm install npm run dev \`\`\` ## Usage \`\`\`typescript import { ${e} } from './device.${t}'; const device = new ${e}(); device.on('serial:data', (data) => { console.log('Received:', data); }); // Must be called from a user-gesture (button click): await device.connect(); // Send data: await device.send('LED_ON\\n'); // Disconnect: await device.disconnect(); \`\`\` `}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};