// tslint:disable:no-bitwise import { Observable } from 'rxjs/Observable'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { ppgGroups } from './config'; import { IConfig, IParam, IData, IDataService, IGroup, IParamParent } from './configs.interfaces'; import { Subject } from 'rxjs/Subject'; import { Bit, writeIntToBuffer } from '../shared/utils/Binary'; import { NgZone } from '@angular/core'; import { BrowserBuffer } from 'app/shared/utils/BrowserBuffer'; import { GenericCache } from 'app/nodes/config-cache'; import { flatten } from 'app/shared/utils/general'; import { ecgGroups, accelerometerGroups } from 'app/nodes/config'; const BitArray = require('node-bitarray'); const range = require('range').range; export class Node { get type() { return this.name ? this.name.split(' ')[1] : undefined; } get name() { return this.peripheral ? this.peripheral.advertisement.localName : undefined; } get id() { return this.peripheral ? this.peripheral.id : undefined; } private configCache = new GenericCache((item: IParamParent) => item.service + '---' + item.characteristic); private datastreamCache = new GenericCache((item: IData) => item.service + '---' + item.characteristic); groups: IGroup[] = []; activeSources: IData[] = []; /* NODE OBSERVABLE PROPERTIES */ discoveredEverything = new BehaviorSubject(false); connected = this.connectedObservable().publishReplay().refCount(); /* BASIC NODE READABLE/SUBSCRIBED CHARACTERISTICS */ batteryStatus = this.createCharacteristicObserver('180F', '2A19', buffer => buffer.readUInt8(0)).publishReplay().refCount(); realTimeClock = this.createCharacteristicObserver('BFC0', 'BFC1', buffer => buffer.readUInt32LE(0)).publishReplay().refCount(); memoryUsage = this.createCharacteristicObserver('BFA0', 'BFA3', buffer => buffer.readUInt32LE(0)).publishReplay().refCount(); totalMemory = this.createCharacteristicObserver('BFA0', 'BFA4', buffer => buffer.readUInt32LE(0)).publishReplay().refCount(); memoryStatus = this.createCharacteristicObserver('BFA0', 'BFA1', (buffer) => buffer.readUInt8(0)).share(); loggingData = this.memoryStatus.map(value => !!(value & 0b1)); sendingSerialData = this.memoryStatus.map(value => !!(value & 0b01)); erasingMemory = this.memoryStatus.map(value => !!(value & 0b001)); loggedChannels = this.createCharacteristicObserver('BFA0', 'BFA2', (buffer) => Bit.parse(buffer.readUInt16LE(0), 16) .map((value, index, array): [number, boolean] => ([array.length - index, !!value])) ).share(); // zone is required for fastest change detection constructor(public peripheral, private zone: NgZone = null) { this.setupConfigurations(); } /* CONFIG - SETUP */ // refactor this to be around config files somewhere setupConfigurations() { if (this.name.startsWith('BF PPG')) { this.groups.push(...ppgGroups); } if (this.name.startsWith('BF ECG')) { this.groups.push(...ecgGroups); } this.groups.push(...accelerometerGroups); } /* CONFIG - ENABLING/DISABLING PLOTS */ togglePlot(plot: IData, enable: Boolean) { const index = this.activeSources.indexOf(plot); if (enable && index < 0) { this.activeSources.push(plot); } else if (!enable && index >= 0) { this.activeSources.splice(index, 1); } } /* CONFIG - CHARACTERISTIC READ/WRITE */ updateConfig(param: IParam, value: number) { const data = this.configCache.get(param); const updatedData = writeIntToBuffer(data, value, param.bits, param.offset); this.configCache.set(param, updatedData); this.writeCharacteristic(param.service, param.characteristic, updatedData, (err) => { if (!err) { console.log('written with successfully'); } else { console.log('error writing config:', err); this.configCache.set(param, data); } }); } readCharacteristic(serviceUUID: string, characteristicUUID: string, cb) { const [service, characteristic] = this.getServiceAndCharacteristic(serviceUUID, characteristicUUID); characteristic.read(cb); } writeCharacteristic(serviceUUID: string, characteristicUUID: string, value: Buffer, cb?) { const [service, characteristic] = this.getServiceAndCharacteristic(serviceUUID, characteristicUUID); if (!characteristic) { throw new Error(`Characteristic ${characteristicUUID} not found`); } characteristic.write(new BrowserBuffer(value), true, cb); } readAllConfigs() { const sections = flatten(this.groups.map(group => group.sections)); const items = flatten(sections.map(section => section.items || [])); const uniqueIdentifiers = items .map(item => item.identifier) .filter((value, index, array) => array.indexOf(value) === index); const promises = uniqueIdentifiers.map(config => new Promise((resolve, reject) => { this.readCharacteristic(config.service, config.characteristic, (error, data) => { if (error) { reject(error); } console.log('read config:', config, data); this.configCache.set(config, data); resolve(); }); })); return Promise.all(promises); } /* DATA READING/STREAMING */ getStream(data: IData): Observable { let stream = this.datastreamCache.get(data); if (!stream) { stream = this.createCharacteristicObserver(data.service, data.characteristic, data.read).share(); this.datastreamCache.set(data, stream); } return stream; } /* CONNECT - DISCONNECT */ connect(): Observable { return new Observable(observer => { if (this.peripheral.state === 'connecting' || this.peripheral.state === 'connected') { return observer.complete(); } this.peripheral.connect((error) => { if (error) { console.log('peripheral connection error'); observer.next(false); } else { observer.next(true); console.log('connected!'); this.peripheral.discoverAllServicesAndCharacteristics((err, serv, char) => { if (!err) { this.readAllConfigs(); this.discoveredEverything.next(true); } else { throw new Error('discoverAllServicesAndCharacteristics: ' + err); } }); } observer.complete(); }); }); } private connectedObservable(): Observable { return new Observable(observer => { observer.next(this.peripheral.state === 'connected'); const onConnect = () => observer.next(true); const onDisconnect = () => observer.next(false); this.peripheral.on('connect', onConnect); this.peripheral.on('disconnect', onDisconnect); return () => { this.peripheral.removeListener('connect', onConnect); this.peripheral.removeListener('disconnect', onDisconnect); }; }); } disconnect() { // since the node is not advertising while connected, we update the last seen time // when disconnecting. if (this.peripheral.state === 'connected') { this.peripheral.seen = new Date(); } this.peripheral.disconnect(() => console.log('disconnected!')); } /* READ/SUBSCRIBE CREATOR */ createCharacteristicObserver(serviceUUID: string, characteristicUUID: string, dataFn: (data: Buffer) => T): Observable { const toggleCallback = (callback, enable: boolean) => (error: string, services: any[], characteristics: any[]) => { const char = characteristics[0]; if (enable) { char.read(); char.notify(true); char.on('data', callback); } else { char.notify(false); char.removeListener('data', callback); } }; return this.connected .filter(connected => connected) .debounceTime(100) .filter(discovered => discovered) .flatMapTo(this.discoveredEverything) .filter(discovered => discovered) .switchMapTo(new Observable(observer => { const onData = (data: Buffer, isNotification = false) => { this.zone ? this.zone.run(() => observer.next(dataFn(data))) : observer.next(dataFn(data)); }; const [service, characteristic] = this.getServiceAndCharacteristic(serviceUUID, characteristicUUID); if (!service || !characteristic) { return; } toggleCallback(onData, true)(null, [service], [characteristic]); return () => toggleCallback(onData, false)(null, [service], [characteristic]); })); } getServiceAndCharacteristic(serviceUUID: string, characteristicUUID: string) { serviceUUID = serviceUUID.toLowerCase(); characteristicUUID = characteristicUUID.toLowerCase(); if (serviceUUID) { const service = this.peripheral.services.find(s => s.uuid === serviceUUID); if (!service) { throw new Error(`Service with uuid '${serviceUUID}' cannot be found`); } // if (!service) { return []; } const characteristic = service.characteristics.find(c => c.uuid === characteristicUUID); if (!service) { throw new Error(`Characteristic with uuid '${characteristicUUID}' cannot be found`); } // if (!characteristic) { return [service]; } return [service, characteristic]; } else { characteristicUUID = serviceUUID; const characteristic = this.peripheral.services .reduce((characteristics, service) => characteristics.concat(service.characteristics)) .find(char => char.uuid === characteristicUUID); return [null, characteristic]; } } }