lavva.webbluetooth
Version:
Library implementing WebBluetooth custom functionality if underlying platform does support it
2 lines • 19.4 kB
JavaScript
(()=>{"use strict";var e,t,i,r={d:(e,t)=>{for(var i in t)r.o(t,i)&&!r.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:t[i]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};r.d({},{_f:()=>d}),function(e){e.Lavva="lavva",e.Exalus="exalus",e.Wisniowski="wisniowski"}(e||(e={})),function(e){e.En="en",e.De="de",e.Pl="pl",e.It="it",e.Es="es",e.Fr="fr",e.Ro="ro",e.Hu="hu",e.Lt="lt",e.No="no",e.Fi="fi",e.Sv="sv",e.Nl="nl",e.Pt="pt",e.Cz="cz",e.Uk="uk",e.Unknown="en"}(t||(t={})),function(e){e[e.Unknown=0]="Unknown",e[e.Remote=1]="Remote",e[e.BlindController=2]="BlindController",e[e.LightController=3]="LightController",e[e.HeatingController=4]="HeatingController",e[e.FacadeBlindController=5]="FacadeBlindController",e[e.GateController=6]="GateController",e[e.DoorController=7]="DoorController",e[e.GatewayController=8]="GatewayController",e[e.Sensor=9]="Sensor",e[e.SwitchController=10]="SwitchController",e[e.Meters=11]="Meters",e[e.Cameras=12]="Cameras",e[e.Intercoms=13]="Intercoms",e[e.GarageDoorController=14]="GarageDoorController",e[e.FencesWicketGatesAndGatesController=15]="FencesWicketGatesAndGatesController"}(i||(i={}));class n{static Delay(e){return new Promise((t=>setTimeout(t,e)))}}var s,o,a,c=function(e,t,i,r){return new(i||(i=Promise))((function(n,s){function o(e){try{c(r.next(e))}catch(e){s(e)}}function a(e){try{c(r.throw(e))}catch(e){s(e)}}function c(e){var t;e.done?n(e.value):(t=e.value,t instanceof i?t:new i((function(e){e(t)}))).then(o,a)}c((r=r.apply(e,t||[])).next())}))};class l{static getTimestamp(){const e=new Date;return`${String(e.getHours()).padStart(2,"0")}:${String(e.getMinutes()).padStart(2,"0")}:${String(e.getSeconds()).padStart(2,"0")}.${String(e.getMilliseconds()).padStart(3,"0")}`}static logInfo(e,...t){console.log(`${l.getTimestamp()} ${e}`,...t)}static logWarn(e,...t){console.warn(`${l.getTimestamp()} ${e}`,...t)}static logError(e,...t){console.error(`${l.getTimestamp()} ${e}`,...t)}constructor(){this.receiveBuffer="",this._maxCharacteristicValueLength=244,this.serviceUuid=65504,this.characteristicUuid=65505,this.receiveSeparator="\n",this.sendSeparator="\n",this.characteristic=null,this.notificationsStarted=!1,this.pendingResponses=[],this.OnGattServiceDisconnectedEvent=new d("OnGattServiceDisconnectedEvent"),this.OnCharacteristicValueChangedEvent=new d("OnCharacteristicValueChangedEvent"),this.OnConnectedEvent=new d("OnConnectedEvent"),this.OnDisconnectedEvent=new d("OnDisconnectedEvent"),this.filter=[{namePrefix:"Lavva-",services:[this.serviceUuid]},{namePrefix:"smartAW",services:[this.serviceUuid]}],this.WHITESPACE_NAMES={32:"SPACE",9:"TAB",10:"LF",13:"CR",11:"VT",12:"FF",160:"NBSP",8232:"LINE SEPARATOR",8233:"PARA SEPARATOR"},this.handleCharacteristicValueChanged=e=>{var t,i;try{if(!this.characteristic)return void this.logWarn("handleCharacteristicValueChanged: this.characteristic is null, ignoring");const r=e.target;if(r!==this.characteristic)return void this.logWarn(`handleCharacteristicValueChanged: event from foreign characteristic ${r.uuid}, expected ${this.characteristic.uuid}`);if(!this.isExpectedCharacteristic(r))return;const n=(new TextDecoder).decode(r.value);this.logWarn(this.visualizeTrailingWhitespace(`Received characteristic value change: ${n}`)),this.receiveBuffer+=n;const s=this.receiveBuffer.split(this.receiveSeparator);this.receiveBuffer=null!==(t=s.pop())&&void 0!==t?t:"";for(const e of s){const t=this.normalizeFrame(e);t&&(this.logWarn(`Received data: ${t}`),this.dispatchPendingResponse(t),null===(i=this.OnCharacteristicValueChangedEvent)||void 0===i||i.Invoke(t))}}catch(e){this.logError("Error handling characteristic value changed:",e)}},l.instances.add(this)}Dispose(){l.instances.delete(this)}getTimestamp(){return l.getTimestamp()}logInfo(e,...t){console.log(`${this.getTimestamp()} ${e}`,...t)}logWarn(e,...t){console.warn(`${this.getTimestamp()} ${e}`,...t)}logError(e,...t){console.error(`${this.getTimestamp()} ${e}`,...t)}visualizeTrailingWhitespace(e){const t=e.match(/\s+$/u);return t?e.slice(0,-t[0].length)+[...t[0]].map((e=>{switch(e){case" ":return"·";case"\t":return"→";case"\r":return"␍";case"\n":return"␊";case" ":return"⍽";default:return"□"}})).join(""):e}logTrailingWhitespaceDetail(e){const t=e.match(/\s+$/u);t?[...t[0]].forEach(((e,t)=>{var i;const r=e.codePointAt(0),n=null!==(i=this.WHITESPACE_NAMES[r])&&void 0!==i?i:"UNKNOWN";this.logInfo(`${t}: U+${r.toString(16).toUpperCase().padStart(4,"0")} (${r}) — ${n}`)})):this.logInfo("🚫 Brak białych znaków na końcu łańcucha.")}updateMaxPayload(e){const t=Math.min(Math.max(e-3,20),244);this._maxCharacteristicValueLength=t}SetFilter(e){this.filter=e}GetConnectedDevice(){return l.selectedDevice}expectedUuidFrom16Bit(e){return`0000${e.toString(16).padStart(4,"0").toLowerCase()}-0000-1000-8000-00805f9b34fb`}isExpectedCharacteristic(e){const t=this.expectedUuidFrom16Bit(this.characteristicUuid),i=e.uuid.toLowerCase(),r=i===t;return this.logWarn(`UUID compare: actual=${i} expected=${t} rawExpected=${this.characteristicUuid} => ${r}`),r}RequestDeviceAndConnectAsync(){return c(this,void 0,void 0,(function*(){var e,t;if(null!==l.selectedDevice)return s.AnotherDeviceIsAlreadyConnected;let i=null;try{if(this.logInfo(`Requesting Bluetooth Device, filter: ${JSON.stringify(this.filter)}`),i=yield navigator.bluetooth.requestDevice({filters:this.filter}),!i)return this.logWarn("No device selected"),s.NoDeviceHasBeenSelected}catch(e){return this.logError("Error requesting device:",e),s.NoDeviceHasBeenSelected}try{if(this.logInfo(`Selected device: ${i.name} with ID: ${i.id}`),this.logWarn(`Connecting to GATT Server of device: ${i.name}`),l.selectedDevice=i,l.globalGattDisconnectedHandlerInstalled||(i.addEventListener("gattserverdisconnected",l.globalGattDisconnectedHandler),l.globalGattDisconnectedHandlerInstalled=!0),l.gattServer=yield null===(e=i.gatt)||void 0===e?void 0:e.connect(),!l.gattServer)return this.logError("Failed to connect to GATT Server"),s.FailedToConnectToGattServer}catch(e){return this.logError("Error connecting to GATT Server:",e),s.FailedToConnectToGattServer}try{if(this.logWarn(`Getting primary service: ${this.serviceUuid}`),l.gattService=yield l.gattServer.getPrimaryService(this.serviceUuid),!l.gattService)return this.logError("Failed to get primary service"),s.FailedToGetPrimaryService}catch(e){return this.logError("Error getting primary service:",e),s.FailedToGetPrimaryService}const r=yield this.AttachToCharacteristicInternal(this.characteristicUuid);return"number"==typeof r?r:(this.logWarn("Connected to device successfully!"),void 0!==(null===(t=l.gattServer)||void 0===t?void 0:t.mtu)&&(this.updateMaxPayload(l.gattServer.mtu),this.logWarn(`MTU: ${l.gattServer.mtu}`)),this.OnConnectedEvent.Invoke(i),i)}))}ConnectToGattCharacteristicAsync(e){return c(this,void 0,void 0,(function*(){var t;if(this.characteristic)return this.logWarn("Already connected to a characteristic in this instance. Use a new instance."),s.AnotherDeviceIsAlreadyConnected;if(!l.selectedDevice)return this.logWarn("No device selected/connected"),s.NoDeviceHasBeenSelected;if(!l.gattServer)return this.logError("No GATT server"),s.FailedToConnectToGattServer;if(!l.gattService)return this.logError("No primary service"),s.FailedToGetPrimaryService;const i=yield this.AttachToCharacteristicInternal(e);return"number"==typeof i||(this.logWarn("Connected to characteristic successfully!"),void 0!==(null===(t=l.gattServer)||void 0===t?void 0:t.mtu)&&(this.updateMaxPayload(l.gattServer.mtu),this.logWarn(`MTU: ${l.gattServer.mtu}`))),i}))}AttachToCharacteristicInternal(e){return c(this,void 0,void 0,(function*(){return this.characteristicUuid=e,this.runExclusiveGattOperation("attach-characteristic",(()=>c(this,void 0,void 0,(function*(){try{if(this.logWarn(`Getting characteristic: ${this.characteristicUuid}`),this.characteristic=yield l.gattService.getCharacteristic(this.characteristicUuid),!this.characteristic)return this.logError("Failed to get characteristic"),s.FailedToGetCharacteristic}catch(e){return this.logError("Error getting characteristic:",e),s.FailedToGetCharacteristic}try{this.logWarn("Starting notifications");const e=yield this.characteristic.startNotifications();if(!e)return this.logError("Failed to start notifications"),s.FailedToStartNotifications;e.addEventListener("characteristicvaluechanged",this.handleCharacteristicValueChanged),this.notificationsStarted=!0}catch(e){return this.logError("Error starting notifications:",e),s.FailedToStartNotifications}return this.characteristic}))))}))}DisconnectDeviceAsync(e){return c(this,void 0,void 0,(function*(){var t,i;try{if(!e)return o.NoDeviceConnected;yield Promise.all([...l.instances].map((e=>e.detachCharacteristic())));try{null===(t=l.selectedDevice)||void 0===t||t.removeEventListener("gattserverdisconnected",l.globalGattDisconnectedHandler)}catch(e){}l.globalGattDisconnectedHandlerInstalled=!1;try{null===(i=e.gatt)||void 0===i||i.disconnect(),yield n.Delay(200)}catch(e){}try{yield e.forget()}catch(e){}return this.rejectPendingResponses(new Error("Device disconnected")),l.selectedDevice=null,l.gattServer=null,l.gattService=null,this.OnDisconnectedEvent.Invoke(e),o.DisconnectedSuccesfully}catch(e){return this.logError("Error disconnecting device:",e),o.FailedToDisconnect}}))}detachCharacteristic(){return c(this,void 0,void 0,(function*(){this.characteristic&&(yield this.runExclusiveGattOperation("detach-characteristic",(()=>c(this,void 0,void 0,(function*(){var e,t;try{null===(e=this.characteristic)||void 0===e||e.removeEventListener("characteristicvaluechanged",this.handleCharacteristicValueChanged)}catch(e){}try{yield null===(t=this.characteristic)||void 0===t?void 0:t.stopNotifications()}catch(e){}this.notificationsStarted=!1,this.receiveBuffer="",this.rejectPendingResponses(new Error("Characteristic detached")),this.characteristic=null})))))}))}SendDataAsync(e){return c(this,void 0,void 0,(function*(){const t=this.serializeOutgoingData(e);return t?this.runExclusiveGattOperation("send-data",(()=>c(this,void 0,void 0,(function*(){return this.sendPayloadAsync(t)})))):a.DataIsEmpty}))}SendAndWaitForResponseAsync(e){return c(this,arguments,void 0,(function*(e,t=5e3){if(!this.characteristic)throw new Error("Connection is not established");return this.runExclusiveRequestResponse("send-and-wait",(()=>c(this,void 0,void 0,(function*(){return this.runExclusiveGattOperation("send-and-wait",(()=>c(this,void 0,void 0,(function*(){const i=this.serializeOutgoingData(e);if(!i)throw new Error("The data is empty");return new Promise(((r,s)=>c(this,void 0,void 0,(function*(){const o=this.createResponseMatcher(e),l=this.tryParseJsonFrame(i),d=this.getStringField(l,"method"),h=null!=d?[...this.getExpectedResponseMethods(d)]:[],u=window.setTimeout((()=>{this.receiveBuffer="",this.pendingResponses=this.pendingResponses.filter((e=>e.onFrame!==g)),this.logWarn(`[REQ] Response timeout. requestMethod=${null!=d?d:"n/a"} expectedResponseMethods=${h.join(",")||"n/a"} timeoutMs=${t}`),s(new Error("Response timeout"))}),t),g=e=>c(this,void 0,void 0,(function*(){clearTimeout(u),this.pendingResponses=this.pendingResponses.filter((e=>e.onFrame!==g));try{yield n.Delay(30);const t=this.tryParseJsonFrame(e),i=this.getStringField(t,"method");this.logWarn(`[REQ] Matched response. requestMethod=${null!=d?d:"n/a"} matchedResponseMethod=${null!=i?i:"n/a"} expectedResponseMethods=${h.join(",")||"n/a"}`),r(JSON.parse(e))}catch(e){this.receiveBuffer="",s(new Error("Failed to parse response"))}}));switch(this.logWarn(`[REQ] Waiting for response. requestMethod=${null!=d?d:"n/a"} expectedResponseMethods=${h.join(",")||"n/a"} timeoutMs=${t}`),this.pendingResponses.push({matcher:o,onFrame:g,reject:s}),yield this.sendPayloadAsync(i)){case a.Sucess:this.logInfo("Data sent successfully");break;case a.DataIsEmpty:clearTimeout(u),this.pendingResponses=this.pendingResponses.filter((e=>e.onFrame!==g)),s(new Error("The data is empty"));break;case a.NoDeviceConnected:clearTimeout(u),this.pendingResponses=this.pendingResponses.filter((e=>e.onFrame!==g)),s(new Error("Failed to send request, device is disconnected"));break;case a.FailedToWriteValue:this.logWarn(`[REQ] Write failed but response wait remains active. requestMethod=${null!=d?d:"n/a"} expectedResponseMethods=${h.join(",")||"n/a"} timeoutMs=${t}`)}}))))}))))}))))}))}WriteValueAsync(e){return c(this,void 0,void 0,(function*(){if(!this.characteristic)return!1;const t=(new TextEncoder).encode(e);for(let i=1;i<=l.GATT_BUSY_MAX_ATTEMPTS;i++){const r=this.characteristic;if(!r)return!1;try{const s=1===i?"Writing":"Retry writing";return this.logWarn(`${s} characteristic [${r.uuid}] value: ${e}`),yield r.writeValue(t),yield n.Delay(10),!0}catch(t){if(i<l.GATT_BUSY_MAX_ATTEMPTS&&this.isGattOperationInProgressError(t)){yield n.Delay(l.GATT_BUSY_RETRY_DELAY_MS*i);continue}return this.logError(`Error writing value: ${e} error:`,t),!1}}return!1}))}SplitByLength(e,t){return e.match(new RegExp(`(.|[\r\n]){1,${t}}`,"g"))}runExclusiveGattOperation(e,t){return c(this,void 0,void 0,(function*(){let i;const r=++l.gattOperationSequence,s=++l.gattQueuedOperations;this.logWarn(`[GATT#${r}] Queued ${e}. queueDepth=${s}`);const o=l.gattOperationQueue;l.gattOperationQueue=new Promise((e=>{i=e}));const a=Date.now();yield o;const c=Date.now()-a;this.logWarn(`[GATT#${r}] Starting ${e}. waitedMs=${c} queueDepth=${l.gattQueuedOperations}`),c>0&&this.logWarn(`[GATT#${r}] Waited ${c}ms before ${e}`);try{return yield t()}finally{yield n.Delay(l.GATT_OPERATION_SETTLE_DELAY_MS),l.gattQueuedOperations=Math.max(0,l.gattQueuedOperations-1),this.logWarn(`[GATT#${r}] Finished ${e}. settleDelayMs=${l.GATT_OPERATION_SETTLE_DELAY_MS} queueDepth=${l.gattQueuedOperations}`),i()}}))}runExclusiveRequestResponse(e,t){return c(this,void 0,void 0,(function*(){let i;const r=++l.requestResponseSequence,n=++l.queuedRequestResponses;this.logWarn(`[REQ#${r}] Queued ${e}. queueDepth=${n}`);const s=l.requestResponseQueue;l.requestResponseQueue=new Promise((e=>{i=e}));const o=Date.now();yield s;const a=Date.now()-o;this.logWarn(`[REQ#${r}] Starting ${e}. waitedMs=${a} queueDepth=${l.queuedRequestResponses}`);try{return yield t()}finally{l.queuedRequestResponses=Math.max(0,l.queuedRequestResponses-1),this.logWarn(`[REQ#${r}] Finished ${e}. queueDepth=${l.queuedRequestResponses}`),i()}}))}sendPayloadAsync(e){return c(this,void 0,void 0,(function*(){try{const t=this.SplitByLength(e+this.sendSeparator,this._maxCharacteristicValueLength);if(!t)return a.FailedToWriteValue;if(!this.characteristic)return a.NoDeviceConnected;for(let e=0;e<t.length;e++)if(!(yield this.WriteValueAsync(t[e])))return a.FailedToWriteValue;return a.Sucess}catch(e){return this.logError("Error sending data:",e),a.FailedToWriteValue}}))}serializeOutgoingData(e){return"string"==typeof e?e:null==e?"":JSON.stringify(e)}normalizeFrame(e){return e.endsWith("\r")?e.slice(0,-1):e}dispatchPendingResponse(e){for(let t=0;t<this.pendingResponses.length;t++){const i=this.pendingResponses[t];if(i.matcher(e))return this.pendingResponses.splice(t,1),i.onFrame(e),!0}return!1}rejectPendingResponses(e){const t=this.pendingResponses.splice(0,this.pendingResponses.length);this.receiveBuffer="",t.forEach((t=>t.reject(e))),this.logWarn(`Rejecting pending responses: ${e.message}`)}createResponseMatcher(e){const t=this.tryParseJsonFrame("string"==typeof e?e:this.serializeOutgoingData(e)),i=this.getCorrelationId(t),r=this.getStringField(t,"method"),n=this.getStringField(t,"Resource"),s=null!=r?this.getExpectedResponseMethods(r):null,o=null!=s?[...s].join(","):"n/a";return e=>{const a=this.tryParseJsonFrame(e);if(null==a)return null==t;const c=this.getCorrelationId(a);if(null!=i||null!=c)return null!=i&&i===c;const l=this.getStringField(a,"method");if(null!=s||null!=l){const e=null!=s&&null!=l&&s.has(l);return e&&this.logWarn(`[REQ] Method matcher accepted frame. requestMethod=${null!=r?r:"n/a"} responseMethod=${null!=l?l:"n/a"} expectedResponseMethods=${o}`),e}const d=this.getStringField(a,"Resource");return null!=n||null!=d?null!=n&&n===d:null==t||null==n}}getExpectedResponseMethods(e){const t=new Set([e]),i=l.METHOD_RESPONSE_MAP[e];return"string"==typeof i?t.add(i):Array.isArray(i)&&i.forEach((e=>t.add(e))),e.startsWith("Get")&&e.length>3&&t.add(e.slice(3)),e.endsWith("Response")||t.add(`${e}Response`),t}tryParseJsonFrame(e){try{const t=JSON.parse(e);if(null!=t&&"object"==typeof t&&!Array.isArray(t))return t}catch(e){return null}return null}getCorrelationId(e){var t,i,r;return null==e?null:null!==(r=null!==(i=null!==(t=this.getStringField(e,"TransactionId"))&&void 0!==t?t:this.getStringField(e,"transactionId"))&&void 0!==i?i:this.getStringField(e,"RequestId"))&&void 0!==r?r:this.getStringField(e,"requestId")}getStringField(e,t){const i=null==e?void 0:e[t];return"string"==typeof i&&i.length>0?i:null}isGattOperationInProgressError(e){return e instanceof Error&&e.message.toLowerCase().includes("operation already in progress")}}l.selectedDevice=null,l.gattServer=null,l.gattService=null,l.gattOperationQueue=Promise.resolve(),l.gattOperationSequence=0,l.gattQueuedOperations=0,l.requestResponseQueue=Promise.resolve(),l.requestResponseSequence=0,l.queuedRequestResponses=0,l.GATT_OPERATION_SETTLE_DELAY_MS=100,l.GATT_BUSY_RETRY_DELAY_MS=40,l.GATT_BUSY_MAX_ATTEMPTS=4,l.METHOD_RESPONSE_MAP={GetKnownWifis:"KnownWifis",GetWiFiNetworks:"WiFiNetworks",GetSettings:"Settings",GetSessionId:"SessionId",SetWifi:"SetWifiResponse",RemoveWifi:"RemoveWifiResponse",RegisterLavva:["RegisterLavvaProgress","RegisterLavvaResponse"],UpdateByWifi:"UpdateByWifiResponse"},l.instances=new Set,l.globalGattDisconnectedHandlerInstalled=!1,l.globalGattDisconnectedHandler=e=>{var t,i;l.logWarn("Gatt Server disconnected (global handler)");for(const e of l.instances)try{e.rejectPendingResponses(new Error("GATT server disconnected")),null===(t=e.OnGattServiceDisconnectedEvent)||void 0===t||t.Invoke(null!==(i=l.gattServer)&&void 0!==i?i:void 0)}catch(e){l.logWarn("Error invoking OnGattServiceDisconnectedEvent:",e)}},function(e){e[e.NoDeviceHasBeenSelected=0]="NoDeviceHasBeenSelected",e[e.AnotherDeviceIsAlreadyConnected=1]="AnotherDeviceIsAlreadyConnected",e[e.FailedToConnectToGattServer=2]="FailedToConnectToGattServer",e[e.FailedToGetPrimaryService=3]="FailedToGetPrimaryService",e[e.FailedToDiscoverService=4]="FailedToDiscoverService",e[e.FailedToGetCharacteristic=5]="FailedToGetCharacteristic",e[e.FailedToStartNotifications=6]="FailedToStartNotifications"}(s||(s={})),function(e){e[e.NoDeviceConnected=0]="NoDeviceConnected",e[e.FailedToForgetDevice=1]="FailedToForgetDevice",e[e.FailedToDisconnect=2]="FailedToDisconnect",e[e.FailedToStopNotifications=3]="FailedToStopNotifications",e[e.DisconnectedSuccesfully=4]="DisconnectedSuccesfully"}(o||(o={})),function(e){e[e.Sucess=0]="Sucess",e[e.DataIsEmpty=1]="DataIsEmpty",e[e.NoDeviceConnected=2]="NoDeviceConnected",e[e.FailedToWriteValue=3]="FailedToWriteValue"}(a||(a={}));class d{constructor(e){this.handlers=[],this.name=e}Subscribe(e){this.handlers.push(e)}Unsubscribe(e){this.handlers=this.handlers.filter((t=>t!==e))}RemoveAllSubscriptions(){this.handlers=[]}Invoke(e){this.handlers.slice(0).forEach((t=>{try{null!=t&&t(e)}catch(e){console.error(`[NativeTypedEvent] Error invoking event ${this.name} handler: ${JSON.stringify(t)} error: ${e}`)}}))}}})();
//# sourceMappingURL=lavva-webbluetooth.js.map