import { WebPlugin } from '@capacitor/core' ; import type { BleCharacteristic , BleCharacteristicProperties , BleDescriptor , BleService , ConnectOptions } from '.' ; import { hexStringToDataView , mapToObject , webUUIDToString } from './conversion' ; import type { BleDevice , BleServices , BluetoothLePlugin , BooleanResult , DeviceIdOptions , GetConnectedDevicesOptions , GetDevicesOptions , GetDevicesResult , ReadDescriptorOptions , ReadOptions , ReadResult , ReadRssiResult , RequestBleDeviceOptions , ScanResultInternal , WriteOptions , WriteDescriptorOptions , } from './definitions' ; import { runWithTimeout } from './timeout' ; export class BluetoothLeWeb extends WebPlugin implements BluetoothLePlugin { private deviceMap = new Map < string , BluetoothDevice > ( ) ; private discoveredDevices = new Map < string , boolean > ( ) ; private scan : BluetoothLEScan | null = null ; private requestBleDeviceOptions : RequestBleDeviceOptions | undefined ; private DEFAULT_CONNECTION_TIMEOUT = 10000 ; async initialize ( ) { if ( typeof navigator === 'undefined' || ! navigator . bluetooth ) { throw this . unavailable ( 'Web Bluetooth API not available in this browser.' ) ; } const isAvailable = await navigator . bluetooth . getAvailability ( ) ; if ( ! isAvailable ) { throw this . unavailable ( 'No Bluetooth radio available.' ) ; } } async isEnabled ( ) { return { value : true } ; } async enable ( ) { throw this . unavailable ( 'enable is not available on web.' ) ; } async disable ( ) { throw this . unavailable ( 'disable is not available on web.' ) ; } async startEnabledNotifications ( ) { } async stopEnabledNotifications ( ) { } async isLocationEnabled ( ) { throw this . unavailable ( 'isLocationEnabled is not available on web.' ) ; } async openLocationSettings ( ) { throw this . unavailable ( 'openLocationSettings is not available on web.' ) ; } async openBluetoothSettings ( ) { throw this . unavailable ( 'openBluetoothSettings is not available on web.' ) ; } async openAppSettings ( ) { throw this . unavailable ( 'openAppSettings is not available on web.' ) ; } async setDisplayStrings ( ) { } async requestDevice ( options ? ) { const filters = this . getFilters ( options ) ; const device = await navigator . bluetooth . requestDevice ( { filters : filters . length ? filters : undefined , optionalServices : options ?. optionalServices , acceptAllDevices : filters . length === 0 , } ) ; this . deviceMap . set ( device . id , device ) ; const bleDevice = this . getBleDevice ( device ) ; return bleDevice ; } async requestLEScan ( options ? ) { this . requestBleDeviceOptions = options ; const filters = this . getFilters ( options ) ; await this . stopLEScan ( ) ; this . discoveredDevices = new Map < string , boolean > ( ) ; navigator . bluetooth . removeEventListener ( 'advertisementreceived' , this . onAdvertisementReceived as EventListener ) ; navigator . bluetooth . addEventListener ( 'advertisementreceived' , this . onAdvertisementReceived ) ; this . scan = await navigator . bluetooth . requestLEScan ( { filters : filters . length ? filters : undefined , acceptAllAdvertisements : filters . length === 0 , keepRepeatedDevices : options ?. allowDuplicates , } ) ; } private onAdvertisementReceived ( event ) { const deviceId = event . device . id ; BluetoothLe . deviceMap . set ( deviceId , event . device ) ; const isNew = ! BluetoothLe . discoveredDevices . has ( deviceId ) ; if ( isNew || BluetoothLe . requestBleDeviceOptions ?. allowDuplicates ) { BluetoothLe . discoveredDevices . set ( deviceId , true ) ; const device = BluetoothLe . getBleDevice ( event . device ) ; const result = { device , localName : device . name , rssi : event . rssi , txPower : event . txPower , manufacturerData : mapToObject ( event . manufacturerData ) , serviceData : mapToObject ( event . serviceData ) , uuids : event . uuids ?. map ( webUUIDToString ) , } ; BluetoothLe . notifyListeners ( 'onScanResult' , result ) ; } } async stopLEScan ( ) { if ( this . scan ?. active ) { this . scan . stop ( ) ; } this . scan = null ; } async getDevices ( _options ) { const devices = await navigator . bluetooth . getDevices ( ) ; const bleDevices = devices . map ( ( device ) => { this . deviceMap . set ( device . id , device ) ; const bleDevice = this . getBleDevice ( device ) ; return bleDevice ; } ) ; return { devices : bleDevices } ; } async getConnectedDevices ( _options ) { const devices = await navigator . bluetooth . getDevices ( ) ; const bleDevices = devices . filter ( ( device ) => { return device . gatt ?. connected ; } ) . map ( ( device ) => { this . deviceMap . set ( device . id , device ) ; const bleDevice = this . getBleDevice ( device ) ; return bleDevice ; } ) ; return { devices : bleDevices } ; } async connect ( options ) { const device = this . getDeviceFromMap ( options . deviceId ) ; device . removeEventListener ( 'gattserverdisconnected' , this . onDisconnected ) ; device . addEventListener ( 'gattserverdisconnected' , this . onDisconnected ) ; const timeoutError = Symbol ( ) ; if ( device . gatt === undefined ) { throw new Error ( 'No gatt server available.' ) ; } try { const timeout = options . timeout ?? this . DEFAULT_CONNECTION_TIMEOUT ; await runWithTimeout ( device . gatt . connect ( ) , timeout , timeoutError ) ; } catch ( error ) { await device . gatt ?. disconnect ( ) ; if ( error === timeoutError ) { throw new Error ( 'Connection timeout' ) ; } else { throw error ; } } } private onDisconnected ( event ) { const deviceId = ( event . target as BluetoothDevice ) . id ; const key = `disconnected|${ deviceId }` ; BluetoothLe . notifyListeners ( key , null ) ; } async createBond ( _options ) { throw this . unavailable ( 'createBond is not available on web.' ) ; } async isBonded ( _options ) { throw this . unavailable ( 'isBonded is not available on web.' ) ; } async disconnect ( options ) { this . getDeviceFromMap ( options . deviceId ) . gatt ?. disconnect ( ) ; } async getServices ( options ) { const services = ( await this . getDeviceFromMap ( options . deviceId ) . gatt ?. getPrimaryServices ( ) ) ?? [ ] ; const bleServices = [ ] ; for ( const service of services ) { const characteristics = await service . getCharacteristics ( ) ; const bleCharacteristics = [ ] ; for ( const characteristic of characteristics ) { bleCharacteristics . push ( { uuid : characteristic . uuid , properties : this . getProperties ( characteristic ) , descriptors : await this . getDescriptors ( characteristic ) , } ) ; } bleServices . push ( { uuid : service . uuid , characteristics : bleCharacteristics } ) ; } return { services : bleServices } ; } private async getDescriptors ( characteristic ) { try { const descriptors = await characteristic . getDescriptors ( ) ; return descriptors . map ( ( descriptor ) => ( { uuid : descriptor . uuid , } ) ) ; } catch { return [ ] ; } } private getProperties ( characteristic ) { return { broadcast : characteristic . properties . broadcast , read : characteristic . properties . read , writeWithoutResponse : characteristic . properties . writeWithoutResponse , write : characteristic . properties . write , notify : characteristic . properties . notify , indicate : characteristic . properties . indicate , authenticatedSignedWrites : characteristic . properties . authenticatedSignedWrites , reliableWrite : characteristic . properties . reliableWrite , writableAuxiliaries : characteristic . properties . writableAuxiliaries , } ; } private async getCharacteristic ( options ) { const service = await this . getDeviceFromMap ( options . deviceId ) . gatt ?. getPrimaryService ( options ?. service ) ; return service ?. getCharacteristic ( options ?. characteristic ) ; } private async getDescriptor ( options ) { const characteristic = await this . getCharacteristic ( options ) ; return characteristic ?. getDescriptor ( options ?. descriptor ) ; } async readRssi ( _options ) { throw this . unavailable ( 'readRssi is not available on web.' ) ; } async read ( options ) { const characteristic = await this . getCharacteristic ( options ) ; const value = await characteristic ?. readValue ( ) ; return { value } ; } async write ( options ) { const characteristic = await this . getCharacteristic ( options ) ; let dataView ; if ( typeof options . value === 'string' ) { dataView = hexStringToDataView ( options . value ) ; } else { dataView = options . value ; } await characteristic ?. writeValueWithResponse ( dataView ) ; } async writeWithoutResponse ( options ) { const characteristic = await this . getCharacteristic ( options ) ; let dataView ; if ( typeof options . value === 'string' ) { dataView = hexStringToDataView ( options . value ) ; } else { dataView = options . value ; } await characteristic ?. writeValueWithoutResponse ( dataView ) ; } async readDescriptor ( options ) { const descriptor = await this . getDescriptor ( options ) ; const value = await descriptor ?. readValue ( ) ; return { value } ; } async writeDescriptor ( options ) { const descriptor = await this . getDescriptor ( options ) ; let dataView ; if ( typeof options . value === 'string' ) { dataView = hexStringToDataView ( options . value ) ; } else { dataView = options . value ; } await descriptor ?. writeValue ( dataView ) ; } async startNotifications ( options ) { const characteristic = await this . getCharacteristic ( options ) ; characteristic ?. removeEventListener ( 'characteristicvaluechanged' , this . onCharacteristicValueChanged ) ; characteristic ?. addEventListener ( 'characteristicvaluechanged' , this . onCharacteristicValueChanged ) ; await characteristic ?. startNotifications ( ) ; } private onCharacteristicValueChanged ( event ) { const characteristic = event . target as BluetoothRemoteGATTCharacteristic ; const key = `notification|${ characteristic . service ?. device . id }|${ characteristic . service ?. uuid }|${ characteristic . uuid }` ; BluetoothLe . notifyListeners ( key , { value : characteristic . value , } ) ; } async stopNotifications ( options ) { const characteristic = await this . getCharacteristic ( options ) ; await characteristic ?. stopNotifications ( ) ; } private getFilters ( options ? ) { const filters = [ ] ; for ( const service of options ?. services ?? [ ] ) { filters . push ( { services : [ service ] , name : options ?. name , namePrefix : options ?. namePrefix , } ) ; } if ( ( options ?. name || options ?. namePrefix ) && filters . length === 0 ) { filters . push ( { name : options . name , namePrefix : options . namePrefix , } ) ; } return filters ; } private getDeviceFromMap ( deviceId ) { const device = this . deviceMap . get ( deviceId ) ; if ( device === undefined ) { throw new Error ( 'Device not found. Call "requestDevice", "requestLEScan" or "getDevices" first.' ) ; } return device ; } private getBleDevice ( device ) { const bleDevice = { deviceId : device . id , name : device . name ?? undefined , uuids : device . uuids , } ; return bleDevice ; } } export const BluetoothLe = new BluetoothLeWeb ( ) ;