UNPKG

@silver-zepp/easy-ble

Version:

Advanced BLE management tool for ZeppOS v3+ watches that features an automated profile generator, a hybrid asynchronous and sequential queue for efficient handling of all operations and more.

1 lines 26.3 kB
import*as hmBle from"@zos/ble";let DEBUG_LOG_LEVEL=1;const SHORT_DELAY=50;const BX_CORE_CODES={"-10":{message:"Missing Attribute",code:"BX_CORE_MISS_ATT"},"-9":{message:"System Error",code:"BX_CORE_SYS"},"-8":{message:"Authentication Error",code:"BX_CORE_AUTH"},"-7":{message:"Invalid Parameter",code:"BX_CORE_PARAM"},"-6":{message:"Invalid Operation",code:"BX_CORE_INVALID"},"-5":{message:"Invalid State",code:"BX_CORE_STATE"},"-4":{message:"Busy",code:"BX_CORE_BUSY"},"-3":{message:"Timeout",code:"BX_CORE_TIMEOUT"},"-2":{message:"Uninitialized",code:"BX_CORE_UNINIT"},"-1":{message:"Fail",code:"BX_CORE_FAIL"},0:{message:"Success",code:"BX_CORE_SUCCESS"}};const ERR_PREFIX="ERR: ";const WARN_PREFIX="WARN: ";const ERR={PID_NOT_FOUND:ERR_PREFIX+"The profile pointer ID is not found for this MAC address. Please use startListener before trying to write char/desc!",PROFILE_CREATION_FAILED:ERR_PREFIX+"Profile creation failed",DEVICE_NOT_CONNECTED:ERR_PREFIX+"Device not connected. Please connect before attempting this operation.",INVALID_MAC_ADDR:ERR_PREFIX+"Invalid MAC address format",DEVICE_NOT_FOUND:ERR_PREFIX+"Device not found",DEVICE_ADDR_UNDEFINED:ERR_PREFIX+"Device address is undefined",CHAR_WRITE_OPERATION_TIMEOUT:ERR_PREFIX+"Characteristic write operation timeout. Make sure to subscribe to the .on.charaWriteComplete(...) event!",CHAR_READ_OPERATION_TIMEOUT:ERR_PREFIX+"Characteristic read operation timeout. Make sure to subscribe to either the .on.charaValueArrived(...) or .on.charaReadComplete(...) event!",DESC_WRITE_OPERATION_TIMEOUT:ERR_PREFIX+"Descriptor write operation timeout. Make sure to subscribe to the .on.descWriteComplete(...) event!",DESC_READ_OPERATION_TIMEOUT:ERR_PREFIX+"Descriptor read operation timeout. Make sure to subscribe to either the .on.descValueArrived(...) or .on.descReadComplete(...) event!",NOT_A_FUNCTION:ERR_PREFIX+"You have to provide a callback function",CB_DEREGISTERED:ERR_PREFIX+"You are trying to execute a callback that was deregistered",DEPRECATED:WARN_PREFIX+"This method is deprecated and will be removed in the future."};export class BLEMaster{#devices={};#last_connected_mac=null;#connection_in_progress=false;#listener_starting=false;#prepare_starting=false;#is_scanning=false;#device_set=new Set;#get_helper;write;read;on;off;get get(){if(!this.#get_helper){this.#get_helper=new Get(this.#getDevices,()=>this.#getCurrentlyConnectedDevice())}return this.#get_helper}constructor(){const queueManager=new QueueManager;this.write=new Write(()=>this.#getCurrentlyConnectedDevice(),queueManager);this.read=new Read(()=>this.#getCurrentlyConnectedDevice(),queueManager)}startScan(response_callback,options={}){debugLog(3,"Starting scan");this.#is_scanning=true;const{throttle_interval=1e3,duration,on_duration,allow_duplicates=false}=options;let device_batch=[];let timer_started=false;const modified_callback=scan_result=>{const mac_address=ab2mac(scan_result.dev_addr);const scan_result_mod={...scan_result,uuid:scan_result.uuid.toString(16),dev_addr:mac_address,vendor_data:ab2str_stripped(scan_result.vendor_data),service_data_array:scan_result.service_data_array?scan_result.service_data_array.map(service=>({...service,service_data:ab2str_stripped(service.service_data)})):[]};if(!this.#device_set.has(mac_address)){debugLog(3,`Adding new device ${mac_address}`);this.#device_set.add(mac_address);this.#devices[mac_address]=scan_result_mod;response_callback(scan_result_mod)}else if(allow_duplicates){device_batch.push(this.#devices[mac_address]);if(!timer_started){debugLog(3,`Starting throttle timer at ${(new Date).toISOString()}`);timer_started=true;setTimeout(()=>{debugLog(3,`Processing batch at ${(new Date).toISOString()}`);device_batch.forEach(device=>response_callback(device));device_batch=[];timer_started=false},throttle_interval)}}};const success=hmBle.mstStartScan(modified_callback);if(duration!==undefined){setTimeout(()=>{this.stopScan();if(on_duration){on_duration()}},duration)}return success}stopScan(){if(this.#is_scanning){this.#is_scanning=false;return hmBle.mstStopScan()}return false}connect(dev_addr,response_callback){if(!dev_addr){debugLog(1,ERR.DEVICE_ADDR_UNDEFINED);return false}debugLog(3,`Attempting to connect to device: ${dev_addr}`);dev_addr=dev_addr.toLowerCase();if(dev_addr.includes("xx")){const matched_dev_addr=findMatchingMacAddress(this.#devices,dev_addr);if(!matched_dev_addr){debugLog(1,ERR.DEVICE_NOT_FOUND,dev_addr);response_callback({connected:false,status:"device not found"});return false}dev_addr=matched_dev_addr}if(this.get.isConnected()){response_callback({connected:true,status:"connected"});return true}if(!isValidMacAddress(dev_addr)){debugLog(1,ERR.INVALID_MAC_ADDR,dev_addr);response_callback({connected:false,status:"invalid mac"});return false}if(this.#connection_in_progress){debugLog(2,"Connection already in progress for:",dev_addr);response_callback({connected:false,status:"in progress"});return false}if(this.#devices[dev_addr]?.is_connected){debugLog(2,"Device already connected:",dev_addr);response_callback({connected:true,status:"connected"});return true}this.#initiateConnection(dev_addr,response_callback,attempt,max_attempts,timeout_duration);return true}disconnect(){if(!this.#last_connected_mac){debugLog(1,ERR.DEVICE_NOT_CONNECTED);return false}const device=this.#devices[this.#last_connected_mac];if(!device||!device.is_connected){debugLog(1,ERR.DEVICE_NOT_CONNECTED);return false}const ok=hmBle.mstDisconnect(device.connect_id);if(ok){this.#last_connected_mac=null}return ok}pair(){const dev_addr=this.#last_connected_mac;debugLog(3,"Pairing with the device:",dev_addr);const device=this.#devices[dev_addr];if(!device||!device.is_connected||!dev_addr){debugLog(ERR.DEVICE_NOT_CONNECTED);return false}return hmBle.mstPair(device.connect_id)}startListener(profile_object,response_callback){if(this.#listener_starting){debugLog(2,"Listener start already in progress");return}this.#listener_starting=true;debugLog(3,"Starting listener with profile object",JSON.stringify(profile_object));hmBle.mstOnPrepare(backend_response=>{if(this.#prepare_starting){debugLog(2,"Prepare start already in progress");return}this.#prepare_starting=true;debugLog(2,"mstOnPrepare callback triggered",JSON.stringify(backend_response));const status_info=BX_CORE_CODES[backend_response.status.toString()]||{message:"Unknown Error",code:"UNKNOWN"};const is_success=backend_response.status===0;if(is_success){debugLog(2,"mstOnPrepare succeed, proceeding with profile pointer saving. Pointer ID:",backend_response.profile);this.#devices[this.#last_connected_mac].profile_pid=backend_response.profile;this.on=new On(backend_response.profile);this.off=new Off;response_callback({success:true,message:status_info.message})}else{debugLog(1,`${status_info.message}. Status: ${status_info.code}`);response_callback({success:false,message:status_info.message,code:status_info.code})}});debugLog(3,"Setting a timeout before calling mstBuildProfile");setTimeout(()=>{const success=hmBle.mstBuildProfile(profile_object);debugLog(2,"mstBuildProfile called with success:",success);this.#listener_starting=false;this.#prepare_starting=false},SHORT_DELAY)}generateProfileObject(services,permissions={}){debugLog(3,"Generating full profile object");const dev_addr=this.#last_connected_mac;if(!dev_addr){debugLog(1,ERR.DEVICE_NOT_CONNECTED);return{success:false,error:ERR.DEVICE_NOT_CONNECTED}}const device=this.#devices[this.#last_connected_mac];if(!device){debugLog(1,ERR.DEVICE_NOT_FOUND,dev_addr);return{success:false,error:ERR.DEVICE_NOT_FOUND+": "+dev_addr}}if(!device.is_connected){debugLog(1,ERR.DEVICE_NOT_CONNECTED);return{success:false,error:ERR.DEVICE_NOT_CONNECTED}}if(typeof device.connect_id!=="number"){debugLog(1,ERR.DEVICE_NOT_CONNECTED);return{success:false,error:ERR.DEVICE_NOT_CONNECTED}}const dev=mac2ab(dev_addr);if(!(dev instanceof ArrayBuffer)||dev.byteLength!==6){debugLog(1,ERR.INVALID_MAC_ADDR,dev_addr);return{success:false,error:ERR.INVALID_MAC_ADDR+": "+dev_addr}}const serv_len=Object.keys(services).length;const profile_object={pair:true,id:device.connect_id,profile:device.dev_name||"undefined",dev:dev,len:1,list:[{uuid:true,size:serv_len,len:serv_len,list:[]}]};for(let[serv_uuid,characteristics]of Object.entries(services)){const chara_len=Object.keys(characteristics).length;const service={uuid:serv_uuid,permission:0,len1:chara_len,len2:chara_len,list:[]};for(let[chara_uuid,desc_uuids]of Object.entries(characteristics)){let chara_perm=permissions[chara_uuid]?permissions[chara_uuid].value:32;let characteristic={uuid:chara_uuid,permission:chara_perm,desc:desc_uuids.length,len:desc_uuids.length};if(desc_uuids.length>0){characteristic.list=desc_uuids.map(descriptor_uuid=>{let desc_perm=permissions[descriptor_uuid]?permissions[descriptor_uuid].value:32;return{uuid:descriptor_uuid,permission:desc_perm}})}service.list.push(characteristic)}profile_object.list[0].list.push(service)}return profile_object}quit(){debugLog(3,"Stopping the BLE Master comms.");if(!this.#last_connected_mac){debugLog(1,"No device is currently connected to stop interaction with.");return}const device=this.#devices[this.#last_connected_mac];if(device&&device.is_connected){this.#disconnectDevice(device)}if(this.off){this.off.deregisterAll()}this.#last_connected_mac=null;if(this.#is_scanning){this.stopScan()}}#disconnectDevice(device){debugLog(2,"Disconnecting device: "+device.dev_name);hmBle.mstOffAllCb();if(device.profile_pid!==undefined){hmBle.mstDestroyProfileInstance(device.profile_pid)}hmBle.mstDisconnect(device.connect_id);device.is_connected=false}#initiateConnection(dev_addr,response_callback){this.#connection_in_progress=true;const dev_addr_ab=mac2ab(dev_addr);hmBle.mstConnect(dev_addr_ab,result=>{const backend_mac=ab2mac(result.dev_addr);if(backend_mac!==dev_addr){debugLog(1,`Discrepancy in MAC addresses. Backend MAC: ${backend_mac}, Expected MAC: ${dev_addr}`)}const result_mod={...result,dev_addr:dev_addr};if(result_mod.connected===0){if(!this.#devices[dev_addr]){this.#devices[dev_addr]={dev_name:"default",connect_id:result.connect_id,is_connected:true}}this.#handleSuccessfulConnection(result_mod);response_callback({connected:true,status:"connected"})}else{response_callback({connected:false,status:result_mod.connected===1?"failed":"disconnected"})}this.#connection_in_progress=false})}#handleSuccessfulConnection(result_mod){debugLog(2,`Successful connection to device: ${result_mod.dev_addr}`);this.#devices[result_mod.dev_addr]={...this.#devices[result_mod.dev_addr],connect_id:result_mod.connect_id,is_connected:true};this.#last_connected_mac=result_mod.dev_addr}#getDevices=()=>this.#devices;#getCurrentlyConnectedDevice(){return this.#last_connected_mac?this.#devices[this.#last_connected_mac]:null}static SetDebugLevel(debug_level){DEBUG_LOG_LEVEL=debug_level}}class QueueManager{constructor(){this.operation_queue_arr=[];this.is_processing=false}enqueueOperation(operation){this.operation_queue_arr.push(operation);this.processQueue()}processQueue(){if(this.is_processing||this.operation_queue_arr.length===0){return}this.is_processing=true;let operation=this.operation_queue_arr.shift();operation.execute(()=>{this.is_processing=false;operation=null;this.processQueue()})}static#queue_timeout=5e3;static#queue_check_interval=100;static _getQueueTimeout(){return this.#queue_timeout}static _getQueueCheckInterval(){return this.#queue_check_interval}}class Write{#getCurrentDevice;#queueManager;constructor(getCurrentDevice,queueManager){this.#getCurrentDevice=getCurrentDevice;this.#queueManager=queueManager}characteristic(uuid,data,write_without_response=false){const operation={execute:callback=>{this.#performCharaWrite(uuid,data,write_without_response,callback)}};this.#queueManager.enqueueOperation(operation)}descriptor(chara,desc,data){const operation={execute:callback=>{this.#performDescriptorWrite(chara,desc,data,callback)}};this.#queueManager.enqueueOperation(operation)}enableCharaNotifications(chara,enable){const cccd_val=enable?"0100":"0000";const uuid_cccd="2902";const operation={execute:callback=>{this.#performDescriptorWrite(chara,uuid_cccd,cccd_val,callback)}};this.#queueManager.enqueueOperation(operation)}#performCharaWrite(uuid,data,write_without_response,callback){const device=this.#getCurrentDevice();if(!device||device.profile_pid===undefined){debugLog(1,ERR.PID_NOT_FOUND);callback();return}const profile_pid=device.profile_pid;const data_ab=data2ab(data);if(write_without_response){debugLog(3,`EXEC: hmBle.mstWriteCharacteristicWithoutResponse(${profile_pid}, ${uuid}, ${ab2hex(data_ab)}, ${data_ab.byteLength})`);hmBle.mstWriteCharacteristicWithoutResponse(profile_pid,uuid,data_ab,data_ab.byteLength);callback()}else{debugLog(3,`EXEC: hmBle.mstWriteCharacteristic(${profile_pid}, ${uuid}, ${ab2hex(data_ab)}, ${data_ab.byteLength})`);hmBle.mstWriteCharacteristic(profile_pid,uuid,data_ab,data_ab.byteLength);this.#waitForWriteCompletion(callback,"char")}}#performDescriptorWrite(chara,desc,data,callback){const device=this.#getCurrentDevice();if(!device||device.profile_pid===undefined){debugLog(1,ERR.PID_NOT_FOUND);callback();return}const profile_pid=device.profile_pid;const data_ab=data2ab(data);debugLog(3,`EXEC: hmBle.mstWriteDescriptor(${profile_pid}, ${chara}, ${desc}, ${ab2hex(data_ab)}, ${data_ab.byteLength})`);hmBle.mstWriteDescriptor(profile_pid,chara,desc,data_ab,data_ab.byteLength);this.#waitForWriteCompletion(callback,"desc")}#waitForWriteCompletion(callback,operation_type){const check_interval=QueueManager._getQueueCheckInterval();const timeout=QueueManager._getQueueTimeout();let start_time=Date.now();const cb_check_completion=()=>{if(Date.now()-start_time>timeout){if(operation_type==="char"){debugLog(1,ERR.CHAR_WRITE_OPERATION_TIMEOUT);callback(ERR.CHAR_WRITE_OPERATION_TIMEOUT)}else{debugLog(1,ERR.DESC_WRITE_OPERATION_TIMEOUT);callback(ERR.DESC_WRITE_OPERATION_TIMEOUT)}return}if(On._getDescWriteCompleteFlag()||On._getCharaWriteCompleteFlag()){On._setDescWriteCompleteFlag(false);On._setCharaWriteCompleteFlag(false);callback()}else{setTimeout(cb_check_completion,check_interval)}};cb_check_completion()}}class Read{#getCurrentDevice;#queueManager;constructor(getCurrentDevice,queueManager){this.#getCurrentDevice=getCurrentDevice;this.#queueManager=queueManager}characteristic(uuid){const operation={execute:callback=>{this.#performReadOperation(uuid,false,callback)}};this.#queueManager.enqueueOperation(operation)}descriptor(chara,desc){const operation={execute:callback=>{this.#performReadOperation(`${chara}-${desc}`,true,callback)}};this.#queueManager.enqueueOperation(operation)}#performReadOperation(uuid,is_descriptor,callback){const device=this.#getCurrentDevice();if(!device||device.profile_pid===undefined){debugLog(1,ERR.PID_NOT_FOUND);callback();return}const profile_pid=device.profile_pid;if(is_descriptor){const[uuid_chara,uuid_desc]=uuid.split("-");debugLog(3,`EXEC: hmBle.mstReadDescriptor(${profile_pid}, ${uuid_chara}, ${uuid_desc})`);hmBle.mstReadDescriptor(profile_pid,uuid_chara,uuid_desc)}else{debugLog(3,`EXEC: hmBle.mstReadCharacteristic(${profile_pid}, ${uuid})`);hmBle.mstReadCharacteristic(profile_pid,uuid)}this.#waitForReadCompletion(is_descriptor,callback)}#waitForReadCompletion(is_descriptor,callback){const check_interval=QueueManager._getQueueCheckInterval();const timeout=QueueManager._getQueueTimeout();let start_time=Date.now();const cb_check_completion=()=>{if(Date.now()-start_time>timeout){if(is_descriptor){debugLog(1,ERR.DESC_READ_OPERATION_TIMEOUT);callback(ERR.DESC_READ_OPERATION_TIMEOUT)}else{debugLog(1,ERR.CHAR_READ_OPERATION_TIMEOUT);callback(ERR.CHAR_READ_OPERATION_TIMEOUT)}return}if(is_descriptor&&On._getDescReadCompleteFlag()||!is_descriptor&&On._getCharaReadCompleteFlag()){On._setDescReadCompleteFlag(false);On._setCharaReadCompleteFlag(false);callback()}else{setTimeout(cb_check_completion,check_interval)}};cb_check_completion()}}class On{#profile_pid;constructor(profile_pid){this.#profile_pid=profile_pid}charaReadComplete(callback){if(typeof callback!=="function"){debugLog(1,ERR.NOT_A_FUNCTION);return}On._cb_charaReadComplete=callback;hmBle.mstOnCharaReadComplete(response=>{const{profile,uuid,status}=response;if(this.#profile_pid===profile){On._setCharaReadCompleteFlag(true);On.#last_chara_read_status=status;if(On._cb_charaReadComplete)On._cb_charaReadComplete(uuid,status);else debugLog(1,ERR.CB_DEREGISTERED+": charaReadComplete")}})}charaValueArrived(callback){if(typeof callback!=="function"){debugLog(1,ERR.NOT_A_FUNCTION);return}On._cb_charaValueArrived=callback;hmBle.mstOnCharaValueArrived(response=>{const{profile,uuid,data,length}=response;if(this.#profile_pid===profile){On._setCharaReadCompleteFlag(true);On.#last_chara_read_status=0;if(On._cb_charaValueArrived)On._cb_charaValueArrived(uuid,data,length);else debugLog(1,ERR.CB_DEREGISTERED+": charaValueArrived")}})}charaWriteComplete(callback){if(typeof callback!=="function"){debugLog(1,ERR.NOT_A_FUNCTION);return}On._cb_charaWriteComplete=callback;hmBle.mstOnCharaWriteComplete(response=>{if(this.#profile_pid===response.profile){On._setCharaWriteCompleteFlag(true);if(On._cb_charaWriteComplete)On._cb_charaWriteComplete(response.uuid,response.status);else debugLog(1,ERR.CB_DEREGISTERED+": charaWriteComplete")}})}descReadComplete(callback){if(typeof callback!=="function"){debugLog(1,ERR.NOT_A_FUNCTION);return}On._cb_descReadComplete=callback;hmBle.mstOnDescReadComplete(response=>{const{profile,chara,desc,status}=response;if(this.#profile_pid===profile){On._setDescReadCompleteFlag(true);On.#last_desc_read_status=status;if(On._cb_descReadComplete)On._cb_descReadComplete(chara,desc,status);else debugLog(1,ERR.CB_DEREGISTERED+": descReadComplete")}})}descValueArrived(callback){if(typeof callback!=="function"){debugLog(1,ERR.NOT_A_FUNCTION);return}On._cb_descValueArrived=callback;hmBle.mstOnDescValueArrived(response=>{const{profile,chara,desc,data,length}=response;if(this.#profile_pid===profile){On._setDescReadCompleteFlag(true);On.#last_desc_read_status=0;if(On._cb_descValueArrived)On._cb_descValueArrived(chara,desc,data,length);else debugLog(1,ERR.CB_DEREGISTERED+": descValueArrived")}})}descWriteComplete(callback){if(typeof callback!=="function"){debugLog(1,ERR.NOT_A_FUNCTION);return}On._cb_descWriteComplete=callback;hmBle.mstOnDescWriteComplete(response=>{const{profile,chara,desc,status}=response;if(this.#profile_pid===profile){On._setDescWriteCompleteFlag(true);On.#last_desc_write_status=status;if(On._cb_descWriteComplete)On._cb_descWriteComplete(chara,desc,status);else debugLog(1,ERR.CB_DEREGISTERED+": descWriteComplete")}})}charaNotification(callback){if(typeof callback!=="function"){debugLog(1,ERR.NOT_A_FUNCTION);return}On._cb_charaNotification=callback;hmBle.mstOnCharaNotification(response=>{const{profile,uuid,data,length}=response;if(this.#profile_pid===profile){if(On._cb_charaNotification)On._cb_charaNotification(uuid,data,length);else debugLog(1,ERR.CB_DEREGISTERED+": charaNotification")}})}serviceChangeBegin(callback){if(typeof callback!=="function"){debugLog(1,ERR.NOT_A_FUNCTION);return}On._cb_serviceChangeBegin=callback;hmBle.mstOnServiceChangeBegin(response=>{const{profile}=response;if(this.#profile_pid===profile){if(On._cb_serviceChangeBegin)On._cb_serviceChangeBegin();else debugLog(1,ERR.CB_DEREGISTERED+": serviceChangeBegin")}})}serviceChangeEnd(callback){if(typeof callback!=="function"){debugLog(1,ERR.NOT_A_FUNCTION);return}On._cb_serviceChangeEnd=callback;hmBle.mstOnServiceChangeEnd(response=>{const{profile}=response;if(this.#profile_pid===profile){if(On._cb_serviceChangeEnd)On._cb_serviceChangeEnd();else debugLog(1,ERR.CB_DEREGISTERED+": serviceChangeEnd")}})}static#chara_write_complete_flag=false;static#desc_write_complete_flag=false;static#last_chara_write_status=0;static#last_desc_write_status=0;static _setCharaWriteCompleteFlag(value){this.#chara_write_complete_flag=value}static _setDescWriteCompleteFlag(value){this.#desc_write_complete_flag=value}static _getCharaWriteCompleteFlag(){return this.#chara_write_complete_flag}static _getDescWriteCompleteFlag(){return this.#desc_write_complete_flag}static _getLastCharaWriteStatus(){return this.#last_chara_write_status}static _getLastDescWriteStatus(){return this.#last_desc_write_status}static#chara_read_complete_flag=false;static#last_chara_read_status=0;static#desc_read_complete_flag=false;static#last_desc_read_status=0;static _setDescReadCompleteFlag(value){this.#desc_read_complete_flag=value}static _getDescReadCompleteFlag(){return this.#desc_read_complete_flag}static _getLastDescReadStatus(){return this.#last_desc_read_status}static _setCharaReadCompleteFlag(value){this.#chara_read_complete_flag=value}static _getCharaReadCompleteFlag(){return this.#chara_read_complete_flag}static _getLastCharaReadStatus(){return this.#last_chara_read_status}static _cb_charaReadComplete=null;static _cb_charaValueArrived=null;static _cb_charaWriteComplete=null;static _cb_descReadComplete=null;static _cb_descValueArrived=null;static _cb_descWriteComplete=null;static _cb_charaNotification=null;static _cb_serviceChangeBegin=null;static _cb_serviceChangeEnd=null}class Off{charaReadComplete(){On._cb_charaReadComplete=null}charaValueArrived(){On._cb_charaValueArrived=null}charaWriteComplete(){On._cb_charaWriteComplete=null}descReadComplete(){On._cb_descReadComplete=null}descValueArrived(){On._cb_descValueArrived=null}descWriteComplete(){On._cb_descWriteComplete=null}charaNotification(){On._cb_charaNotification=null}serviceChangeBegin(){On._cb_serviceChangeBegin=null}serviceChangeEnd(){On._cb_serviceChangeEnd=null}deregisterAll(){On._cb_charaReadComplete=null;On._cb_charaValueArrived=null;On._cb_charaWriteComplete=null;On._cb_descReadComplete=null;On._cb_descValueArrived=null;On._cb_descWriteComplete=null;On._cb_charaNotification=null;On._cb_serviceChangeBegin=null;On._cb_serviceChangeEnd=null}}class Get{#getDevices;#getCurrentDevice;constructor(getDevices,getCurrentDevice){this.#getDevices=getDevices;this.#getCurrentDevice=getCurrentDevice}devices(){return this.#getDevices()}isConnected(){const device=this.#getCurrentDevice();return device&&device.is_connected}hasMAC(dev_addr){if(!dev_addr){debugLog(1,ERR.DEVICE_ADDR_UNDEFINED);return false}dev_addr=dev_addr.toLowerCase();if(dev_addr.includes("xx")){const matched_dev_addr=findMatchingMacAddress(this.#getDevices(),dev_addr);return matched_dev_addr!==null}else{return this.#getDevices().hasOwnProperty(dev_addr)}}hasDevice(dev_addr){debugLog(3,ERR.DEPRECATED,"Please use hasMAC() instead.");return this.hasMAC(dev_addr)}hasDeviceName(dev_name){const devices=this.#getDevices();return Object.values(devices).some(device=>device.dev_name&&device.dev_name.toLowerCase()===dev_name.toLowerCase())}hasService(service_uuid){const devices=this.#getDevices();return Object.values(devices).some(device=>device.service_uuid_array&&device.service_uuid_array.includes(service_uuid))}hasServiceData(service_data){const devices=this.#getDevices();return Object.values(devices).some(device=>device.service_data_array&&device.service_data_array.some(data=>data.service_data.includes(service_data)))}hasServiceDataUUID(uuid){const devices=this.#getDevices();return Object.values(devices).some(device=>device.service_data_array&&device.service_data_array.some(service_data=>service_data.uuid===uuid))}hasVendorData(vendor_data){const devices=this.#getDevices();return Object.values(devices).some(device=>device.vendor_data&&device.vendor_data.toLowerCase().includes(vendor_data.toLowerCase()))}hasVendorID(vendor_id){const devices=this.#getDevices();return Object.values(devices).some(device=>device.vendor_id===vendor_id)}profilePID(){const device=this.#getCurrentDevice();if(!device||!device.is_connected){debugLog(1,ERR.DEVICE_NOT_CONNECTED);return null}return device.profile_pid}connectionID(){const device=this.#getCurrentDevice();if(!device||!device.is_connected){debugLog(1,ERR.DEVICE_NOT_CONNECTED);return null}return device.connect_id}}function debugLog(level,...params){if(level<=DEBUG_LOG_LEVEL){console.log("[easy-ble]",...params)}}function str2ab_with_len(str){const data_arr=str.split("").map(char=>char.charCodeAt(0));return{data_ab:new Uint8Array(data_arr).buffer,data_len:data_arr.length}}function ab2mac(ab){const bytes=new Uint8Array(ab);const mac=Array.from(bytes).map(byte=>byte.toString(16).padStart(2,"0")).join(":");return mac}function mac2ab(mac){const bytes=mac.split(":").map(byte=>parseInt(byte,16));const ab=new Uint8Array(bytes).buffer;return ab}function ab2str_stripped(buffer){return Array.prototype.map.call(new Uint8Array(buffer),u=>("00"+u.toString(16)).slice(-2)).join("")}function data2ab(data){if(data instanceof ArrayBuffer){return data}else if(data instanceof Uint8Array){return data.buffer}else if(typeof data==="string"){if(/^[0-9A-Fa-f]+$/.test(data)){const bytes_arr=[];for(let i=0;i<data.length;i+=2){bytes_arr.push(parseInt(data.substring(i,i+2),16))}return new Uint8Array(bytes_arr).buffer}else{return str2ab_with_len(data).data_ab}}}export function arr2ab(arr){let buf=new Uint8Array(arr);return buf.buffer}export function ab2arr(arr_buf){let array_list=Array.prototype.slice.call(new Uint8Array(arr_buf));return array_list}function bytes2str(bytes){return String.fromCharCode(...bytes)}function isValidMacAddress(mac){const regex=/^[0-9A-Fa-f]{2}([-:])[0-9A-Fa-f]{2}(\1[0-9A-Fa-f]{2}){4}$/;return regex.test(mac)}function findMatchingMacAddress(devices,pattern){pattern=pattern.toLowerCase();for(const mac in devices){if(isMacAddressMatch(mac,pattern)){debugLog(2,"MAC pattern match found:",mac);return mac}}return null}function isMacAddressMatch(mac,pattern){const mac_bytes=mac.split(":");const pattern_bytes=pattern.split(":");if(mac_bytes.length!==pattern_bytes.length)return false;for(let i=0;i<mac_bytes.length;i++){if(pattern_bytes[i]!=="xx"&&pattern_bytes[i]!==mac_bytes[i]){return false}}return true}export function ab2hex(buffer,space=false){const hex_array=Array.prototype.map.call(new Uint8Array(buffer),byte=>{return("0"+byte.toString(16)).slice(-2)});return hex_array.join(space?" ":"").toUpperCase()}export function ab2num(buffer){const uint8_arr=new Uint8Array(buffer);let num=0;for(let i=0;i<uint8_arr.length;i++){num=num<<8|uint8_arr[i]}return num}export function ab2str(buffer){return String.fromCharCode.apply(null,new Uint8Array(buffer))}