UNPKG

@nebulae/angular-ble

Version:

A Web Bluetooth (Bluetooth Low Energy) module for angular (v2+)

1,054 lines (1,044 loc) 125 kB
import { Injectable, NgModule, InjectionToken, EventEmitter, defineInjectable, inject } from '@angular/core'; import { ModeOfOperation, Counter, utils } from 'aes-js'; import { map, mergeMap, concat, mapTo, filter, takeUntil, scan, tap, take, timeout, retryWhen, delay } from 'rxjs/operators'; import { Observable, forkJoin, Subject, defer, fromEvent, of, from } from 'rxjs'; import { CommonModule } from '@angular/common'; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class BrowserWebBluetooth { constructor() { this._ble = navigator.bluetooth; if (!this._ble) { console.log('error cargando bluetooth'); // bluetoothService.setBluetoothAvailable(false); // throw new Error('Your browser does not support Smart Bluetooth. See http://caniuse.com/#search=Bluetooth for more details.'); } } /** * @param {?} options * @return {?} */ requestDevice(options) { return this._ble.requestDevice(options); } } BrowserWebBluetooth.decorators = [ { type: Injectable }, ]; /** @nocollapse */ BrowserWebBluetooth.ctorParameters = () => []; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** @type {?} */ const GattServices = { GENERIC_ACCESS: { SERVICE: 'generic_access', DEVICE_NAME: 'device_name', APPEARANCE: 'appearance', PRIVACY_FLAG: 'privacy_flag', RECONNECTION_ADDRESS: 'reconnection_address', PERIPHERAL_PREFERRED_CONNECTION_PARAMETERS: 'peripheral_preferred_connection_parameters' }, BATTERY: { SERVICE: 'battery_service', BATTERY_LEVEL: 'battery_level' }, DEVICE_INFORMATION: { SERVICE: 'device_information', MANUFACTURER_NAME: 'manufacturer_name_string', MODEL_NUMBER: 'model_number_string', SERIAL_NUMBER: 'serial_number_string', HARDWARE_REVISION: 'hardware_revision_string', FIRMWARE_REVISION: 'firmware_revision_string', SOFTWARE_REVISION: 'software_revision_string', SYSTEM_ID: 'system_id', PNP_ID: 'pnp_id' } }; /** @type {?} */ const GATT_SERVICES = Object.keys(GattServices).map(key => GattServices[key].SERVICE); /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class CypherAesService { constructor() { this.masterKey = []; this.initialVector = []; this.encryptMethod = 'CBC'; this.isStaticInitialVector = true; this.isConfigExecuted = false; } /** * Initial config used to initalice all required params * @param {?} masterKey key used to encrypt and decrypt * @param {?=} initialVector vector used to encrypt abd decrypt except when ECB encrypt method is used * @param {?=} encryptMethod type of encrypt method is used, the possible options are: CBC, CTR, CFB, OFB, ECB * @param {?=} additionalEncryptMethodParams configuration params used by the selected encrypt method. * Note: if the method CTR or CFB is used this param is required otherwise is an optinal param. * By CTR require the param counter and by CFB require the param segmentSize * @param {?=} isStaticInitialVector defines if the initial vector is changed or not when the data are encrypted or not * @return {?} */ config(masterKey, initialVector = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], encryptMethod = 'CBC', additionalEncryptMethodParams = {}, isStaticInitialVector = true) { this.isConfigExecuted = true; this.masterKey = masterKey; this.initialVector = initialVector; this.encryptMethod = encryptMethod; this.isStaticInitialVector = isStaticInitialVector; this.additionalEncryptMethodParams = additionalEncryptMethodParams; if (!isStaticInitialVector) { this.enctrypMethodInstance = this.generateEncryptMethodInstance(); } } /** * Encrypt the data using the encrypt method previously configured * @param {?} dataArrayBuffer data to encrypt * @return {?} */ encrypt(dataArrayBuffer) { if (!this.isConfigExecuted) { throw new Error('Must configurate cypher-aes before call this method, use the method config()'); } if (this.encryptMethod === 'CBC' || this.encryptMethod === 'ECB') { dataArrayBuffer = this.addPadding(dataArrayBuffer); } return this.isStaticInitialVector ? this.generateEncryptMethodInstance().encrypt(dataArrayBuffer) : this.enctrypMethodInstance.encrypt(dataArrayBuffer); } /** * Decrypt the data using the encrypt method previously configured * @param {?} dataArrayBuffer data to decrypt * @return {?} */ decrypt(dataArrayBuffer) { if (!this.isConfigExecuted) { throw new Error('Must configurate cypher-aes before call this method, use the method config()'); } return this.isStaticInitialVector ? this.generateEncryptMethodInstance().decrypt(dataArrayBuffer) : this.enctrypMethodInstance.decrypt(dataArrayBuffer); } /** * Change the current initalVector * @param {?} initialVector new initalVector * @return {?} */ changeInitialVector(initialVector) { if (!this.isStaticInitialVector) { this.enctrypMethodInstance = this.generateEncryptMethodInstance(); } this.initialVector = initialVector; } /** * Change the current encyptMethod * @param {?} encryptMethod new encryptMethod * @return {?} */ changeEncryptMethod(encryptMethod) { if (!this.isStaticInitialVector) { this.enctrypMethodInstance = this.generateEncryptMethodInstance(); } this.encryptMethod = encryptMethod; } /** * Change the current isStaticInitialVector * @param {?} isStaticInitialVector new isStaticInitalVector * @return {?} */ changeStaticInitialVector(isStaticInitialVector) { if (!isStaticInitialVector) { this.enctrypMethodInstance = this.generateEncryptMethodInstance(); } this.isStaticInitialVector = isStaticInitialVector; } /** * Change the current masterKey * @param {?} masterKey new masterKey * @return {?} */ changeMasterKey(masterKey) { if (!this.isStaticInitialVector) { this.enctrypMethodInstance = this.generateEncryptMethodInstance(); } this.masterKey = masterKey; } /** * Add padding to the list * @param {?} arrayBuffer * @return {?} */ addPadding(arrayBuffer) { /** @type {?} */ const paddingLength = Math.ceil(Array.from(arrayBuffer).length / 16) * 16; /** @type {?} */ const paddingList = new Array(paddingLength - Array.from(arrayBuffer).length).fill(0); return new Uint8Array(Array.from(arrayBuffer).concat(paddingList)); } /** * generate a instance of the encrypt method using the current method configured * @return {?} */ generateEncryptMethodInstance() { /** @type {?} */ let enctrypMethodInstance; switch (this.encryptMethod) { case 'CBC': enctrypMethodInstance = new ModeOfOperation.cbc(this.masterKey, this.initialVector); break; case 'CTR': if (!this.additionalEncryptMethodParams.counter) { throw new Error('additionalEncryptMethodParams.counter is required to use encrypt method CTR'); } enctrypMethodInstance = new ModeOfOperation.ctr(this.masterKey, this.initialVector, new Counter(this.additionalEncryptMethodParams.counter)); break; case 'CFB': if (!this.additionalEncryptMethodParams.segmentSize) { throw new Error('additionalEncryptMethodParams.segmentSize is required to use encrypt method CFB'); } enctrypMethodInstance = new ModeOfOperation.cfb(this.masterKey, this.initialVector, this.additionalEncryptMethodParams.segmentSize); break; case 'OFB': enctrypMethodInstance = new ModeOfOperation.ofb(this.masterKey, this.initialVector); break; case 'ECB': enctrypMethodInstance = new ModeOfOperation.ecb(this.masterKey); break; } return enctrypMethodInstance; } /** * Convert the text to bytes * @param {?} text Text to convert * @return {?} */ textToBytes(text) { return utils.utf8.toBytes(text); } /** * Convert the bytes to text * @param {?} bytes Bytes to convert * @return {?} */ bytesToText(bytes) { return utils.utf8.fromBytes(bytes); } /** * Convert the bytes to hex * @param {?} bytes bytes to convert * @return {?} */ bytesTohex(bytes) { return utils.hex.fromBytes(bytes); } /** * Convert the hex to bytes * @param {?} hex Hex to convert * @return {?} */ hexToBytes(hex) { return utils.hex.toBytes(hex); } /** * @param {?} key * @return {?} */ generateSubkeys(key) { /** @type {?} */ const const_Zero = new Uint8Array(16); /** @type {?} */ const const_Rb = new Buffer('00000000000000000000000000000087', 'hex'); /** @type {?} */ const enctrypMethodInstance = new ModeOfOperation.cbc(key, new Uint8Array(16)); /** @type {?} */ const lEncrypted = enctrypMethodInstance.encrypt(const_Zero); /** @type {?} */ const l = new Buffer(this.bytesTohex(lEncrypted), 'hex'); /** @type {?} */ let subkey1 = this.bitShiftLeft(l); // tslint:disable-next-line:no-bitwise if (l[0] & 0x80) { subkey1 = this.xor(subkey1, const_Rb); } /** @type {?} */ let subkey2 = this.bitShiftLeft(subkey1); // tslint:disable-next-line:no-bitwise if (subkey1[0] & 0x80) { subkey2 = this.xor(subkey2, const_Rb); } return { subkey1: subkey1, subkey2: subkey2 }; } /** * @param {?} key * @param {?} message * @return {?} */ aesCmac(key, message) { console.log('INICIA CIFRADO!!!!!!!!!!!!!!!!'); /** @type {?} */ const subkeys = this.generateSubkeys(key); /** @type {?} */ let blockCount = Math.ceil(message.length / 16); /** @type {?} */ let lastBlockCompleteFlag; /** @type {?} */ let lastBlock; /** @type {?} */ let lastBlockIndex; if (blockCount === 0) { blockCount = 1; lastBlockCompleteFlag = false; } else { lastBlockCompleteFlag = message.length % 16 === 0; } lastBlockIndex = blockCount - 1; if (lastBlockCompleteFlag) { lastBlock = this.xor(this.getMessageBlock(message, lastBlockIndex), subkeys.subkey1); } else { lastBlock = this.xor(this.getPaddedMessageBlock(message, lastBlockIndex), subkeys.subkey2); } /** @type {?} */ let x = new Buffer('00000000000000000000000000000000', 'hex'); /** @type {?} */ let y; /** @type {?} */ let enctrypMethodInstance; for (let index = 0; index < lastBlockIndex; index++) { enctrypMethodInstance = new ModeOfOperation.cbc(key, new Uint8Array(16)); y = this.xor(x, this.getMessageBlock(message, index)); /** @type {?} */ const xEncrypted = enctrypMethodInstance.encrypt(y); console.log('X normal ===============> ', this.bytesTohex(y)); console.log('X encrypted ==============> ', this.bytesTohex(xEncrypted)); x = new Buffer(this.bytesTohex(xEncrypted), 'hex'); } y = this.xor(lastBlock, x); enctrypMethodInstance = new ModeOfOperation.cbc(key, new Uint8Array(16)); /** @type {?} */ const yEncrypted = enctrypMethodInstance.encrypt(y); console.log('Y normal ==============> ', this.bytesTohex(y)); console.log('Y encrypted ==============> ', this.bytesTohex(yEncrypted)); return yEncrypted; } /** * @param {?} message * @param {?} blockIndex * @return {?} */ getMessageBlock(message, blockIndex) { /** @type {?} */ const block = new Buffer(16); /** @type {?} */ const start = blockIndex * 16; /** @type {?} */ const end = start + 16; /** @type {?} */ let blockI = 0; for (let i = start; i < end; i++) { block[blockI] = message[i]; blockI++; } return block; } /** * @param {?} message * @param {?} blockIndex * @return {?} */ getPaddedMessageBlock(message, blockIndex) { /** @type {?} */ const block = new Buffer(16); /** @type {?} */ const start = blockIndex * 16; /** @type {?} */ const end = message.length; block.fill(0); /** @type {?} */ let blockI = 0; for (let i = start; i < end; i++) { block[blockI] = message[i]; blockI++; } block[end - start] = 0x80; return block; } /** * @param {?} buffer * @return {?} */ bitShiftLeft(buffer) { /** @type {?} */ const shifted = new Buffer(buffer.length); /** @type {?} */ const last = buffer.length - 1; for (let index = 0; index < last; index++) { // tslint:disable-next-line:no-bitwise shifted[index] = buffer[index] << 1; // tslint:disable-next-line:no-bitwise if (buffer[index + 1] & 0x80) { shifted[index] += 0x01; } } // tslint:disable-next-line:no-bitwise shifted[last] = buffer[last] << 1; return shifted; } /** * @param {?} bufferA * @param {?} bufferB * @return {?} */ xor(bufferA, bufferB) { /** @type {?} */ const length = Math.min(bufferA.length, bufferB.length); /** @type {?} */ const output = new Buffer(length); for (let index = 0; index < length; index++) { // tslint:disable-next-line:no-bitwise output[index] = bufferA[index] ^ bufferB[index]; } return output; } } CypherAesService.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] }, ]; /** @nocollapse */ CypherAesService.ctorParameters = () => []; /** @nocollapse */ CypherAesService.ngInjectableDef = defineInjectable({ factory: function CypherAesService_Factory() { return new CypherAesService(); }, token: CypherAesService, providedIn: "root" }); /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class ConsoleLoggerService { /** * @param {...?} args * @return {?} */ log(...args) { console.log.apply(console, args); } /** * @param {...?} args * @return {?} */ error(...args) { console.error.apply(console, args); } /** * @param {...?} args * @return {?} */ warn(...args) { console.warn.apply(console, args); } } ConsoleLoggerService.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] }, ]; /** @nocollapse */ ConsoleLoggerService.ngInjectableDef = defineInjectable({ factory: function ConsoleLoggerService_Factory() { return new ConsoleLoggerService(); }, token: ConsoleLoggerService, providedIn: "root" }); class NoLoggerService { /** * @param {...?} args * @return {?} */ log(...args) { } /** * @param {...?} args * @return {?} */ error(...args) { } /** * @param {...?} args * @return {?} */ warn(...args) { } } NoLoggerService.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] }, ]; /** @nocollapse */ NoLoggerService.ngInjectableDef = defineInjectable({ factory: function NoLoggerService_Factory() { return new NoLoggerService(); }, token: NoLoggerService, providedIn: "root" }); /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ class BluetoothService extends Subject { /** * @param {?} _webBle * @param {?} cypherAesService * @param {?} _console */ constructor(_webBle, cypherAesService, _console) { super(); this._webBle = _webBle; this.cypherAesService = cypherAesService; this._console = _console; this.serviceCharacteristicVsSubscriptionList = {}; this.notifierSubject = new Subject(); this.notifierStartedSubject = new Subject(); this.bluetoothAvailable = false; this._device$ = new EventEmitter(); if (_webBle._ble) { this.bluetoothAvailable = true; } } /** * @return {?} */ isBluetoothAvailable() { return this.bluetoothAvailable; } /** * get the current device, if the device return null is because the connection has lost * @return {?} the current connceted device */ getDevice$() { return this._device$; } /** * @return {?} */ getNotifierStartedSubject$() { return this.notifierStartedSubject; } /** * start a stream by notifiers characteristics * @param {?} service The service to which the characteristic belongs * @param {?} characteristic The characteristic whose value you want to listen * @param {?} options object that contains the * startByte:number (required), stopByte:number (required), * lengthPosition: { start: number, end: number, lengthPadding: number } (required) * @return {?} A DataView than contains the characteristic value */ startNotifierListener$(service, characteristic, options) { return defer(() => { of(service) .pipe(tap(() => this._console.log('Inicia el notifier con la instancia: ', this.serviceCharacteristicVsSubscriptionList)), filter(_ => Object.keys(this.serviceCharacteristicVsSubscriptionList).indexOf(`${service}-${characteristic}`) === -1)) .subscribe(() => { this.serviceCharacteristicVsSubscriptionList[`${service}-${characteristic}`] = this.buildNotifierListener$(service, characteristic, options).subscribe(message => { this.notifierSubject.next(message); }); }, err => { this._console.log('[BLE::Info] Error in notifier: ', err); }, () => { }); return of(`notifier as subscribed: service= ${service}, characteristic= ${characteristic}`); }); } /** * @param {?} service * @param {?} characteristic * @return {?} */ stopNotifierListener$(service, characteristic) { return defer(() => { // this.serviceCharacteristicVsSubscriptionList[`${service}-${characteristic}`].unsubscribe(); delete this.serviceCharacteristicVsSubscriptionList[`${service}-${characteristic}`]; return of(`the notifier of the characteristic ${characteristic} as been stopped`); }); } /** * @param {?} service * @param {?} characteristic * @param {?} options * @return {?} */ buildNotifierListener$(service, characteristic, options) { return this.getPrimaryService$(service).pipe(tap(serviceInstance => this._console.log(`toma exitosamente el servicio ================> ${serviceInstance}`)), mergeMap(primaryService => this.getCharacteristic$(primaryService, characteristic) .pipe(tap(char => this._console.log(`toma exitosamente la caracteristica ================> ${char}`)))), mergeMap((char) => { return defer(() => { // enable the characteristic notifier return char.startNotifications(); }).pipe(retryWhen(error => error.pipe(tap(() => this._console.log('ERROR EN EL startNotifications ================> ')), delay(1000), take(5))), tap(() => { this.notifierStartedSubject.next(true); this._console.log(`incia las notifiaciones de la caracteristica 2 ================> ${characteristic}`); }), mergeMap(_ => { // start the lister from the even characteristicvaluechanged to get all changes on the specific // characteristic return fromEvent(char, 'characteristicvaluechanged').pipe(takeUntil(fromEvent(char, 'gattserverdisconnected')), map((event) => { // get a event from the characteristic and map that return { startByteMatches: false, stopByteMatches: false, lengthMatches: false, messageLength: 0, timestamp: Date.now(), data: Array.from(new Uint8Array((/** @type {?} */ ((/** @type {?} */ (event.target)) .value)).buffer)) }; }), scan((acc, value) => { acc.timestamp = acc.timestamp === 0 ? value.timestamp : acc.timestamp; // if the current accumulator value is a valid message, then is restarted to get the next // message if ((acc.lengthMatches && acc.startByteMatches && acc.stopByteMatches) || acc.timestamp + 1000 < Date.now()) { acc = { startByteMatches: false, stopByteMatches: false, lengthMatches: false, messageLength: 0, timestamp: 0, data: [] }; } // validate the start byte if (!acc.startByteMatches && value.data[0] === options.startByte) { // get the message length using the start and end position acc.messageLength = new DataView(new Uint8Array(value.data.slice(options.lengthPosition.start, options.lengthPosition.end)).buffer).getInt16(0, false) + (options.lengthPosition.lengthPadding ? options.lengthPosition.lengthPadding : 0); // valid that the initial byte was found acc.startByteMatches = true; } if (!acc.stopByteMatches && value.data[value.data.length - 1] === options.stopByte) { // valid that the end byte was found acc.stopByteMatches = true; } if (acc.startByteMatches) { // merge the new data bytes to the old bytes acc.data = acc.data.concat(value.data); } acc.lengthMatches = acc.startByteMatches && acc.stopByteMatches && acc.messageLength === acc.data.length; return acc; }, { startByteMatches: false, stopByteMatches: false, lengthMatches: false, messageLength: 0, timestamp: 0, data: [] }), // only publish the complete and valid message filter(data => data.lengthMatches && data.startByteMatches && data.stopByteMatches), // remove all custom data and publish the message data map(result => result.data)); })); })); } /** * Send a request to the device and wait a unique response * @param {?} message Message to send * @param {?} service The service to which the characteristic belongs * @param {?} characteristic The characteristic whose value you want to send the message * @param {?} responseType filter to use to identify the response, Sample: [{position: 3, byteToMatch: 0x83}, * {position: 13, byteToMatch: 0x45}] * @param {?=} cypherMasterKey master key to decrypt the message, only use this para if the message to receive is encrypted * @return {?} */ sendAndWaitResponse$(message, service, characteristic, responseType, cypherMasterKey) { this._console.log('[BLE::Info] Send message to device: ', this.cypherAesService.bytesTohex(message)); return forkJoin(this.subscribeToNotifierListener(responseType, cypherMasterKey).pipe(take(1)), this.sendToNotifier$(message, service, characteristic)).pipe(map(([messageResp, _]) => messageResp), timeout(3000)); } /** * Subscribe to the notifiers filtering by byte checking * @param {?} filterOptions must specific the position and the byte to match Sample: * [{position: 3, byteToMatch: 0x83}, {position: 13, byteToMatch: 0x45}] * @param {?=} cypherMasterKey master key to decrypt the message, only use this para if the message to receive is encrypted * @return {?} */ subscribeToNotifierListener(filterOptions, cypherMasterKey) { return this.notifierSubject.pipe(map(messageUnformated => { /** @type {?} */ let messageFormmated = /** @type {?} */ (messageUnformated); // validate if the message is cyphered if (cypherMasterKey) { /** @type {?} */ const datablockLength = new DataView(new Uint8Array(messageFormmated.slice(1, 3)).buffer).getInt16(0, false); this.cypherAesService.config(cypherMasterKey); /** @type {?} */ const datablock = Array.from(this.cypherAesService.decrypt((/** @type {?} */ (messageUnformated)).slice(3, datablockLength + 3))); // merge the datablock and the message messageFormmated = (/** @type {?} */ (messageUnformated)) .slice(0, 3) .concat(datablock) .concat((/** @type {?} */ (messageUnformated)).slice(-2)); } this._console.log('[BLE::Info] Notification reived from device: ', this.cypherAesService.bytesTohex(messageFormmated)); return messageFormmated; }), // filter the message using the filter options filter(message => { /** @type {?} */ let availableMessage = false; for (const option of filterOptions) { if (message[option.position] === option.byteToMatch) { availableMessage = true; } else { availableMessage = false; break; } } return availableMessage; })); } /** * Start a request to the browser to list all available bluetooth devices * @param {?=} options Options to request the devices the structure is: * acceptAllDevices: true|false * filters: BluetoothDataFilterInit (see https://webbluetoothcg.github.io/web-bluetooth/#dictdef-bluetoothlescanfilterinit for more info) * optionalServices: [] (services that are going to be used in * communication with the device, must use the UIID or GATT identfier to list ther services) * @return {?} */ discoverDevice$(options = /** @type {?} */ ({})) { return defer(() => this._webBle.requestDevice(options)).pipe(mergeMap(device => { this.device = device; this._device$.emit(device); return this.configureDeviceDisconnection$(device).pipe(mapTo(device)); })); } /** * @param {?} device * @return {?} */ configureDeviceDisconnection$(device) { return Observable.create(observer => { of(device) .pipe(mergeMap(dev => fromEvent(device, 'gattserverdisconnected')), take(1)) .subscribe(() => { this._console.log('Se desconecta disp en OnDevice disconnected!!!!!!!'); this.device = null; this._device$.emit(null); }, err => { this._console.log('[BLE::Info] Error in notifier: ', err); observer.error(err); }, () => { }); observer.next(`DisconnectionEvent as been register`); }); } /** * Discover all available devices and connect to a selected device * @param {?=} options Options to request the devices the structure is: * acceptAllDevices: true|false * filters: BluetoothDataFilterInit (see https://webbluetoothcg.github.io/web-bluetooth/#dictdef-bluetoothlescanfilterinit for more info) * optionalServices: [] (services that are going to be used in * communication with the device, must use the UIID or GATT identfier to list ther services) * @return {?} the connected device */ connectDevice$(options) { if (!options) { options = { acceptAllDevices: true, optionalServices: [] }; } else if (!options.optionalServices) { options.optionalServices = []; } options.optionalServices.push(GattServices.GENERIC_ACCESS.SERVICE); options.optionalServices.push(GattServices.BATTERY.SERVICE); options.optionalServices.push(GattServices.DEVICE_INFORMATION.SERVICE); return this.discoverDevice$(options).pipe(mergeMap(device => { return defer(() => (/** @type {?} */ (device)).gatt.connect()); })); } /** * Disconnect the current device * @return {?} */ disconnectDevice() { if (this.device) { this._console.log('se deconecta dispositivo'); this.device.gatt.disconnect(); } } /** * get a data from the device using the characteristic * @param {?} service UUID or GATT identifier service * @param {?} characteristic UUID or GATT identifier characteristic * @return {?} The characteristic data in a DataView object */ readDeviceValue$(service, characteristic) { if (!this.device) { throw new Error('Must start a connection to a device before read the device value'); } return this.getPrimaryService$(service).pipe(mergeMap(primaryService => this.getCharacteristic$(primaryService, characteristic)), mergeMap((characteristicValue) => this.readValue$(characteristicValue))); } /** * write a value in the selected characteristic * @param {?} service * @param {?} characteristic the characterisitc where you want write the value * @param {?} value value the value to write * @return {?} */ writeDeviceValue$(service, characteristic, value) { if (!this.device) { throw new Error('Must start a connection to a device before read the device value'); } return this.getPrimaryService$(service).pipe(mergeMap(primaryService => this.getCharacteristic$(primaryService, characteristic)), mergeMap((characteristicValue) => this.writeValue$(characteristicValue, value))); } /** * get a primary service instance using the service UIID or GATT identifier * @param {?} service service identifier * @return {?} service instance */ getPrimaryService$(service) { return of(this.device).pipe(mergeMap(device => { return device.gatt.getPrimaryService(service); })); } /** * Get a characterisitic instance using the service instance and a characteristic UUID * @param {?} primaryService service instance * @param {?} characteristic characterisitic identifier * @return {?} characteristic instance */ getCharacteristic$(primaryService, characteristic) { return defer(() => primaryService.getCharacteristic(characteristic)); } /** * read the characteristic value * @param {?} characteristic characteristic instance * @return {?} The characteristic data in a DataView object */ readValue$(characteristic) { return from(characteristic .readValue() .then((data) => Promise.resolve(data), (error) => Promise.reject(`${error.message}`))); } /** * write a value in the selected characteristic * @param {?} characteristic the characterisitc where you want write the value * @param {?} value the value to write * @return {?} */ writeValue$(characteristic, value) { return defer(() => characteristic .writeValue(value)); } /** * change the state of the characteristic to enable it * @param {?} service parent service of the characteristic * @param {?} characteristic characteristic to change the state * @param {?=} state new state * @return {?} */ enableCharacteristic$(service, characteristic, state) { state = state || new Uint8Array([1]); return this.setCharacteristicState$(service, characteristic, state); } /** * change the state of the characteristic to disable it * @param {?} service parent service of the characteristic * @param {?} characteristic characteristic to change the state * @param {?=} state new state * @return {?} */ disbaleCharacteristic$(service, characteristic, state) { state = state || new Uint8Array([0]); return this.setCharacteristicState$(service, characteristic, state); } /** * set a state to an specific characteristic * @param {?} service parent service of the characteristic * @param {?} characteristic characteristic to change the state * @param {?} state new state * @return {?} */ setCharacteristicState$(service, characteristic, state) { /** @type {?} */ const primaryService = this.getPrimaryService$(service); return primaryService.pipe(mergeMap(_primaryService => this.getCharacteristic$(_primaryService, characteristic)), map((_characteristic) => this.writeValue$(_characteristic, state))); } /** * Send a message using a notifier characteristic * @param {?} message message to send * @param {?} service service to which the characteristic belongs * @param {?} characteristic feature in which you want to send the notification * @return {?} */ sendToNotifier$(message, service, characteristic) { return this.getPrimaryService$(service).pipe(mergeMap((primaryService) => this.getCharacteristic$(primaryService, characteristic)), mergeMap((char) => { if (message.length > 16) { /** @type {?} */ let obsTest = of(undefined); while (message.length > 16) { obsTest = obsTest.pipe(concat(this.writeValue$(char, message.slice(0, 16)))); message = message.slice(16, message.length); } if (message.length > 0) { obsTest = obsTest.pipe(concat(this.writeValue$(char, message))); } return obsTest; } else { return this.writeValue$(char, message); } })); } /** * The Battery Level characteristic is read using the GATT Read Characteristic * Value sub-procedure and returns the current battery level as a percentage * from 0% to 100%; 0% represents a battery that is fully discharged, 100% * represents a battery that is fully charged * @return {?} */ getBatteryLevel$() { return this.readDeviceValue$(GattServices.BATTERY.SERVICE, GattServices.BATTERY.BATTERY_LEVEL).pipe(map(value => value.getUint8(0))); } /** * This characteristic represents the name of the manufacturer of the device. * @return {?} */ getManufacturerName$() { return this.readDeviceValue$(GattServices.DEVICE_INFORMATION.SERVICE, GattServices.DEVICE_INFORMATION.MANUFACTURER_NAME).pipe(map(dataView => this.cypherAesService.bytesToText(new Uint8Array(dataView.buffer)))); } /** * This characteristic represents the model number that is assigned by the device vendor. * @return {?} */ getModelNumber$() { return this.readDeviceValue$(GattServices.DEVICE_INFORMATION.SERVICE, GattServices.DEVICE_INFORMATION.MODEL_NUMBER).pipe(map(dataView => this.cypherAesService.bytesToText(new Uint8Array(dataView.buffer)))); } /** * This characteristic represents the serial number for a particular instance of the device. * @return {?} */ getSerialNumber$() { return this.readDeviceValue$(GattServices.DEVICE_INFORMATION.SERVICE, GattServices.DEVICE_INFORMATION.SERIAL_NUMBER).pipe(map(dataView => this.cypherAesService.bytesToText(new Uint8Array(dataView.buffer)))); } /** * This characteristic represents the hardware revision for the hardware within the device. * @return {?} */ getHardwareRevision$() { return this.readDeviceValue$(GattServices.DEVICE_INFORMATION.SERVICE, GattServices.DEVICE_INFORMATION.HARDWARE_REVISION).pipe(map(dataView => this.cypherAesService.bytesToText(new Uint8Array(dataView.buffer)))); } /** * This characteristic represents the firmware revision for the firmware within the device. * @return {?} */ getFirmwareRevision$() { return this.readDeviceValue$(GattServices.DEVICE_INFORMATION.SERVICE, GattServices.DEVICE_INFORMATION.FIRMWARE_REVISION).pipe(map(dataView => this.cypherAesService.bytesToText(new Uint8Array(dataView.buffer)))); } /** * This characteristic represents the software revision for the software within the device. * @return {?} */ getSoftwareRevision$() { return this.readDeviceValue$(GattServices.DEVICE_INFORMATION.SERVICE, GattServices.DEVICE_INFORMATION.SOFTWARE_REVISION).pipe(map(dataView => this.cypherAesService.bytesToText(new Uint8Array(dataView.buffer)))); } /** * This characteristic represents a structure containing an Organizationally Unique Identifier * (OUI) followed by a manufacturer-defined identifier and is unique for each individual instance of the product. * @return {?} */ getSystemId$() { return this.readDeviceValue$(GattServices.DEVICE_INFORMATION.SERVICE, GattServices.DEVICE_INFORMATION.SYSTEM_ID); } /** * The PnP_ID characteristic is a set of values used to create a device ID value that is unique for this device. * @return {?} */ getPnpId$() { return this.readDeviceValue$(GattServices.DEVICE_INFORMATION.SERVICE, GattServices.DEVICE_INFORMATION.PNP_ID); } } BluetoothService.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] }, ]; /** @nocollapse */ BluetoothService.ctorParameters = () => [ { type: BrowserWebBluetooth }, { type: CypherAesService }, { type: ConsoleLoggerService } ]; /** @nocollapse */ BluetoothService.ngInjectableDef = defineInjectable({ factory: function BluetoothService_Factory() { return new BluetoothService(inject(BrowserWebBluetooth), inject(CypherAesService), inject(ConsoleLoggerService)); }, token: BluetoothService, providedIn: "root" }); /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** * @return {?} */ function browserWebBluetooth() { return new BrowserWebBluetooth(); } /** * @param {?} options * @return {?} */ function consoleLoggerServiceConfig(options) { if (options && options.enableTracing) { return new ConsoleLoggerService(); } else { return new NoLoggerService(); } } /** * @return {?} */ function makeMeTokenInjector() { return new InjectionToken('AWBOptions'); } class AngularBleModule { /** * @param {?=} options * @return {?} */ static forRoot(options = {}) { return { ngModule: AngularBleModule, providers: [ BluetoothService, { provide: BrowserWebBluetooth, useFactory: browserWebBluetooth }, { provide: makeMeTokenInjector, useValue: options }, { provide: ConsoleLoggerService, useFactory: consoleLoggerServiceConfig, deps: [makeMeTokenInjector] } ] }; } } AngularBleModule.decorators = [ { type: NgModule, args: [{ imports: [CommonModule] },] }, ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} checked by tsc */ export { BrowserWebBluetooth, browserWebBluetooth, consoleLoggerServiceConfig, makeMeTokenInjector, AngularBleModule, CypherAesService, BluetoothService, ConsoleLoggerService as ɵa }; //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibmVidWxhZS1hbmd1bGFyLWJsZS5qcy5tYXAiLCJzb3VyY2VzIjpbIm5nOi8vQG5lYnVsYWUvYW5ndWxhci1ibGUvbGliL3BsYXRmb3JtL2Jyb3dzZXIudHMiLCJuZzovL0BuZWJ1bGFlL2FuZ3VsYXItYmxlL2xpYi9ibHVldG9vdGgvZ2F0dC1zZXJ2aWNlcy50cyIsIm5nOi8vQG5lYnVsYWUvYW5ndWxhci1ibGUvbGliL2N5cGhlci9jeXBoZXItYWVzLnNlcnZpY2UudHMiLCJuZzovL0BuZWJ1bGFlL2FuZ3VsYXItYmxlL2xpYi9sb2dnZXIuc2VydmljZS50cyIsIm5nOi8vQG5lYnVsYWUvYW5ndWxhci1ibGUvbGliL2JsdWV0b290aC9ibHVldG9vdGguc2VydmljZS50cyIsIm5nOi8vQG5lYnVsYWUvYW5ndWxhci1ibGUvbGliL2FuZ3VsYXItYmxlLm1vZHVsZS50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBJbmplY3RhYmxlIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XG5pbXBvcnQgeyBCbHVldG9vdGhTZXJ2aWNlIH0gZnJvbSAnLi4vYmx1ZXRvb3RoL2JsdWV0b290aC5zZXJ2aWNlJztcblxuQEluamVjdGFibGUoKVxuZXhwb3J0IGNsYXNzIEJyb3dzZXJXZWJCbHVldG9vdGgge1xuICBwdWJsaWMgX2JsZTtcblxuICBjb25zdHJ1Y3RvcigpIHtcbiAgICB0aGlzLl9ibGUgPSBuYXZpZ2F0b3IuYmx1ZXRvb3RoO1xuICAgIGlmICghdGhpcy5fYmxlKSB7XG4gICAgICBjb25zb2xlLmxvZygnZXJyb3IgY2FyZ2FuZG8gYmx1ZXRvb3RoJyk7XG4gICAgICAvLyBibHVldG9vdGhTZXJ2aWNlLnNldEJsdWV0b290aEF2YWlsYWJsZShmYWxzZSk7XG4gICAgICAvLyB0aHJvdyBuZXcgRXJyb3IoJ1lvdXIgYnJvd3NlciBkb2VzIG5vdCBzdXBwb3J0IFNtYXJ0IEJsdWV0b290aC4gU2VlIGh0dHA6Ly9jYW5pdXNlLmNvbS8jc2VhcmNoPUJsdWV0b290aCBmb3IgbW9yZSBkZXRhaWxzLicpO1xuICAgIH0gZWxzZSB7XG4gICAgICAvLyBibHVldG9vdGhTZXJ2aWNlLnNldEJsdWV0b290aEF2YWlsYWJsZSh0cnVlKTtcbiAgICB9XG4gIH1cblxuICByZXF1ZXN0RGV2aWNlKG9wdGlvbnM6IFJlcXVlc3REZXZpY2VPcHRpb25zKTogUHJvbWlzZTxCbHVldG9vdGhEZXZpY2U+IHtcbiAgICByZXR1cm4gdGhpcy5fYmxlLnJlcXVlc3REZXZpY2Uob3B0aW9ucyk7XG4gIH1cbn1cbiIsImV4cG9ydCBjb25zdCBHYXR0U2VydmljZXMgPSB7XG4gIEdFTkVSSUNfQUNDRVNTOiB7XG4gICAgU0VSVklDRTogJ2dlbmVyaWNfYWNjZXNzJyxcbiAgICBERVZJQ0VfTkFNRTogJ2RldmljZV9uYW1lJyxcbiAgICBBUFBFQVJBTkNFOiAnYXBwZWFyYW5jZScsXG4gICAgUFJJVkFDWV9GTEFHOiAncHJpdmFjeV9mbGFnJyxcbiAgICBSRUNPTk5FQ1RJT05fQUREUkVTUzogJ3JlY29ubmVjdGlvbl9hZGRyZXNzJyxcbiAgICBQRVJJUEhFUkFMX1BSRUZFUlJFRF9DT05ORUNUSU9OX1BBUkFNRVRFUlM6ICdwZXJpcGhlcmFsX3ByZWZlcnJlZF9jb25uZWN0aW9uX3BhcmFtZXRlcnMnXG4gIH0sXG4gIEJBVFRFUlk6IHtcbiAgICBTRVJWSUNFOiAnYmF0dGVyeV9zZXJ2aWNlJyxcbiAgICBCQVRURVJZX0xFVkVMOiAnYmF0dGVyeV9sZXZlbCdcbiAgfSxcbiAgREVWSUNFX0lORk9STUFUSU9OOiB7XG4gICAgU0VSVklDRTogJ2RldmljZV9pbmZvcm1hdGlvbicsXG4gICAgTUFOVUZBQ1RVUkVSX05BTUU6ICdtYW51ZmFjdHVyZXJfbmFtZV9zdHJpbmcnLFxuICAgIE1PREVMX05VTUJFUjogJ21vZGVsX251bWJlcl9zdHJpbmcnLFxuICAgIFNFUklBTF9OVU1CRVI6ICdzZXJpYWxfbnVtYmVyX3N0cmluZycsXG4gICAgSEFSRFdBUkVfUkVWSVNJT046ICdoYXJkd2FyZV9yZXZpc2lvbl9zdHJpbmcnLFxuICAgIEZJUk1XQVJFX1JFVklTSU9OOiAnZmlybXdhcmVfcmV2aXNpb25fc3RyaW5nJyxcbiAgICBTT0ZUV0FSRV9SRVZJU0lPTjogJ3NvZnR3YXJlX3JldmlzaW9uX3N0cmluZycsXG4gICAgU1lTVEVNX0lEOiAnc3lzdGVtX2lkJyxcbiAgICBQTlBfSUQ6ICdwbnBfaWQnXG4gIH1cbn07XG5cbmV4cG9ydCBjb25zdCBHQVRUX1NFUlZJQ0VTID0gT2JqZWN0LmtleXMoR2F0dFNlcnZpY2VzKS5tYXAoXG4gIGtleSA9PiBHYXR0U2VydmljZXNba2V5XS5TRVJWSUNFXG4pO1xuIiwiZGVjbGFyZSBjb25zdCBCdWZmZXI7XG5pbXBvcnQgKiBhcyBhZXMgZnJvbSAnYWVzLWpzJztcbmltcG9ydCB7IEluamVjdGFibGUgfSBmcm9tICdAYW5ndWxhci9jb3JlJztcblxuQEluamVjdGFibGUoe1xuICBwcm92aWRlZEluOiAncm9vdCdcbn0pXG5leHBvcnQgY2xhc3MgQ3lwaGVyQWVzU2VydmljZSB7XG4gIHByaXZhdGUgbWFzdGVyS2V5ID0gW107XG4gIHByaXZhdGUgaW5pdGlhbFZlY3RvciA9IFtdO1xuICBwcml2YXRlIGVuY3J5cHRNZXRob2QgPSAnQ0JDJztcbiAgcHJpdmF0ZSBpc1N0YXRpY0luaXRpYWxWZWN0b3IgPSB0cnVlO1xuICBwcml2YXRlIGVuY3RyeXBNZXRob2RJbnN0YW5jZTtcbiAgcHJpdmF0ZSBpc0NvbmZpZ0V4ZWN1dGVkID0gZmFsc2U7XG4gIHByaXZhdGUgYWRkaXRpb25hbEVuY3J5cHRNZXRob2RQYXJhbXM7XG4gIGNvbnN0cnVjdG9yKCkge31cbiAgLyoqXG4gICAqIEluaXRpYWwgY29uZmlnIHVzZWQgdG8gaW5pdGFsaWNlIGFsbCByZXF1aXJlZCBwYXJhbXNcbiAgICogQHBhcmFtIG1hc3RlcktleSBrZXkgdXNlZCB0byBlbmNyeXB0IGFuZCBkZWNyeXB0XG4gICAqIEBwYXJhbSBpbml0aWFsVmVjdG9yIHZlY3RvciB1c2VkIHRvIGVuY3J5cHQgYWJkIGRlY3J5cHQgZXhjZXB0IHdoZW4gRUNCIGVuY3J5cHQgbWV0aG9kIGlzIHVzZWRcbiAgICogQHBhcmFtIGVuY3J5cHRNZXRob2QgdHlwZSBvZiBlbmNyeXB0IG1ldGhvZCBpcyB1c2VkLCB0aGUgcG9zc2libGUgb3B0aW9ucyBhcmU6IENCQywgQ1RSLCBDRkIsIE9GQiwgRUNCXG4gICAqIEBwYXJhbSBhZGRpdGlvbmFsRW5jcnlwdE1ldGhvZFBhcmFtcyBjb25maWd1cmF0aW9uIHBhcmFtcyB1c2VkIGJ5IHRoZSBzZWxlY3RlZCBlbmNyeXB0IG1ldGhvZC5cbiAgICogTm90ZTogaWYgdGhlIG1ldGhvZCBDVFIgb3IgQ0ZCIGlzIHVzZWQgdGhpcyBwYXJhbSBpcyByZXF1aXJlZCBvdGhlcndpc2UgaXMgYW4gb3B0aW5hbCBwYXJhbS5cbiAgICogQnkgQ1RSIHJlcXVpcmUgdGhlIHBhcmFtIGNvdW50ZXIgYW5kIGJ5IENGQiByZXF1aXJlIHRoZSBwYXJhbSBzZWdtZW50U2l6ZVxuICAgKiBAcGFyYW0gaXNTdGF0aWNJbml0aWFsVmVjdG9yIGRlZmluZXMgaWYgdGhlIGluaXRpYWwgdmVjdG9yIGlzIGNoYW5nZWQgb3Igbm90IHdoZW4gdGhlIGRhdGEgYXJlIGVuY3J5cHRlZCBvciBub3RcbiAgICovXG4gIGNvbmZpZyhcbiAgICBtYXN0ZXJLZXksXG4gICAgaW5pdGlhbFZlY3RvciA9IFswLCAwLCAwLCAwLCAwLCAwLCAwLCAwLCAwLCAwLCAwLCAwLCAwLCAwLCAwLCAwXSxcbiAgICBlbmNyeXB0TWV0aG9kID0gJ0NCQycsXG4gICAgYWRkaXRpb25hbEVuY3J5cHRNZXRob2RQYXJhbXMgPSB7fSxcbiAgICBpc1N0YXRpY0luaXRpYWxWZWN0b3IgPSB0cnVlXG4gICkge1xuICAgIHRoaXMuaXNDb25maWdFeGVjdXRlZCA9IHRydWU7XG4gICAgdGhpcy5tYXN0ZXJLZXkgPSBtYXN0ZXJLZXk7XG4gICAgdGhpcy5pbml0aWFsVmVjdG9yID0gaW5pdGlhbFZlY3RvcjtcbiAgICB0aGlzLmVuY3J5cHRNZXRob2QgPSBlbmNyeXB0TWV0aG9kO1xuICAgIHRoaXMuaXNTdGF0aWNJbml0aWFsVmVjdG9yID0gaXNTdGF0aWNJbml0aWFsVmVjdG9yO1xuICAgIHRoaXMuYWRkaXRpb25hbEVuY3J5cHRNZXRob2RQYXJhbXMgPSBhZGRpdGlvbmFsRW5jcnlwdE1ldGhvZFBhcmFtcztcbiAgICBpZiAoIWlzU3RhdGljSW5pdGlhbFZlY3Rvcikge1xuICAgICAgdGhpcy5lbmN0cnlwTWV0aG9kSW5zdGFuY2UgPSB0aGlzLmdlbmVyYXRlRW5jcnlwdE1ldGhvZEluc3RhbmNlKCk7XG4gICAgfVxuICB9XG4gIC8qKlxuICAgKiBFbmNyeXB0IHRoZSBkYXRhIHVzaW5nIHRoZSBlbmNyeXB0IG1ldGhvZCBwcmV2aW91c2x5IGNvbmZpZ3VyZWRcbiAgICogQHBhcmFtIGRhdGFBcnJheUJ1ZmZlciBkYXRhIHRvIGVuY3J5cHRcbiAgICovXG4gIGVuY3J5cHQoZGF0YUFycmF5QnVmZmVyOiBVaW50OEFycmF5IHwgVWludDE2QXJyYXkgfCBVaW50MzJBcnJheSkge1xuICAgIGlmICghdGhpcy5pc0NvbmZpZ0V4ZWN1dGVkKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoXG4gICAgICAgICdNdXN0IGNvbmZpZ3VyYXRlIGN5cGhlci1hZXMgYmVmb3JlIGNhbGwgdGhpcyBtZXRob2QsIHVzZSB0aGUgbWV0aG9kIGNvbmZpZygpJ1xuICAgICAgKTtcbiAgICB9XG4gICAgaWYgKHRoaXMuZW5jcnlwdE1ldGhvZCA9PT0gJ0NCQycgfHwgdGhpcy5lbmNyeXB0TWV0aG9kID09PSAnRUNCJykge1xuICAgICAgZGF0YUFycmF5QnVmZmVyID0gdGhpcy5hZGRQYWRkaW5nKGRhdGFBcnJheUJ1ZmZlcik7XG4gICAgfVxuICAgIHJldHVybiB0aGlzLmlzU3RhdGljSW5pdGlhbFZlY3RvclxuICAgICAgPyB0aGlzLmdlbmVyYXRlRW5jcnlwdE1ldGhvZEluc3RhbmNlKCkuZW5jcnlwdChkYXRhQXJyYXlCdWZmZXIpXG4gICAgICA6IHRoaXMuZW5jdHJ5cE1ldGhvZEluc3RhbmNlLmVuY3J5cHQoZGF0YUFycmF5QnVmZmVyKTtcbiAgfVxuICAvKipcbiAgICogRGVjcnlwdCB0aGUgZGF0YSB1c2luZyB0aGUgZW5jcnlwdCBtZXRob2QgcHJldmlvdXNseSBjb25maWd1cmVkXG4gICAqIEBwYXJhbSBkYXRhQXJyYXlCdWZmZXIgZGF0YSB0byBkZWNyeXB0XG4gICAqL1xuICBkZWNyeXB0KGRhdGFBcnJheUJ1ZmZlcjogVWludDhBcnJheSB8IFVpbnQxNkFycmF5IHwgVWludDMyQXJyYXkpIHtcbiAgICBpZiAoIXRoaXMuaXNDb25maWdFeGVjdXRlZCkge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICAnTXVzdCBjb25maWd1cmF0ZSBjeXBoZXItYWVzIGJlZm9yZSBjYWxsIHRoaXMgbWV0aG9kLCB1c2UgdGhlIG1ldGhvZCBjb25maWcoKSdcbiAgICAgICk7XG4gICAgfVxuICAgIHJldHVybiB0aGlzLmlzU3RhdGljSW5pdGlhbFZlY3RvclxuICAgICAgPyB0aGlzLmdlbmVyYXRlRW5jcnlwdE1ldGhvZEluc3RhbmNlKCkuZGVjcnlwdChkYXRhQXJyYXlCdWZmZXIpXG4gICAgICA6IHRoaXMuZW5jdHJ5cE1ldGhvZEluc3RhbmNlLmRlY3J5cHQoZGF0YUFycmF5QnVmZmVyKTtcbiAgfVxuICAvKipcbiAgICogQ2hhbmdlIHRoZSBjdXJyZW50IGluaXRhbFZlY3RvclxuICAgKiBAcGFyYW0gaW5pdGlhbFZlY3RvciBuZXcgaW5pdGFsVmVjdG9yXG4gICAqL1xuICBjaGFuZ2VJbml0aWFsVmVjdG9yKGluaXRpYWxWZWN0b3IpIHtcbiAgICBpZiAoIXRoaXMuaXNTdGF0aWNJbml0aWFsVmVjdG9yKSB7XG4gICAgICB0aGlzLmVuY3RyeXBNZXRob2RJbnN0YW5jZSA9IHRoaXMuZ2VuZXJhdGVFbmNyeXB0TWV0aG9kSW5zdGFuY2UoKTtcbiAgICB9XG4gICAgdGhpcy5pbml0aWFsVmVjdG9yID0gaW5pdGlhbFZlY3RvcjtcbiAgfVxuXG4gIC8qKlxuICAgKiBDaGFuZ2UgdGhlIGN1cnJlbnQgZW5jeXB0TWV0aG9kXG4gICAqIEBwYXJhbSBlbmNyeXB0TWV0aG9kIG5ldyBlbmNyeXB0TWV0aG9kXG4gICAqL1xuICBjaGFuZ2VFbmNyeXB0TWV0aG9kKGVuY3J5cHRNZXRob2QpIHtcbiAgICBpZiAoIXRoaXMuaXNTdGF0aWNJbml0aWFsVmVjdG9yKSB7XG4gICAgICB0aGlzLmVuY3RyeXBNZXRob2RJbnN0YW5jZSA9IHRoaXMuZ2VuZXJhdGVFbmNyeXB0TWV0aG9kSW5zdGFuY2UoKTtcbiAgICB9XG4gICAgdGhpcy5lbmNyeXB0TWV0aG9kID0gZW5jcnlwdE1ldGhvZDtcbiAgfVxuXG4gIC8qKlxuICAgKiBDaGFuZ2UgdGhlIGN1cnJlbnQgaXNTdGF0aWNJbml0aWFsVmVjdG9yXG4gICAqIEBwYXJhbSBpc1N0YXRpY0luaXRpYWxWZWN0b3IgbmV3IGlzU3RhdGljSW5pdGFsVmVjdG9yXG4gICAqL1xuICBjaGFuZ2VTdGF0aWNJbml0aWFsVmVjdG9yKGlzU3RhdGljSW5pdGlhbFZlY3Rvcikge1xuICAgIGlmICghaXNTd