import { EventEmitter , Injectable } from '@angular/core' ; import { from , fromEvent , Observable , throwError } from 'rxjs' ; import { filter , map , mergeMap , takeUntil } from 'rxjs/operators' ; import { ConsoleLoggerService } from './logger.service' ; import { BrowserWebBluetooth } from './platform/browser' ; type ReadValueOptions = { acceptAllDevices ? : boolean ; optionalServices ? : BluetoothServiceUUID [ ] ; characteristic : BluetoothCharacteristicUUID ; service : BluetoothServiceUUID ; } ; @ Injectable ( { providedIn : 'root' } ) export class BluetoothCore { private device$ : EventEmitter < BluetoothDevice > ; private gatt$ : EventEmitter < BluetoothRemoteGATTServer > ; private characteristicValueChanges$ : EventEmitter < DataView > ; private gattServer : BluetoothRemoteGATTServer ; constructor ( private readonly webBle , private readonly console ) { this . device$ = new EventEmitter < BluetoothDevice > ( ) ; this . gatt$ = new EventEmitter < BluetoothRemoteGATTServer > ( ) ; this . characteristicValueChanges$ = new EventEmitter < DataView > ( ) ; this . gattServer = null ; } getDevice$ ( ) { return this . device$ ; } getGATT$ ( ) { return this . gatt$ ; } streamValues$ ( ) { return this . characteristicValueChanges$ . pipe ( filter ( value => value && value . byteLength > 0 ) ) ; } param options async value ( options ) { this . console . log ( '[BLE::Info] Reading value with options %o' , options ) ; if ( typeof options . acceptAllDevices === 'undefined' ) { options . acceptAllDevices = true ; } if ( typeof options . optionalServices === 'undefined' ) { options . optionalServices = [ options . service ] ; } else { options . optionalServices = [ ... options . optionalServices ] ; } this . console . log ( '[BLE::Info] Reading value with options %o' , options ) ; try { const device = await this . discover ( { acceptAllDevices : options . acceptAllDevices , optionalServices : options . optionalServices } ) as BluetoothDevice ; this . console . log ( '[BLE::Info] Device info %o' , device ) ; const gatt = await this . connectDevice ( device ) ; this . console . log ( '[BLE::Info] GATT info %o' , gatt ) ; const primaryService = await this . getPrimaryService ( gatt , options . service ) as BluetoothRemoteGATTService ; this . console . log ( '[BLE::Info] Primary Service info %o' , primaryService ) ; const characteristic = await this . getCharacteristic ( primaryService , options . characteristic ) as BluetoothRemoteGATTCharacteristic ; this . console . log ( '[BLE::Info] Characteristic info %o' , characteristic ) ; const value = await characteristic . readValue ( ) ; this . console . log ( '[BLE::Info] Value info %o' , value ) ; return value ; } catch ( error ) { throw new Error ( error ) ; } } value$ ( options ) { return from ( this . value ( options ) ) ; } param Options return async discover ( options = { } as RequestDeviceOptions ) { options . optionalServices = options . optionalServices || [ 'generic_access' ] ; this . console . log ( '[BLE::Info] Requesting devices with options %o' , options ) ; let device = null ; try { device = await this . webBle . requestDevice ( options ) ; device . addEventListener ( 'gattserverdisconnected' , this . onDeviceDisconnected . bind ( this ) ) ; if ( device ) { this . device$ . emit ( device ) ; } else { this . device$ . error ( `[BLE::Error] Can not get the Bluetooth Remote GATT Server. Abort.` ) ; } } catch ( error ) { this . console . error ( error ) ; } return device ; } param event onDeviceDisconnected ( event ) { const disconnectedDevice = event . target as BluetoothDevice ; this . console . log ( '[BLE::Info] disconnected device %o' , disconnectedDevice ) ; this . device$ . emit ( null ) ; } param Options return discover$ ( options ? ) { return from ( this . discover ( options ) ) . pipe ( mergeMap ( ( device ) => this . connectDevice$ ( device ) ) ) ; } return async connectDevice ( device ) { if ( device ) { this . console . log ( '[BLE::Info] Connecting to Bluetooth Remote GATT Server of %o' , device ) ; try { const gattServer = await device . gatt . connect ( ) ; this . gattServer = gattServer ; this . gatt$ . emit ( gattServer ) ; return gattServer ; } catch ( error ) { Promise . reject ( `${ error . message }` ) ; this . gatt$ . error ( `${ error . message }` ) ; } } else { this . console . error ( '[BLE::Error] Was not able to connect to Bluetooth Remote GATT Server' ) ; this . gatt$ . error ( null ) ; } } return connectDevice$ ( device ) { return from ( this . connectDevice ( device ) ) ; } /**
     * Disconnect the current connected device
     */ disconnectDevice ( ) { if ( ! this . gattServer ) { return ; } this . console . log ( '[BLE::Info] Disconnecting from Bluetooth Device %o' , this . gattServer ) ; if ( this . gattServer . connected ) { this . gattServer . disconnect ( ) ; } else { this . console . log ( '[BLE::Info] Bluetooth device is already disconnected' ) ; } } param gatt param service return async getPrimaryService ( gatt , service ) { try { const remoteService = await gatt . getPrimaryService ( service ) ; return await Promise . resolve ( remoteService ) ; } catch ( error ) { return await Promise . reject ( `${ error . message } (${ service })` ) ; } } param gatt param service return getPrimaryService$ ( gatt , service ) { this . console . log ( '[BLE::Info] Getting primary service "%s" (if available) of %o' , service , gatt ) ; if ( gatt ) { return from ( this . getPrimaryService ( gatt , service ) ) ; } else { return throwError ( new Error ( '[BLE::Error] Was not able to connect to the Bluetooth Remote GATT Server' ) ) ; } } param primaryService param characteristic returns async getCharacteristic ( primaryService , characteristic ) { this . console . log ( '[BLE::Info] Getting Characteristic "%s" of %o' , characteristic , primaryService ) ; try { const char = await primaryService . getCharacteristic ( characteristic ) ; if ( char . properties . notify ) { char . startNotifications ( ) . then ( _ => { this . console . log ( '[BLE::Info] Starting notifications of "%s"' , characteristic ) ; char . addEventListener ( 'characteristicvaluechanged' , this . onCharacteristicChanged . bind ( this ) ) ; } , ( error ) => { Promise . reject ( `${ error . message } (${ characteristic })` ) ; } ) ; } else { char . addEventListener ( 'characteristicvaluechanged' , this . onCharacteristicChanged . bind ( this ) ) ; } return char ; } catch ( rejectionError ) { Promise . reject ( `${ rejectionError . message } (${ characteristic })` ) ; } } param primaryService param characteristic returns getCharacteristic$ ( primaryService , characteristic ) { this . console . log ( '[BLE::Info] Getting Characteristic "%s" of %o' , characteristic , primaryService ) ; return from ( this . getCharacteristic ( primaryService , characteristic ) ) ; } param service param characteristic param state return setCharacteristicState ( service , characteristic , state ) { const primaryService = this . getPrimaryService$ ( this . gattServer , service ) ; primaryService . pipe ( mergeMap ( ( _primaryService ) => this . getCharacteristic$ ( _primaryService , characteristic ) ) ) . subscribe ( ( characteristic ) => this . writeValue$ ( characteristic , state ) ) ; return primaryService ; } param service param characteristic return enableCharacteristic ( service , characteristic , state ? ) { state = state || new Uint8Array ( [ 1 ] ) ; return this . setCharacteristicState ( service , characteristic , state ) ; } param service param characteristic return disbaleCharacteristic ( service , characteristic , state ? ) { state = state || new Uint8Array ( [ 0 ] ) ; return this . setCharacteristicState ( service , characteristic , state ) ; } param event onCharacteristicChanged ( event ) { this . console . log ( '[BLE::Info] Dispatching new characteristic value %o' , event ) ; const value = ( event . target as BluetoothRemoteGATTCharacteristic ) . value ; this . characteristicValueChanges$ . emit ( value ) ; } param characteristic return readValue$ ( characteristic ) { this . console . log ( '[BLE::Info] Reading Characteristic %o' , characteristic ) ; return from ( characteristic . readValue ( ) . then ( ( data ) => Promise . resolve ( data ) , ( error ) => Promise . reject ( `${ error . message }` ) ) ) ; } param characteristic param value return writeValue$ ( characteristic , value ) { this . console . log ( '[BLE::Info] Writing Characteristic %o' , characteristic ) ; return from ( characteristic . writeValue ( value ) . then ( _ => Promise . resolve ( ) , ( error ) => Promise . reject ( `${ error . message }` ) ) ) ; } param characteristic return observeValue$ ( characteristic ) { characteristic . startNotifications ( ) ; const disconnected = fromEvent ( characteristic . service . device , 'gattserverdisconnected' ) ; return fromEvent ( characteristic , 'characteristicvaluechanged' ) . pipe ( map ( ( event ) => ( event . target as BluetoothRemoteGATTCharacteristic ) . value as DataView ) , takeUntil ( disconnected ) ) ; } param data param byteOffset return littleEndianToUint16 ( data , byteOffset ) { return ( this . littleEndianToUint8 ( data , byteOffset + 1 ) << 8 ) + this . littleEndianToUint8 ( data , byteOffset ) ; } param data param byteOffset return littleEndianToUint8 ( data , byteOffset ) { return data . getUint8 ( byteOffset ) ; } return fakeNext ( fakeValue ? ) { if ( fakeValue === undefined ) { fakeValue = ( ) => { const dv = new DataView ( new ArrayBuffer ( 8 ) ) ; dv . setUint8 ( 0 , ( Math . random ( ) * 110 ) | 0 ) ; return dv ; } ; } this . characteristicValueChanges$ . emit ( fakeValue ( ) ) ; } }