@nebulae/angular-ble
Version:
A Web Bluetooth (Bluetooth Low Energy) module for angular (v2+)
1,054 lines (1,044 loc) • 125 kB
JavaScript
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