heliobind 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-4tf7hq8f.js +4 -0
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-hap08v1b.js +0 -4
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var E=null;function te(e){let t=new Uint8Array(e.length);for(let n=0;n<e.length;n+=1)t[n]=e.charCodeAt(n);return t}var Ze=new Uint8Array(16);class b extends Error{}var A=null;function ne(e){for(let[t,n]of[["key",e.key],["IV",e.iv]]){if(n.length!==16)throw new b(`the cipher ${t} must be 16 characters, got ${n.length}`);if(!/^[\x20-\x7e]+$/.test(n))throw new b(`the cipher ${t} must be printable ASCII`)}E=e,A=null}function re(){E=null,A=null}function oe(){return E!==null}function ie(){if(!E)throw new b("the cipher constants have not been set; see the note in the README");return E}function ae(){return te(ie().iv)}function se(){return A??=crypto.subtle.importKey("raw",te(ie().key),{name:"AES-CBC"},!1,["encrypt","decrypt"]),A}async function ce(e){let t=await crypto.subtle.encrypt({name:"AES-CBC",iv:ae()},await se(),e);return new Uint8Array(t)}async function le(e){if(e.length===0||e.length%16!==0)throw new b(`ciphertext is ${e.length} octets, not a whole number of blocks`);let t=await se(),n=e.subarray(e.length-16),r=new Uint8Array(16).fill(16),i=new Uint8Array(16);for(let p=0;p<16;p+=1)i[p]=r[p]^n[p];let c=(await Je(t,i)).subarray(0,16),a=new Uint8Array(e.length+16);a.set(e,0),a.set(c,e.length);let d=await crypto.subtle.decrypt({name:"AES-CBC",iv:ae()},t,a);return new Uint8Array(d)}async function Je(e,t){if(t.length!==16)throw new b("raw encryption takes exactly one block");let n=await crypto.subtle.encrypt({name:"AES-CBC",iv:Ze},e,t);return new Uint8Array(n)}var B={writeConfig:24,readConfig:25};class h extends Error{}function pe(e){let t=65535;for(let n of e){t^=n;for(let r=0;r<8;r+=1)t=t&1?t>>>1^40961:t>>>1}return t}function u(e,t,n){if(t+2>e.length)throw new h(`no room for a u16 at ${t}`);e[t]=n>>>8&255,e[t+1]=n&255}function f(e,t){if(t+2>e.length)throw new h(`no u16 available at ${t}`);return(e[t]&255)<<8|e[t+1]&255}function K(e){let t=new Uint8Array(e.length);for(let n=0;n<e.length;n+=1){let r=e.charCodeAt(n);if(r>127)throw new h(`value is not ASCII: ${JSON.stringify(e)}`);t[n]=r}return t}function H(e){return Array.from(e,(t)=>String.fromCharCode(t)).join("")}function M(e,t="0000000000"){if(e.length===0)throw new h("a read needs at least one parameter");let n=K(t),r=new Uint8Array(n.length+2+e.length*2);r.set(n,0),u(r,n.length,e.length);for(let[i,c]of e.entries())u(r,n.length+2+i*2,c);return r}function he(e,t="0000000000"){if(e.length===0)throw new h("a write needs at least one parameter");let n=e.map((d)=>K(d.value)),r=n.reduce((d,p)=>d+4+p.length,0),i=K(t),c=new Uint8Array(i.length+4+r);c.set(i,0),u(c,i.length,e.length),u(c,i.length+2,r);let a=i.length+4;return e.forEach((d,p)=>{let v=n[p];u(c,a,d.param),u(c,a+2,v.length),c.set(v,a+4),a+=4+v.length}),c}function ue(e,t,n){let r=new Uint8Array(8+t.length+2);return u(r,0,8+t.length-2+2),u(r,2,6),u(r,4,n+2),r[6]=1,r[7]=e,r.set(t,8),u(r,r.length-2,pe(r.subarray(0,r.length-2))),r}function W(e){if(e.length<2)return null;return f(e,0)+2}function fe(e){let t=W(e);if(t===null)return null;if(t<10)return!1;if(e.length<4)return null;let n=f(e,2);return n===6||n===7}function me(e){if(e.length<10)throw new h(`frame is ${e.length} octets, shorter than a header and CRC`);let t=f(e,0)+2;if(t!==e.length)throw new h(`header declares ${t} octets, got ${e.length}`);let n=pe(e.subarray(0,e.length-2)),r=f(e,e.length-2);if(n!==r)throw new h(`CRC mismatch: computed ${de(n)}, frame carries ${de(r)}`);let i=f(e,4)-2;if(i<0)throw new h("plaintext length field is less than two");return{protocol:f(e,2),address:e[6],function:e[7],plaintextLen:i,body:e.subarray(8,e.length-2)}}function de(e){return`0x${e.toString(16).padStart(4,"0")}`}function s(e,t,n,r){return{number:e,name:t,summary:r,writable:!1,confidence:n}}var ge=s(8,"serial_number","device","The device serial, also its MQTT client id."),ye=s(20,"model_id","device","Model identifier."),ve=s(21,"sw_version","device","Datalogger firmware version."),be=s(22,"hw_version","device","Hardware revision."),we=s(9,"protocol_version","device","Wire protocol version."),Qe=s(13,"device_type","inferred","A numeric type code."),et=s(16,"mac_address","device","MAC-shaped, but a constant shared across the product family rather than this unit's."),tt=s(7,"password","device","A six-digit vendor default."),R=s(54,"bind_key","device","The handshake key. Presenting it is authentication rather than configuration, and Device.open is the only thing that writes it."),nt=s(61,"sdk_version","device","ESP-IDF version of the running build."),xe=s(56,"wifi_ssid","device","Wi-Fi network name. Readable in clear. Writing this wrongly takes the device off the network."),ke=s(57,"wifi_password","device","Wi-Fi passphrase. Returned in clear by a read."),Ee=s(76,"wifi_signal","device","Signal strength in dBm, sign as sent."),_e=s(139,"dhcp_lease_0","device","Address, mask, gateway and resolver of the joined network."),Ce=s(140,"dhcp_lease_1","device","Address, mask, gateway and resolver of the joined network."),Le=s(12,"dns_ip","device","Resolver the device is configured with."),Se=s(17,"server_address","device","Hostname or address the device reports to. Reads back the vendor's broker; a write is untested."),Ae=s(18,"remote_port","device","Port the device dials."),Be=s(19,"remote_url","device","Hostname the device dials."),Re=s(4,"data_interval","device","Telemetry cadence in seconds."),Ie=s(31,"datetime","device","The device clock."),rt=s(30,"timezone","device","Reports GMT+8, and is not what the clock follows."),ot=s(32,"restart","vendor-app",'Write "1" to restart the datalogger. Recoverable: it reboots and reconnects by itself.'),it=s(35,"clear_log","vendor-app",'Write "1" to clear the datalogger log.'),at=s(14,"local_ip","device","Reports 192.168.5.1 and does not describe the live network."),st=s(25,"subnet_mask","device","Inert default, like local_ip."),ct=s(26,"default_gateway","device","Inert default, like local_ip."),lt=s(105,"network_mac","inferred","A second MAC-shaped field, equal to 16."),dt=s(106,"network_ip","inferred","A second address field."),pt=s(107,"network_mask","inferred","A second mask field."),ht=s(108,"network_gateway","inferred","A second gateway field."),ut=s(109,"network_dns","inferred","A second resolver field."),ft=s(80,"update_url","device","The last update URL the device was told to install."),mt=s(121,"link_diagnostics","device","A string of labelled fields: state, err_wifi, reconnect, server."),gt=s(144,"assembled_values","inferred","A concatenation of other parameters, including the handshake key of 54."),yt=s(55,"unknown_55","inferred","Read by the vendor app during provisioning. Contents unknown, which is why it is a good first read."),vt=s(60,"unknown_60","device",'Reads back "16". Meaning unestablished.'),bt=Array.from({length:15},(e,t)=>s(124+t,`connection_event_${String(t).padStart(2,"0")}`,"device","One timestamped connection record: time, network, endpoint. A slot in a ring buffer.")),V=[Re,tt,ge,we,Le,Qe,at,et,Se,Ae,Be,ye,ve,be,st,ct,rt,Ie,ot,it,R,yt,xe,ke,vt,nt,Ee,ft,lt,dt,pt,ht,ut,mt,...bt,_e,Ce,gt],Te=[ge,ye,ve,be,we,R,xe,ke,Ee,_e,Ce,Le,Se,Ae,Be,Re,Ie],z=145;function I(){return Array.from({length:z+1},(e,t)=>t)}function Pe(e){return e.map((t)=>t.number)}function Ne(e,t){let n=Math.max(1,Math.floor(t)),r=[];for(let i=0;i<e.length;i+=n)r.push(e.slice(i,i+n));return r}var wt=new Map(V.map((e)=>[e.number,e]));function xt(e){return wt.get(e)}function T(e){return xt(e)?.name??`unknown_${e}`}var j=10,Oe=j+2,G=Oe+1;class _ extends Error{}function q(e){return e.status===0}function De(e){if(e.length<G)throw new _(`response is ${e.length} octets, shorter than the ${G} it must have`);let t=H(e.subarray(0,j)),n=f(e,j),r=e[Oe],i=[],c=G;while(c<e.length){if(c+4>e.length)throw new _(`a parameter header is cut short ${e.length-c} octets from the end`);let a=f(e,c),d=f(e,c+2);if(c+4+d>e.length)throw new _(`parameter ${a} claims ${d} octets, more than the body holds`);i.push({param:a,value:H(e.subarray(c+4,c+4+d))}),c+=4+d}if(i.length>n)throw new _(`count says ${n} parameters, the body holds ${i.length}`);return{serial:t,count:n,status:r,values:i}}class Ue extends Error{}function kt(e){return e instanceof Error?e.message:String(e)}class P extends Ue{status;constructor(e){super(`the device refused the key, status ${e}`);this.status=e}}class N{connection;serial;constructor(e,t){this.connection=e;this.serial=t}static async open(e,t){let n=await F(e,B.writeConfig,he([{param:R.number,value:t}]));if(!q(n))throw new P(n.status);return new N(e,n.serial)}get connected(){return this.connection.connected}async read(e){return F(this.connection,B.readConfig,M(e))}async*sweep(e=1,t=I()){for(let n of Ne(t,e)){let r;try{r=await this.read(n)}catch(a){yield{kind:"unanswered",params:n,reason:kt(a)};continue}for(let a of r.values)yield{kind:"value",param:a.param,value:a.value};let i=new Set(r.values.map((a)=>a.param)),c=n.filter((a)=>!i.has(a));if(c.length>0)yield{kind:"silent",params:c}}}async readVia(e,t){return F(this.connection,B.readConfig,M(e),t)}}async function F(e,t,n,r){let i=ue(t,await ce(n),n.length),c=r?await e.requestVia(i,r):await e.request(i),a=me(c),d=(await le(a.body)).subarray(0,a.plaintextLen);return De(d)}var $e=!1;async function Ke(e){if(!("serviceWorker"in navigator))return"this browser does not support offline use";let t;try{t=await navigator.serviceWorker.register("./sw.js",{scope:"./"})}catch(r){return r instanceof Error?r.message:String(r)}navigator.serviceWorker.addEventListener("controllerchange",()=>{if($e)return;$e=!0,window.location.reload()}),e.onAccept(()=>{t.waiting?.postMessage("take-over")});let n=(r)=>{if(r?.state==="installed"&&navigator.serviceWorker.controller)e.show()};n(t.waiting),t.addEventListener("updatefound",()=>{let r=t.installing;r?.addEventListener("statechange",()=>n(r))});try{await t.update()}catch{}return null}var O={cipherKey:"heliobind.cipher.key",cipherIv:"heliobind.cipher.iv",bindKey:"heliobind.bind.key"},X={cipherKey:Y(()=>""),cipherIv:Y(()=>""),bindKey:Y(()=>"")};function Y(e){try{return e()??""}catch{return""}}function He(){return{cipherKey:Z("cipherKey")||X.cipherKey,cipherIv:Z("cipherIv")||X.cipherIv,bindKey:Z("bindKey")||X.bindKey}}function Me(e,t){try{if(t)localStorage.setItem(O[e],t);else localStorage.removeItem(O[e])}catch{}}function We(){for(let e of Object.values(O))try{localStorage.removeItem(e)}catch{}}function Z(e){try{return localStorage.getItem(O[e])??""}catch{return""}}var Ve=255,w={service:"000000ff-0000-1000-8000-00805f9b34fb",io:"0000ff01-0000-1000-8000-00805f9b34fb",info:"0000ff02-0000-1000-8000-00805f9b34fb",ff04:"0000ff04-0000-1000-8000-00805f9b34fb"},Et=5000;class g extends Error{}function J(){return typeof navigator<"u"&&"bluetooth"in navigator}async function ze(){if(!J())throw new g("this browser does not support Web Bluetooth");return navigator.bluetooth.requestDevice({filters:[{services:[Ve]}],optionalServices:[Ve]})}class D{device;io;onNotification=null;constructor(e,t){this.device=e;this.io=t}static async open(e){let t=e.gatt;if(!t)throw new g("device exposes no GATT server");let i=await(await(await t.connect()).getPrimaryService(w.service)).getCharacteristic(w.io),c=new D(e,i);return i.addEventListener("characteristicvaluechanged",()=>{let a=i.value;if(a)c.onNotification?.(new Uint8Array(a.buffer.slice(0)))}),await i.startNotifications(),c}get connected(){return this.device.gatt?.connected??!1}async close(){if(this.connected)this.device.gatt?.disconnect()}async readInfo(){let e=await this.device.gatt?.getPrimaryService(w.service);if(!e)throw new g("not connected");let n=await(await e.getCharacteristic(w.info)).readValue();return new Uint8Array(n.buffer.slice(0))}async request(e){if(!this.connected)throw new g("not connected");let t=this.collect();return await this.write(e),t}async requestVia(e,t){if(!this.connected)throw new g("not connected");let n=this.io;if(t.characteristic==="ff04"){let i=await this.device.gatt?.getPrimaryService(w.service);if(!i)throw new g("not connected");n=await i.getCharacteristic(w.ff04)}let r=this.collect();if(t.withoutResponse)await n.writeValueWithoutResponse(e);else await n.writeValueWithResponse(e);return r}async write(e){await this.io.writeValueWithResponse(e)}collect(){return new Promise((e,t)=>{let n=new Uint8Array(0),r=(a)=>{clearTimeout(i),this.io.removeEventListener("characteristicvaluechanged",c),a()},i=setTimeout(()=>{r(()=>t(new g(n.length===0?"no reply within the timeout":`reply incomplete: ${n.length} octets after the timeout`)))},Et),c=()=>{let a=this.io.value;if(!a)return;let d=new Uint8Array(a.buffer.slice(0)),p=new Uint8Array(n.length+d.length);if(p.set(n,0),p.set(d,n.length),n=p,fe(n)===!1){n=new Uint8Array(0);return}let v=W(n);if(v!==null&&n.length>=v){let Ye=n.subarray(0,v);r(()=>e(Ye))}};this.io.addEventListener("characteristicvaluechanged",c)})}}function l(e){let t=document.getElementById(e);if(!t)throw Error(`missing element: ${e}`);return t}var o={unsupported:l("unsupported"),app:l("app"),secrets:l("secrets"),cipherKey:l("cipher-key"),cipherIv:l("cipher-iv"),key:l("key"),forget:l("forget"),connect:l("connect"),device:l("device"),deviceName:l("device-name"),deviceStatus:l("device-status"),deviceSerial:l("device-serial"),readout:l("readout"),param:l("param"),read:l("read"),readProvisioning:l("read-provisioning"),readSpace:l("read-space"),info:l("info"),clearResult:l("clear-result"),clearLog:l("clear-log"),result:l("result"),log:l("log"),update:l("update"),updateNow:l("update-now"),build:l("build")};function x(e){o.deviceStatus.textContent=e}function Ge(e){let t="";for(let n of e){let r=n.charCodeAt(0);t+=r>=32&&r<=126?n:`\\x${r.toString(16).padStart(2,"0")}`}return t}function je(){o.result.textContent=""}function qe(){o.log.textContent=""}function y(e){o.result.textContent=o.result.textContent?`${o.result.textContent}
|
|
2
|
+
${e}`:e,o.result.scrollTop=o.result.scrollHeight}function m(e){let t=new Date().toISOString().slice(11,23),n=o.log.textContent;o.log.textContent=n?`${n}
|
|
3
|
+
${t} ${e}`:`${t} ${e}`}function U(e){return e instanceof Error?`${e.name}: ${e.message}`:String(e)}function Fe(e){for(let t of e){let n=document.createElement("option");n.value=String(t.number),n.textContent=`${t.number} — ${t.name}`,n.title=t.summary,o.param.append(n)}}var k={unsupported(){o.unsupported.hidden=!1},ready(){o.app.hidden=!1},device(e){o.device.hidden=!e},readout(e){o.readout.hidden=!e}};async function Q(e,t){for(let n of e)n.disabled=!0;try{return await t()}finally{for(let n of e)n.disabled=!1}}var _t="development",C=null,L=null;async function Ct(){o.connect.disabled=!0;try{let e=await ze();k.device(!0),o.deviceName.textContent=e.name??"(unnamed)",x("connecting"),e.addEventListener("gattserverdisconnected",Lt),C=await D.open(e),C.onNotification=(t)=>m(`notify ${t.length} octets`),m("connected"),x("authenticating"),L=await N.open(C,o.key.value.trim()),o.deviceSerial.textContent=L.serial,x("ready"),m(`authenticated; the device reports serial ${L.serial}`),k.readout(!0),y("Authenticated. Reads should now be answered.")}catch(e){x(e instanceof P?"key refused":"not connected"),y(U(e)),m(U(e)),S()}}function Lt(){C=null,L=null,m("device disconnected"),x("disconnected"),k.readout(!1),S()}function S(){let t=![o.cipherKey,o.cipherIv,o.key].some((n)=>n.value.trim()==="")&&oe();o.connect.disabled=!t,o.connect.title=t?"":"Enter the protocol constants first"}async function ee(e,t){let n=L;if(!n)return;y(`reading ${t}…`),await Q([o.read,o.readProvisioning,o.readSpace],async()=>{let r=0,i=0,c=0;for await(let a of n.sweep(1,e))switch(a.kind){case"value":if(a.value.length>0)r+=1;else i+=1;y(`${T(a.param).padEnd(22)} ${Ge(a.value)}`);break;case"silent":i+=a.params.length;break;case"unanswered":c+=a.params.length,m(`no answer for ${a.params.join(", ")}: ${a.reason}`);break}y(`
|
|
4
|
+
${r} held a value, ${i} answered nothing, ${c} went unanswered`),m(`read ${e.length} parameters: ${r} values, ${i} empty, ${c} lost`)})}async function St(){let e=C;if(!e)return;await Q([o.info],async()=>{try{let t=await e.readInfo();y(`0xFF02 returned ${t.length} octets: ${new TextDecoder().decode(t)}`)}catch(t){y(U(t))}})}function At(){Ke({show:()=>{o.update.hidden=!1,m("a newer version is ready; press Update to switch to it")},onAccept:(e)=>o.updateNow.addEventListener("click",e)}).then((e)=>{})}function Bt(){let e=He();o.cipherKey.value=e.cipherKey,o.cipherIv.value=e.cipherIv,o.key.value=e.bindKey;let t=()=>{try{ne({key:o.cipherKey.value.trim(),iv:o.cipherIv.value.trim()})}catch{re()}},n=(r,i)=>{i.addEventListener("input",()=>{if(r!=="bindKey")t();S()}),i.addEventListener("change",()=>Me(r,i.value.trim()))};n("cipherKey",o.cipherKey),n("cipherIv",o.cipherIv),n("bindKey",o.key),o.forget.addEventListener("click",(r)=>{r.preventDefault(),r.stopPropagation(),We();for(let i of[o.cipherKey,o.cipherIv,o.key])i.value="";o.secrets.open=!0,S(),m("the stored constants were forgotten")}),t(),S(),o.secrets.open=!(e.cipherKey&&e.cipherIv&&e.bindKey)}function Xe(){if(o.build.textContent=`build ${_t} · read-only`,At(),!J()){k.unsupported();return}Fe(V),Bt(),k.ready(),o.connect.addEventListener("click",()=>void Ct()),o.read.addEventListener("click",()=>{let e=Number(o.param.value);ee([e],T(e))}),o.readProvisioning.addEventListener("click",()=>void ee(Pe(Te),"the provisioning settings")),o.readSpace.addEventListener("click",()=>void ee(I(),`parameters 0 to ${z}`)),o.clearResult.addEventListener("click",je),o.clearLog.addEventListener("click",qe),o.info.addEventListener("click",()=>void St())}Xe();
|
package/dist/index.html
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<link rel="icon" href="./heliobind-pd6mv76e.svg" type="image/svg+xml">
|
|
25
25
|
<link rel="apple-touch-icon" href="./apple-touch-icon-78jb078y.png">
|
|
26
26
|
|
|
27
|
-
<link rel="stylesheet" crossorigin href="./chunk-h2a7cxvg.css"><script type="module" crossorigin src="./chunk-
|
|
27
|
+
<link rel="stylesheet" crossorigin href="./chunk-h2a7cxvg.css"><script type="module" crossorigin src="./chunk-4tf7hq8f.js"></script></head>
|
|
28
28
|
|
|
29
29
|
<body>
|
|
30
30
|
<main>
|
package/dist/sw.js
CHANGED
package/package.json
CHANGED
package/dist/chunk-hap08v1b.js
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
var E=null;function ee(e){let t=new Uint8Array(e.length);for(let n=0;n<e.length;n+=1)t[n]=e.charCodeAt(n);return t}var Ye=new Uint8Array(16);class b extends Error{}var A=null;function te(e){for(let[t,n]of[["key",e.key],["IV",e.iv]]){if(n.length!==16)throw new b(`the cipher ${t} must be 16 characters, got ${n.length}`);if(!/^[\x20-\x7e]+$/.test(n))throw new b(`the cipher ${t} must be printable ASCII`)}E=e,A=null}function ne(){E=null,A=null}function re(){return E!==null}function oe(){if(!E)throw new b("the cipher constants have not been set; see the note in the README");return E}function ie(){return ee(oe().iv)}function ae(){return A??=crypto.subtle.importKey("raw",ee(oe().key),{name:"AES-CBC"},!1,["encrypt","decrypt"]),A}async function se(e){let t=await crypto.subtle.encrypt({name:"AES-CBC",iv:ie()},await ae(),e);return new Uint8Array(t)}async function ce(e){if(e.length===0||e.length%16!==0)throw new b(`ciphertext is ${e.length} octets, not a whole number of blocks`);let t=await ae(),n=e.subarray(e.length-16),r=new Uint8Array(16).fill(16),i=new Uint8Array(16);for(let p=0;p<16;p+=1)i[p]=r[p]^n[p];let c=(await Ze(t,i)).subarray(0,16),a=new Uint8Array(e.length+16);a.set(e,0),a.set(c,e.length);let d=await crypto.subtle.decrypt({name:"AES-CBC",iv:ie()},t,a);return new Uint8Array(d)}async function Ze(e,t){if(t.length!==16)throw new b("raw encryption takes exactly one block");let n=await crypto.subtle.encrypt({name:"AES-CBC",iv:Ye},e,t);return new Uint8Array(n)}var B={writeConfig:24,readConfig:25};class h extends Error{}function de(e){let t=65535;for(let n of e){t^=n;for(let r=0;r<8;r+=1)t=t&1?t>>>1^40961:t>>>1}return t}function u(e,t,n){if(t+2>e.length)throw new h(`no room for a u16 at ${t}`);e[t]=n>>>8&255,e[t+1]=n&255}function f(e,t){if(t+2>e.length)throw new h(`no u16 available at ${t}`);return(e[t]&255)<<8|e[t+1]&255}function K(e){let t=new Uint8Array(e.length);for(let n=0;n<e.length;n+=1){let r=e.charCodeAt(n);if(r>127)throw new h(`value is not ASCII: ${JSON.stringify(e)}`);t[n]=r}return t}function H(e){return Array.from(e,(t)=>String.fromCharCode(t)).join("")}function M(e,t="0000000000"){if(e.length===0)throw new h("a read needs at least one parameter");let n=K(t),r=new Uint8Array(n.length+2+e.length*2);r.set(n,0),u(r,n.length,e.length);for(let[i,c]of e.entries())u(r,n.length+2+i*2,c);return r}function pe(e,t="0000000000"){if(e.length===0)throw new h("a write needs at least one parameter");let n=e.map((d)=>K(d.value)),r=n.reduce((d,p)=>d+4+p.length,0),i=K(t),c=new Uint8Array(i.length+4+r);c.set(i,0),u(c,i.length,e.length),u(c,i.length+2,r);let a=i.length+4;return e.forEach((d,p)=>{let v=n[p];u(c,a,d.param),u(c,a+2,v.length),c.set(v,a+4),a+=4+v.length}),c}function he(e,t,n){let r=new Uint8Array(8+t.length+2);return u(r,0,8+t.length-2+2),u(r,2,6),u(r,4,n+2),r[6]=1,r[7]=e,r.set(t,8),u(r,r.length-2,de(r.subarray(0,r.length-2))),r}function W(e){if(e.length<2)return null;return f(e,0)+2}function ue(e){let t=W(e);if(t===null)return null;if(t<10)return!1;if(e.length<4)return null;let n=f(e,2);return n===6||n===7}function fe(e){if(e.length<10)throw new h(`frame is ${e.length} octets, shorter than a header and CRC`);let t=f(e,0)+2;if(t!==e.length)throw new h(`header declares ${t} octets, got ${e.length}`);let n=de(e.subarray(0,e.length-2)),r=f(e,e.length-2);if(n!==r)throw new h(`CRC mismatch: computed ${le(n)}, frame carries ${le(r)}`);let i=f(e,4)-2;if(i<0)throw new h("plaintext length field is less than two");return{protocol:f(e,2),address:e[6],function:e[7],plaintextLen:i,body:e.subarray(8,e.length-2)}}function le(e){return`0x${e.toString(16).padStart(4,"0")}`}function s(e,t,n,r){return{number:e,name:t,summary:r,writable:!1,confidence:n}}var me=s(8,"serial_number","device","The device serial, also its MQTT client id."),ge=s(20,"model_id","device","Model identifier."),ye=s(21,"sw_version","device","Datalogger firmware version."),ve=s(22,"hw_version","device","Hardware revision."),be=s(9,"protocol_version","device","Wire protocol version."),Je=s(13,"device_type","inferred","A numeric type code."),Qe=s(16,"mac_address","device","MAC-shaped, but a constant shared across the product family rather than this unit's."),et=s(7,"password","device","A six-digit vendor default."),R=s(54,"bind_key","device","The handshake key. Presenting it is authentication rather than configuration, and Device.open is the only thing that writes it."),tt=s(61,"sdk_version","device","ESP-IDF version of the running build."),we=s(56,"wifi_ssid","device","Wi-Fi network name. Readable in clear. Writing this wrongly takes the device off the network."),xe=s(57,"wifi_password","device","Wi-Fi passphrase. Returned in clear by a read."),ke=s(76,"wifi_signal","device","Signal strength in dBm, sign as sent."),Ee=s(139,"dhcp_lease_0","device","Address, mask, gateway and resolver of the joined network."),_e=s(140,"dhcp_lease_1","device","Address, mask, gateway and resolver of the joined network."),Ce=s(12,"dns_ip","device","Resolver the device is configured with."),Le=s(17,"server_address","device","Hostname or address the device reports to. Reads back the vendor's broker; a write is untested."),Se=s(18,"remote_port","device","Port the device dials."),Ae=s(19,"remote_url","device","Hostname the device dials."),Be=s(4,"data_interval","device","Telemetry cadence in seconds."),Re=s(31,"datetime","device","The device clock."),nt=s(30,"timezone","device","Reports GMT+8, and is not what the clock follows."),rt=s(32,"restart","vendor-app",'Write "1" to restart the datalogger. Recoverable: it reboots and reconnects by itself.'),ot=s(35,"clear_log","vendor-app",'Write "1" to clear the datalogger log.'),it=s(14,"local_ip","device","Reports 192.168.5.1 and does not describe the live network."),at=s(25,"subnet_mask","device","Inert default, like local_ip."),st=s(26,"default_gateway","device","Inert default, like local_ip."),ct=s(105,"network_mac","inferred","A second MAC-shaped field, equal to 16."),lt=s(106,"network_ip","inferred","A second address field."),dt=s(107,"network_mask","inferred","A second mask field."),pt=s(108,"network_gateway","inferred","A second gateway field."),ht=s(109,"network_dns","inferred","A second resolver field."),ut=s(80,"update_url","device","The last update URL the device was told to install."),ft=s(121,"link_diagnostics","device","A string of labelled fields: state, err_wifi, reconnect, server."),mt=s(144,"assembled_values","inferred","A concatenation of other parameters, including the handshake key of 54."),gt=s(55,"unknown_55","inferred","Read by the vendor app during provisioning. Contents unknown, which is why it is a good first read."),yt=s(60,"unknown_60","device",'Reads back "16". Meaning unestablished.'),vt=Array.from({length:15},(e,t)=>s(124+t,`connection_event_${String(t).padStart(2,"0")}`,"device","One timestamped connection record: time, network, endpoint. A slot in a ring buffer.")),V=[Be,et,me,be,Ce,Je,it,Qe,Le,Se,Ae,ge,ye,ve,at,st,nt,Re,rt,ot,R,gt,we,xe,yt,tt,ke,ut,ct,lt,dt,pt,ht,ft,...vt,Ee,_e,mt],Ie=[me,ge,ye,ve,be,R,we,xe,ke,Ee,_e,Ce,Le,Se,Ae,Be,Re],z=145;function I(){return Array.from({length:z+1},(e,t)=>t)}function Te(e){return e.map((t)=>t.number)}function Pe(e,t){let n=Math.max(1,Math.floor(t)),r=[];for(let i=0;i<e.length;i+=n)r.push(e.slice(i,i+n));return r}var bt=new Map(V.map((e)=>[e.number,e]));function wt(e){return bt.get(e)}function T(e){return wt(e)?.name??`unknown_${e}`}var j=10,Ne=j+2,G=Ne+1;class _ extends Error{}function q(e){return e.status===0}function Oe(e){if(e.length<G)throw new _(`response is ${e.length} octets, shorter than the ${G} it must have`);let t=H(e.subarray(0,j)),n=f(e,j),r=e[Ne],i=[],c=G;while(c<e.length){if(c+4>e.length)throw new _(`a parameter header is cut short ${e.length-c} octets from the end`);let a=f(e,c),d=f(e,c+2);if(c+4+d>e.length)throw new _(`parameter ${a} claims ${d} octets, more than the body holds`);i.push({param:a,value:H(e.subarray(c+4,c+4+d))}),c+=4+d}if(i.length>n)throw new _(`count says ${n} parameters, the body holds ${i.length}`);return{serial:t,count:n,status:r,values:i}}class De extends Error{}function xt(e){return e instanceof Error?e.message:String(e)}class P extends De{status;constructor(e){super(`the device refused the key, status ${e}`);this.status=e}}class N{connection;serial;constructor(e,t){this.connection=e;this.serial=t}static async open(e,t){let n=await F(e,B.writeConfig,pe([{param:R.number,value:t}]));if(!q(n))throw new P(n.status);return new N(e,n.serial)}get connected(){return this.connection.connected}async read(e){return F(this.connection,B.readConfig,M(e))}async*sweep(e=1,t=I()){for(let n of Pe(t,e)){let r;try{r=await this.read(n)}catch(a){yield{kind:"unanswered",params:n,reason:xt(a)};continue}for(let a of r.values)yield{kind:"value",param:a.param,value:a.value};let i=new Set(r.values.map((a)=>a.param)),c=n.filter((a)=>!i.has(a));if(c.length>0)yield{kind:"silent",params:c}}}async readVia(e,t){return F(this.connection,B.readConfig,M(e),t)}}async function F(e,t,n,r){let i=he(t,await se(n),n.length),c=r?await e.requestVia(i,r):await e.request(i),a=fe(c),d=(await ce(a.body)).subarray(0,a.plaintextLen);return Oe(d)}var Ue=!1;async function $e(e){if(!("serviceWorker"in navigator))return"this browser does not support offline use";let t;try{t=await navigator.serviceWorker.register("./sw.js",{scope:"./"})}catch(r){return r instanceof Error?r.message:String(r)}navigator.serviceWorker.addEventListener("controllerchange",()=>{if(Ue)return;Ue=!0,window.location.reload()}),e.onAccept(()=>{t.waiting?.postMessage("take-over")});let n=(r)=>{if(r?.state==="installed"&&navigator.serviceWorker.controller)e.show()};n(t.waiting),t.addEventListener("updatefound",()=>{let r=t.installing;r?.addEventListener("statechange",()=>n(r))});try{await t.update()}catch{}return null}var O={cipherKey:"heliobind.cipher.key",cipherIv:"heliobind.cipher.iv",bindKey:"heliobind.bind.key"},X={cipherKey:process.env.BUN_PUBLIC_HELIOBIND_CIPHER_KEY??"",cipherIv:process.env.BUN_PUBLIC_HELIOBIND_CIPHER_IV??"",bindKey:process.env.BUN_PUBLIC_HELIOBIND_BIND_KEY??""};function Ke(){return{cipherKey:Y("cipherKey")||X.cipherKey,cipherIv:Y("cipherIv")||X.cipherIv,bindKey:Y("bindKey")||X.bindKey}}function He(e,t){try{if(t)localStorage.setItem(O[e],t);else localStorage.removeItem(O[e])}catch{}}function Me(){for(let e of Object.values(O))try{localStorage.removeItem(e)}catch{}}function Y(e){try{return localStorage.getItem(O[e])??""}catch{return""}}var We=255,w={service:"000000ff-0000-1000-8000-00805f9b34fb",io:"0000ff01-0000-1000-8000-00805f9b34fb",info:"0000ff02-0000-1000-8000-00805f9b34fb",ff04:"0000ff04-0000-1000-8000-00805f9b34fb"},kt=5000;class g extends Error{}function Z(){return typeof navigator<"u"&&"bluetooth"in navigator}async function Ve(){if(!Z())throw new g("this browser does not support Web Bluetooth");return navigator.bluetooth.requestDevice({filters:[{services:[We]}],optionalServices:[We]})}class D{device;io;onNotification=null;constructor(e,t){this.device=e;this.io=t}static async open(e){let t=e.gatt;if(!t)throw new g("device exposes no GATT server");let i=await(await(await t.connect()).getPrimaryService(w.service)).getCharacteristic(w.io),c=new D(e,i);return i.addEventListener("characteristicvaluechanged",()=>{let a=i.value;if(a)c.onNotification?.(new Uint8Array(a.buffer.slice(0)))}),await i.startNotifications(),c}get connected(){return this.device.gatt?.connected??!1}async close(){if(this.connected)this.device.gatt?.disconnect()}async readInfo(){let e=await this.device.gatt?.getPrimaryService(w.service);if(!e)throw new g("not connected");let n=await(await e.getCharacteristic(w.info)).readValue();return new Uint8Array(n.buffer.slice(0))}async request(e){if(!this.connected)throw new g("not connected");let t=this.collect();return await this.write(e),t}async requestVia(e,t){if(!this.connected)throw new g("not connected");let n=this.io;if(t.characteristic==="ff04"){let i=await this.device.gatt?.getPrimaryService(w.service);if(!i)throw new g("not connected");n=await i.getCharacteristic(w.ff04)}let r=this.collect();if(t.withoutResponse)await n.writeValueWithoutResponse(e);else await n.writeValueWithResponse(e);return r}async write(e){await this.io.writeValueWithResponse(e)}collect(){return new Promise((e,t)=>{let n=new Uint8Array(0),r=(a)=>{clearTimeout(i),this.io.removeEventListener("characteristicvaluechanged",c),a()},i=setTimeout(()=>{r(()=>t(new g(n.length===0?"no reply within the timeout":`reply incomplete: ${n.length} octets after the timeout`)))},kt),c=()=>{let a=this.io.value;if(!a)return;let d=new Uint8Array(a.buffer.slice(0)),p=new Uint8Array(n.length+d.length);if(p.set(n,0),p.set(d,n.length),n=p,ue(n)===!1){n=new Uint8Array(0);return}let v=W(n);if(v!==null&&n.length>=v){let Xe=n.subarray(0,v);r(()=>e(Xe))}};this.io.addEventListener("characteristicvaluechanged",c)})}}function l(e){let t=document.getElementById(e);if(!t)throw Error(`missing element: ${e}`);return t}var o={unsupported:l("unsupported"),app:l("app"),secrets:l("secrets"),cipherKey:l("cipher-key"),cipherIv:l("cipher-iv"),key:l("key"),forget:l("forget"),connect:l("connect"),device:l("device"),deviceName:l("device-name"),deviceStatus:l("device-status"),deviceSerial:l("device-serial"),readout:l("readout"),param:l("param"),read:l("read"),readProvisioning:l("read-provisioning"),readSpace:l("read-space"),info:l("info"),clearResult:l("clear-result"),clearLog:l("clear-log"),result:l("result"),log:l("log"),update:l("update"),updateNow:l("update-now"),build:l("build")};function x(e){o.deviceStatus.textContent=e}function ze(e){let t="";for(let n of e){let r=n.charCodeAt(0);t+=r>=32&&r<=126?n:`\\x${r.toString(16).padStart(2,"0")}`}return t}function Ge(){o.result.textContent=""}function je(){o.log.textContent=""}function y(e){o.result.textContent=o.result.textContent?`${o.result.textContent}
|
|
2
|
-
${e}`:e,o.result.scrollTop=o.result.scrollHeight}function m(e){let t=new Date().toISOString().slice(11,23),n=o.log.textContent;o.log.textContent=n?`${n}
|
|
3
|
-
${t} ${e}`:`${t} ${e}`}function U(e){return e instanceof Error?`${e.name}: ${e.message}`:String(e)}function qe(e){for(let t of e){let n=document.createElement("option");n.value=String(t.number),n.textContent=`${t.number} — ${t.name}`,n.title=t.summary,o.param.append(n)}}var k={unsupported(){o.unsupported.hidden=!1},ready(){o.app.hidden=!1},device(e){o.device.hidden=!e},readout(e){o.readout.hidden=!e}};async function J(e,t){for(let n of e)n.disabled=!0;try{return await t()}finally{for(let n of e)n.disabled=!1}}var Et="development",C=null,L=null;async function _t(){o.connect.disabled=!0;try{let e=await Ve();k.device(!0),o.deviceName.textContent=e.name??"(unnamed)",x("connecting"),e.addEventListener("gattserverdisconnected",Ct),C=await D.open(e),C.onNotification=(t)=>m(`notify ${t.length} octets`),m("connected"),x("authenticating"),L=await N.open(C,o.key.value.trim()),o.deviceSerial.textContent=L.serial,x("ready"),m(`authenticated; the device reports serial ${L.serial}`),k.readout(!0),y("Authenticated. Reads should now be answered.")}catch(e){x(e instanceof P?"key refused":"not connected"),y(U(e)),m(U(e)),S()}}function Ct(){C=null,L=null,m("device disconnected"),x("disconnected"),k.readout(!1),S()}function S(){let t=![o.cipherKey,o.cipherIv,o.key].some((n)=>n.value.trim()==="")&&re();o.connect.disabled=!t,o.connect.title=t?"":"Enter the protocol constants first"}async function Q(e,t){let n=L;if(!n)return;y(`reading ${t}…`),await J([o.read,o.readProvisioning,o.readSpace],async()=>{let r=0,i=0,c=0;for await(let a of n.sweep(1,e))switch(a.kind){case"value":if(a.value.length>0)r+=1;else i+=1;y(`${T(a.param).padEnd(22)} ${ze(a.value)}`);break;case"silent":i+=a.params.length;break;case"unanswered":c+=a.params.length,m(`no answer for ${a.params.join(", ")}: ${a.reason}`);break}y(`
|
|
4
|
-
${r} held a value, ${i} answered nothing, ${c} went unanswered`),m(`read ${e.length} parameters: ${r} values, ${i} empty, ${c} lost`)})}async function Lt(){let e=C;if(!e)return;await J([o.info],async()=>{try{let t=await e.readInfo();y(`0xFF02 returned ${t.length} octets: ${new TextDecoder().decode(t)}`)}catch(t){y(U(t))}})}function St(){$e({show:()=>{o.update.hidden=!1,m("a newer version is ready; press Update to switch to it")},onAccept:(e)=>o.updateNow.addEventListener("click",e)}).then((e)=>{})}function At(){let e=Ke();o.cipherKey.value=e.cipherKey,o.cipherIv.value=e.cipherIv,o.key.value=e.bindKey;let t=()=>{try{te({key:o.cipherKey.value.trim(),iv:o.cipherIv.value.trim()})}catch{ne()}},n=(r,i)=>{i.addEventListener("input",()=>{if(r!=="bindKey")t();S()}),i.addEventListener("change",()=>He(r,i.value.trim()))};n("cipherKey",o.cipherKey),n("cipherIv",o.cipherIv),n("bindKey",o.key),o.forget.addEventListener("click",(r)=>{r.preventDefault(),r.stopPropagation(),Me();for(let i of[o.cipherKey,o.cipherIv,o.key])i.value="";o.secrets.open=!0,S(),m("the stored constants were forgotten")}),t(),S(),o.secrets.open=!(e.cipherKey&&e.cipherIv&&e.bindKey)}function Fe(){if(o.build.textContent=`build ${Et} · read-only`,St(),!Z()){k.unsupported();return}qe(V),At(),k.ready(),o.connect.addEventListener("click",()=>void _t()),o.read.addEventListener("click",()=>{let e=Number(o.param.value);Q([e],T(e))}),o.readProvisioning.addEventListener("click",()=>void Q(Te(Ie),"the provisioning settings")),o.readSpace.addEventListener("click",()=>void Q(I(),`parameters 0 to ${z}`)),o.clearResult.addEventListener("click",Ge),o.clearLog.addEventListener("click",je),o.info.addEventListener("click",()=>void Lt())}Fe();
|