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 • 12.2 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 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();