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.
1 lines • 13.6 kB
JavaScript
import{S as e,_ as t,a as n,b as r,d as i,f as a,g as o,h as s,i as c,m as ee,n as l,o as te,p as u,r as d,t as f,u as p,v as ne,x as re,y as ie}from"./demo-shared-BVQKG6NC.js";function m(e){return new Promise((t,n)=>{e.addEventListener(`open`,()=>t(),{once:!0}),e.addEventListener(`error`,e=>n(e),{once:!0})})}function h(e,t){return new Promise(n=>{let r=i=>{let a=JSON.parse(i.data);a.type===t&&(e.removeEventListener(`message`,r),n(a.payload))};e.addEventListener(`message`,r)})}function g(e,t){let n=null,r=null;return{get readable(){return n},get writable(){return r},getInfo(){return{usbVendorId:t.vendorId,usbProductId:t.productId}},async open(i){e.send(JSON.stringify({type:`open`,path:t.path,baudRate:i.baudRate,dataBits:i.dataBits,stopBits:i.stopBits,parity:i.parity,parser:{type:`delimiter`,value:`\\n`}})),await h(e,`opened`);let a=[],o=null,s=!1;function c(e){let t=JSON.parse(e.data);if(t.type===`data`&&t.bytes){let e=new Uint8Array(t.bytes);o?o.enqueue(e):a.push(e)}t.type===`closed`&&(s=!0,o&&o.close())}e.addEventListener(`message`,c),n=new ReadableStream({start(e){o=e;for(let t of a)e.enqueue(t);a.length=0,s&&e.close()},cancel(){e.removeEventListener(`message`,c),o=null}}),r=new WritableStream({write(t){e.send(JSON.stringify({type:`write`,bytes:Array.from(t)}))}})},async close(){e.send(JSON.stringify({type:`close`})),n=null,r=null,e.close()}}}function ae(e){return{async requestPort(t){let n=new WebSocket(e);await m(n),n.send(JSON.stringify({type:`list-ports`,filters:t?.filters??[]}));let r=(await h(n,`port-list`))[0];if(!r)throw Error(`No ports available on the bridge server. Make sure the Node.js server is running and a device is connected.`);return g(n,r)},async getPorts(){let t=new WebSocket(e);return await m(t),t.send(JSON.stringify({type:`list-ports`,filters:[]})),(await h(t,`port-list`)).map(e=>g(t,e))}}}var _=[],v=[],oe=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(d(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)})}},y=e=>document.getElementById(e),b=y(`messages`),x=y(`btn-connect`),S=y(`btn-disconnect`),C=y(`btn-send`),w=y(`input-send`),T=y(`mode-toggle`),se=y(`status-dot`),E=y(`status-text`),ce=y(`console-dot`),D=y(`console-text`),O=y(`sidebar`),k=y(`code-panel`),le=y(`code-view`),ue=y(`code-tab`),de=y(`menu-btn`),fe=y(`code-toggle-btn`),A=y(`theme-btn`),pe=y(`clear-btn`),j=y(`copy-btn`),me=y(`dl-btn`),he=y(`cfg-export-btn`),M=y(`cfg-import-input`);function N(){let e=e=>(y(e)?.value??``).trim(),t=(t,n)=>parseInt(e(t))||n,n=e=>y(e)?.value??``;return{wsUrl:e(`cfg-wsurl`)||`ws://localhost:8080`,baudRate:t(`cfg-baud`,9600),dataBits:t(`cfg-databits`,8),stopBits:t(`cfg-stopbits`,1),parity:n(`cfg-parity`)||`none`,flowControl:n(`cfg-flow`)||`none`,bufferSize:t(`cfg-bufsize`,255),commandTimeout:t(`cfg-timeout`,3e3),autoReconnect:(e=>y(e)?.checked??!1)(`cfg-autoreconnect`),autoReconnectInterval:t(`cfg-reconnect-ms`,1500),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 ge(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-wsurl`,e.wsUrl??`ws://localhost:8080`),t(`cfg-baud`,e.baudRate??9600),t(`cfg-databits`,e.dataBits??8),t(`cfg-stopbits`,e.stopBits??1),t(`cfg-parity`,e.parity??`none`),t(`cfg-flow`,e.flowControl??`none`),t(`cfg-bufsize`,e.bufferSize??255),t(`cfg-timeout`,e.commandTimeout??3e3),t(`cfg-handshake`,e.handshakeTimeout??2e3),t(`cfg-reconnect-ms`,e.autoReconnectInterval??1500),t(`cfg-autoreconnect`,e.autoReconnect??!1),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){[se,ce].forEach(t=>{t&&(t.className=`status-dot`,e!==`disconnected`&&t.classList.add(e))}),E&&(E.textContent=t),D&&(D.textContent=t)}var R=null;function z(){R&&clearTimeout(R),R=setTimeout(()=>{let e=N(),t=P((y(`dl-name`)?.value??`MyWsDevice`).trim()),n=(document.querySelector(`input[name='dl-lang']:checked`)?.value??`ts`)===`ts`,r=n?`ts`:`js`;ne(le,p(e,t,n,_,v)),ue&&(ue.textContent=`${t.substring(0,10).toLowerCase()}.${r}`)},180)}var _e=u();A&&(A.textContent=_e===`dark`?`☀️`:`🌙`),de?.addEventListener(`click`,()=>O.classList.toggle(`collapsed`)),fe?.addEventListener(`click`,()=>k.classList.toggle(`collapsed`));var B=y(`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=k.getBoundingClientRect().width,B.classList.add(`dragging`),document.addEventListener(`pointermove`,n),document.addEventListener(`pointerup`,r,{once:!0}),i.preventDefault()})}var V=y(`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=O.getBoundingClientRect().width,V.classList.add(`dragging`),document.addEventListener(`pointermove`,n),document.addEventListener(`pointerup`,r,{once:!0}),i.preventDefault()})}window.innerWidth<=960&&k.classList.add(`collapsed`),window.innerWidth<=640&&O.classList.add(`collapsed`),A?.addEventListener(`click`,()=>{let e=ie();A&&(A.textContent=e===`dark`?`☀️`:`🌙`)}),pe?.addEventListener(`click`,()=>l(b)),j?.addEventListener(`click`,async()=>{let e=le?.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{}}),me?.addEventListener(`click`,()=>{let e=N(),r=(y(`dl-name`)?.value??`my-ws-device`).trim(),i=P(r),a=(document.querySelector(`input[name='dl-lang']:checked`)?.value??`ts`)===`ts`,c=document.querySelector(`input[name='dl-type']:checked`)?.value??`project`,l=a?`ts`:`js`,u=p(e,i,a,_,v);c===`project`?te([{name:`device.${l}`,content:u},{name:`package.json`,content:s(r,a,`ws`)},{name:`index.html`,content:ee(i,l,`WebSocket`)},{name:`README.md`,content:o(i,l,`WebSocket`,"Requires a WebSocket-to-serial bridge. Start the bridge: `cd demos/websocket && node server.js`")},...a?[{name:`tsconfig.json`,content:t()}]:[]],`${r}-project-${l}`):n(`${r}.${l}`,u)}),he?.addEventListener(`click`,()=>{let e=(y(`dl-name`)?.value??`my-ws-device`).trim(),t=P(e);c({$version:1,provider:`ws`,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:_,listeners:v},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!==`ws`)return;let n=y(`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}),ge(t.cfg),_=t.commands.map(e=>({...e,id:crypto.randomUUID()})),v=t.listeners.map(e=>({...e,id:crypto.randomUUID()})),Q(),$(),z()}catch{}M.value=``},t.readAsText(e)}),[`cfg-wsurl`,`cfg-baud`,`cfg-databits`,`cfg-stopbits`,`cfg-parity`,`cfg-flow`,`cfg-bufsize`,`cfg-timeout`,`cfg-handshake`,`cfg-reconnect-ms`,`cfg-autoreconnect`,`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`;T?.addEventListener(`click`,()=>{H=H===`text`?`hex`:`text`,T.textContent=H===`text`?`TXT`:`HEX`,w.placeholder=H===`text`?`Type a command, e.g. LED_ON`:`Hex bytes, e.g. FF 01 A3`});var U=null;function W(e){C.disabled=!e,w.disabled=!e,S.disabled=!e,x.disabled=e}x?.addEventListener(`click`,async()=>{if(U){try{await U.disconnect()}catch{}U=null}let t=N();e.setProvider(ae(t.wsUrl));let n=t.delimiter?d(t.delimiter):``;U=new oe({baudRate:t.baudRate,dataBits:t.dataBits,stopBits:t.stopBits,parity:t.parity,flowControl:t.flowControl,bufferSize:t.bufferSize,commandTimeout:t.commandTimeout,parser:n?re(n):r(),autoReconnect:t.autoReconnect,autoReconnectInterval:t.autoReconnectInterval,handshakeTimeout:t.handshakeTimeout},t.hsCmd,t.hsCmdMode,t.hsExpect,t.hsExpectMode),U.on(`serial:connecting`,()=>{L(`connecting`,`Connecting…`),x.disabled=!0,f(b,`Connecting to ${t.wsUrl}…`,{kind:`system`})}),U.on(`serial:connected`,()=>{L(`connected`,`Connected`),W(!0),f(b,`Connected via WebSocket (${t.wsUrl})!`,{kind:`system`})}),U.on(`serial:disconnected`,()=>{L(`disconnected`,`Disconnected`),W(!1),f(b,`Disconnected.`,{kind:`system`}),U=null}),U.on(`serial:data`,e=>{f(b,String(e),{kind:`received`,label:`Device`})}),U.on(`serial:error`,e=>{L(`error`,`Error`),f(b,`Error: ${e.message}`,{kind:`error`}),x.disabled=!1}),U.on(`serial:need-permission`,()=>{L(`error`,`Connection refused`),f(b,`Could not connect — is the WebSocket bridge running?`,{kind:`error`}),x.disabled=!1}),U.on(`serial:timeout`,e=>{f(b,`Timeout: ${F(e)}`,{kind:`error`})}),U.on(`serial:reconnecting`,()=>{L(`connecting`,`Reconnecting…`),f(b,`Auto-reconnecting via WebSocket…`,{kind:`system`})});try{await U.connect()}catch{}}),S?.addEventListener(`click`,async()=>{await U?.disconnect()});async function G(){let e=w.value.trim();if(!e||!U)return;let t=N(),n=t.append?d(t.append):t.delimiter?d(t.delimiter):``;try{if(H===`hex`){let t=I(e);f(b,`HEX: ${F(t)}`,{kind:`sent`,label:`You`}),await U.send(t)}else{let r=t.prepend+e+n;f(b,e,{kind:`sent`,label:`You`}),await U.send(r)}w.value=``,w.focus()}catch(e){f(b,`Send error: ${e instanceof Error?e.message:String(e)}`,{kind:`error`})}}C?.addEventListener(`click`,G),w?.addEventListener(`keydown`,e=>{e.key===`Enter`&&G()});var K=y(`cmd-name`),q=y(`cmd-value`),ve=y(`cmd-mode`),ye=y(`cmd-add`),J=y(`cmd-list`),Y=y(`lst-name`),X=y(`lst-pattern`),be=y(`lst-match`),xe=y(`lst-add`),Z=y(`lst-list`);function Q(){if(J){J.innerHTML=``;for(let e of _){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(b,`HEX: ${F(t)}`,{kind:`sent`,label:`You`})}else{let t=N(),n=t.append?d(t.append):d(t.delimiter);U.send(t.prepend+e.value+n).catch(()=>{}),f(b,e.name,{kind:`sent`,label:`You`})}});let o=document.createElement(`button`);o.className=`chip-del`,o.title=`Remove`,o.textContent=`×`,o.addEventListener(`click`,()=>{_=_.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 v){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`,()=>{v=v.filter(t=>t.id!==e.id),$(),z()}),t.append(n,r,i,a),Z.appendChild(t)}}}ye?.addEventListener(`click`,()=>{let e=K?.value.trim(),t=q?.value.trim();if(!e||!t)return;let n=ve?.value??`text`;_.push({id:crypto.randomUUID(),name:e,value:t,mode:n}),K&&(K.value=``),q&&(q.value=``),Q(),z()}),xe?.addEventListener(`click`,()=>{let e=Y?.value.trim(),t=X?.value.trim();if(!e||!t)return;let n=be?.value??`exact`;v.push({id:crypto.randomUUID(),name:e,pattern:t,match:n}),Y&&(Y.value=``),X&&(X.value=``),$(),z()}),f(b,`WebSocket bridge demo — start the Node.js bridge first: cd demos/websocket && node server.js`,{kind:`system`}),i(),a();