Arquivos

1076 linhas
34 KiB
JavaScript

'use strict';
const EventEmitter = require('events').EventEmitter;
const _ = require('lodash');
let noble;
const util = require('util');
// Local imports
const ganglionSample = require('./openBCIGanglionSample');
const k = require('./openBCIConstants');
const openBCIUtils = require('./openBCIUtils');
const clone = require('clone');
const _options = {
debug: false,
nobleAutoStart: true,
nobleScanOnPowerOn: true,
sendCounts: false,
simulate: false,
simulatorBoardFailure: false,
simulatorHasAccelerometer: true,
simulatorInternalClockDrift: 0,
simulatorInjectAlpha: true,
simulatorInjectLineNoise: [k.OBCISimulatorLineNoiseHz60, k.OBCISimulatorLineNoiseHz50, k.OBCISimulatorLineNoiseNone],
simulatorSampleRate: 200,
verbose: false
};
/**
* @description The initialization method to call first, before any other method.
* @param options {object} (optional) - Board optional configurations.
* - `debug` {Boolean} - Print out a raw dump of bytes sent and received. (Default `false`)
*
* - `nobleAutoStart` {Boolean} - Automatically initialize `noble`. Subscribes to blue tooth state changes and such.
* (Default `true`)
*
* - `nobleScanOnPowerOn` {Boolean} - Start scanning for Ganglion BLE devices as soon as power turns on.
* (Default `true`)
*
* - `sendCounts` {Boolean} - Send integer raw counts instead of scaled floats.
* (Default `false`)
*
* - `simulate` {Boolean} - (IN-OP) Full functionality, just mock data. Must attach Daisy module by setting
* `simulatorDaisyModuleAttached` to `true` in order to get 16 channels. (Default `false`)
*
* - `simulatorBoardFailure` {Boolean} - (IN-OP) Simulates board communications failure. This occurs when the RFduino on
* the board is not polling the RFduino on the dongle. (Default `false`)
*
* - `simulatorHasAccelerometer` - {Boolean} - Sets simulator to send packets with accelerometer data. (Default `true`)
*
* - `simulatorInjectAlpha` - {Boolean} - Inject a 10Hz alpha wave in Channels 1 and 2 (Default `true`)
*
* - `simulatorInjectLineNoise` {String} - Injects line noise on channels.
* 3 Possible Options:
* `60Hz` - 60Hz line noise (Default) [America]
* `50Hz` - 50Hz line noise [Europe]
* `none` - Do not inject line noise.
*
* - `simulatorSampleRate` {Number} - The sample rate to use for the simulator. Simulator will set to 125 if
* `simulatorDaisyModuleAttached` is set `true`. However, setting this option overrides that
* setting and this sample rate will be used. (Default is `250`)
*
* - `verbose` {Boolean} - Print out useful debugging events. (Default `false`)
* @param callback {function} (optional) - A callback function used to determine if the noble module was able to be started.
* This can be very useful on Windows when there is no compatible BLE device found.
* @constructor
* @author AJ Keller (@pushtheworldllc)
*/
function Ganglion (options, callback) {
if (!(this instanceof Ganglion)) {
return new Ganglion(options, callback);
}
if (options instanceof Function) {
callback = options;
options = {};
}
options = (typeof options !== 'function') && options || {};
let opts = {};
/** Configuring Options */
let o;
for (o in _options) {
var userOption = (o in options) ? o : o.toLowerCase();
var userValue = options[userOption];
delete options[userOption];
if (typeof _options[o] === 'object') {
// an array specifying a list of choices
// if the choice is not in the list, the first one is defaulted to
if (_options[o].indexOf(userValue) !== -1) {
opts[o] = userValue;
} else {
opts[o] = _options[o][0];
}
} else {
// anything else takes the user value if provided, otherwise is a default
if (userValue !== undefined) {
opts[o] = userValue;
} else {
opts[o] = _options[o];
}
}
}
for (o in options) throw new Error('"' + o + '" is not a valid option');
// Set to global options object
this.options = clone(opts);
/** Private Properties (keep alphabetical) */
this._accelArray = [0, 0, 0];
this._connected = false;
this._decompressedSamples = new Array(3);
this._droppedPacketCounter = 0;
this._firstPacket = true;
this._lastDroppedPacket = null;
this._lastPacket = null;
this._localName = null;
this._multiPacketBuffer = null;
this._packetCounter = k.OBCIGanglionByteId18Bit.max;
this._peripheral = null;
this._rfduinoService = null;
this._receiveCharacteristic = null;
this._scanning = false;
this._sendCharacteristic = null;
this._streaming = false;
/** Public Properties (keep alphabetical) */
this.peripheralArray = [];
this.ganglionPeripheralArray = [];
this.previousPeripheralArray = [];
this.manualDisconnect = false;
/** Initializations */
for (var i = 0; i < 3; i++) {
this._decompressedSamples[i] = [0, 0, 0, 0];
}
try {
noble = require('noble');
if (this.options.nobleAutoStart) this._nobleInit(); // It get's the noble going
if (callback) callback();
} catch (e) {
if (callback) callback(e);
}
}
// This allows us to use the emitter class freely outside of the module
util.inherits(Ganglion, EventEmitter);
/**
* Used to enable the accelerometer. Will result in accelerometer packets arriving 10 times a second.
* Note that the accelerometer is enabled by default.
* @return {Promise}
*/
Ganglion.prototype.accelStart = function () {
return this.write(k.OBCIAccelStart);
};
/**
* Used to disable the accelerometer. Prevents accelerometer data packets from arriving.
* @return {Promise}
*/
Ganglion.prototype.accelStop = function () {
return this.write(k.OBCIAccelStop);
};
/**
* Used to start a scan if power is on. Useful if a connection is dropped.
*/
Ganglion.prototype.autoReconnect = function () {
// TODO: send back reconnect status, or reconnect fail
if (noble.state === k.OBCINobleStatePoweredOn) {
this._nobleScanStart();
} else {
console.warn('BLE not AVAILABLE');
}
};
/**
* @description Send a command to the board to turn a specified channel off
* @param channelNumber
* @returns {Promise.<T>}
* @author AJ Keller (@pushtheworldllc)
*/
Ganglion.prototype.channelOff = function (channelNumber) {
return k.commandChannelOff(channelNumber).then((charCommand) => {
// console.log('sent command to turn channel ' + channelNumber + ' by sending command ' + charCommand)
return this.write(charCommand);
});
};
/**
* @description Send a command to the board to turn a specified channel on
* @param channelNumber
* @returns {Promise.<T>|*}
* @author AJ Keller (@pushtheworldllc)
*/
Ganglion.prototype.channelOn = function (channelNumber) {
return k.commandChannelOn(channelNumber).then((charCommand) => {
// console.log('sent command to turn channel ' + channelNumber + ' by sending command ' + charCommand)
return this.write(charCommand);
});
};
/**
* @description The essential precursor method to be called initially to establish a
* ble connection to the OpenBCI ganglion board.
* @param id {String | Object} - a string local name or peripheral object
* @returns {Promise} If the board was able to connect.
* @author AJ Keller (@pushtheworldllc)
*/
Ganglion.prototype.connect = function (id) {
return new Promise((resolve, reject) => {
if (_.isString(id)) {
k.getPeripheralWithLocalName(this.ganglionPeripheralArray, id)
.then((p) => {
return this._nobleConnect(p);
})
.then(resolve)
.catch(reject);
} else if (_.isObject(id)) {
this._nobleConnect(id)
.then(resolve)
.catch(reject);
} else {
reject(k.OBCIErrorInvalidByteLength);
}
});
};
/**
* Destroys the noble!
*/
Ganglion.prototype.destroyNoble = function () {
this._nobleDestroy();
};
/**
* Destroys the multi packet buffer.
*/
Ganglion.prototype.destroyMultiPacketBuffer = function () {
this._multiPacketBuffer = null;
};
/**
* @description Closes the connection to the board. Waits for stop streaming command to
* be sent if currently streaming.
* @param stopStreaming {Boolean} (optional) - True if you want to stop streaming before disconnecting.
* @returns {Promise} - fulfilled by a successful close, rejected otherwise.
* @author AJ Keller (@pushtheworldllc)
*/
Ganglion.prototype.disconnect = function (stopStreaming) {
// no need for timeout here; streamStop already performs a delay
return Promise.resolve()
.then(() => {
if (stopStreaming) {
if (this.isStreaming()) {
if (this.options.verbose) console.log('stop streaming');
return this.streamStop();
}
}
return Promise.resolve();
})
.then(() => {
return new Promise((resolve, reject) => {
// serial emitting 'close' will call _disconnected
if (this._peripheral) {
this._peripheral.disconnect((err) => {
if (err) {
this._disconnected();
reject(err);
} else {
this._disconnected();
resolve();
}
});
} else {
reject('no peripheral to disconnect');
}
});
});
};
/**
* Return the local name of the attached Ganglion device.
* @return {null|String}
*/
Ganglion.prototype.getLocalName = function () {
return this._localName;
};
/**
* Get's the multi packet buffer.
* @return {null|Buffer} - Can be null if no multi packets received.
*/
Ganglion.prototype.getMutliPacketBuffer = function () {
return this._multiPacketBuffer;
};
/**
* Call to start testing impedance.
* @return {global.Promise|Promise}
*/
Ganglion.prototype.impedanceStart = function () {
return this.write(k.OBCIGanglionImpedanceStart);
};
/**
* Call to stop testing impedance.
* @return {global.Promise|Promise}
*/
Ganglion.prototype.impedanceStop = function () {
return this.write(k.OBCIGanglionImpedanceStop);
};
/**
* @description Checks if the driver is connected to a board.
* @returns {boolean} - True if connected.
*/
Ganglion.prototype.isConnected = function () {
return this._connected;
};
/**
* @description Checks if bluetooth is powered on.
* @returns {boolean} - True if bluetooth is powered on.
*/
Ganglion.prototype.isNobleReady = function () {
return this._nobleReady();
};
/**
* @description Checks if noble is currently scanning.
* @returns {boolean} - True if streaming.
*/
Ganglion.prototype.isSearching = function () {
return this._scanning;
};
/**
* @description Checks if the board is currently sending samples.
* @returns {boolean} - True if streaming.
*/
Ganglion.prototype.isStreaming = function () {
return this._streaming;
};
/**
* @description This function is used as a convenience method to determine how many
* channels the current board is using.
* @returns {Number} A number
* Note: This is dependent on if you configured the board correctly on setup options
* @author AJ Keller (@pushtheworldllc)
*/
Ganglion.prototype.numberOfChannels = function () {
return k.OBCINumberOfChannelsGanglion;
};
/**
* @description To print out the register settings to the console
* @returns {Promise.<T>|*}
* @author AJ Keller (@pushtheworldllc)
*/
Ganglion.prototype.printRegisterSettings = function () {
return this.write(k.OBCIMiscQueryRegisterSettings);
};
/**
* @description Get the the current sample rate is.
* @returns {Number} The sample rate
* Note: This is dependent on if you configured the board correctly on setup options
*/
Ganglion.prototype.sampleRate = function () {
if (this.options.simulate) {
return this.options.simulatorSampleRate;
} else {
return k.OBCISampleRate200;
}
};
/**
* @description List available peripherals so the user can choose a device when not
* automatically found.
* @param `maxSearchTime` {Number} - The amount of time to spend searching. (Default is 20 seconds)
* @returns {Promise} - If scan was started
*/
Ganglion.prototype.searchStart = function (maxSearchTime) {
const searchTime = maxSearchTime || k.OBCIGanglionBleSearchTime;
return new Promise((resolve, reject) => {
this._searchTimeout = setTimeout(() => {
this._nobleScanStop().catch(reject);
reject('Timeout: Unable to find Ganglion');
}, searchTime);
this._nobleScanStart()
.then(() => {
resolve();
})
.catch((err) => {
if (err !== k.OBCIErrorNobleAlreadyScanning) { // If it's already scanning
clearTimeout(this._searchTimeout);
reject(err);
}
});
});
};
/**
* Called to end a search.
* @return {global.Promise|Promise}
*/
Ganglion.prototype.searchStop = function () {
return this._nobleScanStop();
};
/**
* @description Sends a soft reset command to the board
* @returns {Promise} - Fulfilled if the command was sent to board.
* @author AJ Keller (@pushtheworldllc)
*/
Ganglion.prototype.softReset = function () {
return this.write(k.OBCIMiscSoftReset);
};
/**
* @description Sends a start streaming command to the board.
* @returns {Promise} indicating if the signal was able to be sent.
* Note: You must have successfully connected to an OpenBCI board using the connect
* method. Just because the signal was able to be sent to the board, does not
* mean the board will start streaming.
* @author AJ Keller (@pushtheworldllc)
*/
Ganglion.prototype.streamStart = function () {
return new Promise((resolve, reject) => {
if (this.isStreaming()) return reject('Error [.streamStart()]: Already streaming');
this._streaming = true;
this.write(k.OBCIStreamStart)
.then(() => {
if (this.options.verbose) console.log('Sent stream start to board.');
resolve();
})
.catch(reject);
});
};
/**
* @description Sends a stop streaming command to the board.
* @returns {Promise} indicating if the signal was able to be sent.
* Note: You must have successfully connected to an OpenBCI board using the connect
* method. Just because the signal was able to be sent to the board, does not
* mean the board stopped streaming.
* @author AJ Keller (@pushtheworldllc)
*/
Ganglion.prototype.streamStop = function () {
return new Promise((resolve, reject) => {
if (!this.isStreaming()) return reject('Error [.streamStop()]: No stream to stop');
this._streaming = false;
this.write(k.OBCIStreamStop)
.then(() => {
resolve();
})
.catch(reject);
});
};
/**
* @description Puts the board in synthetic data generation mode. Must call streamStart still.
* @returns {Promise} indicating if the signal was able to be sent.
* @author AJ Keller (@pushtheworldllc)
*/
Ganglion.prototype.syntheticEnable = function () {
return new Promise((resolve, reject) => {
this.write(k.OBCIGanglionSyntheticDataEnable)
.then(() => {
if (this.options.verbose) console.log('Enabled synthetic data mode.');
resolve();
})
.catch(reject);
});
};
/**
* @description Takes the board out of synthetic data generation mode. Must call streamStart still.
* @returns {Promise} - fulfilled if the command was sent.
* @author AJ Keller (@pushtheworldllc)
*/
Ganglion.prototype.syntheticDisable = function () {
return new Promise((resolve, reject) => {
this.write(k.OBCIGanglionSyntheticDataDisable)
.then(() => {
if (this.options.verbose) console.log('Disabled synthetic data mode.');
resolve();
})
.catch(reject);
});
};
/**
* @description Used to send data to the board.
* @param data {Array | Buffer | Number | String} - The data to write out
* @returns {Promise} - fulfilled if command was able to be sent
* @author AJ Keller (@pushtheworldllc)
*/
Ganglion.prototype.write = function (data) {
return new Promise((resolve, reject) => {
if (this._sendCharacteristic) {
if (!Buffer.isBuffer(data)) {
data = new Buffer(data);
}
this._sendCharacteristic.write(data, true, (err) => {
if (err) {
reject(err);
} else {
if (this.options.debug) openBCIUtils.debugBytes('>>>', data);
resolve();
}
});
} else {
reject('Send characteristic not set, please call connect method');
}
});
};
// //////// //
// PRIVATES //
// //////// //
/**
* Builds a sample object from an array and sample number.
* @param sampleNumber
* @param rawData
* @return {{sampleNumber: *}}
* @private
*/
Ganglion.prototype._buildSample = function (sampleNumber, rawData) {
let sample = {
sampleNumber: sampleNumber,
timeStamp: Date.now()
};
if (this.options.sendCounts) {
sample['channelDataCounts'] = rawData;
} else {
sample['channelData'] = [];
for (let j = 0; j < k.OBCINumberOfChannelsGanglion; j++) {
sample.channelData.push(rawData[j] * k.OBCIGanglionScaleFactorPerCountVolts);
}
}
return sample;
};
/**
* Utilize `receivedDeltas` to get actual count values.
* @param receivedDeltas {Array} - An array of deltas
* of shape 2x4 (2 samples per packet and 4 channels per sample.)
* @private
*/
Ganglion.prototype._decompressSamples = function (receivedDeltas) {
// add the delta to the previous value
for (let i = 1; i < 3; i++) {
for (let j = 0; j < 4; j++) {
this._decompressedSamples[i][j] = this._decompressedSamples[i - 1][j] - receivedDeltas[i - 1][j];
}
}
};
/**
* @description Called once when for any reason the ble connection is no longer open.
* @private
*/
Ganglion.prototype._disconnected = function () {
this._streaming = false;
this._connected = false;
// Clean up _noble
// TODO: Figure out how to fire function on process ending from inside module
// noble.removeListener('discover', this._nobleOnDeviceDiscoveredCallback);
if (this._receiveCharacteristic) {
this._receiveCharacteristic.removeAllListeners(k.OBCINobleEmitterServiceRead);
}
this._receiveCharacteristic = null;
if (this._rfduinoService) {
this._rfduinoService.removeAllListeners(k.OBCINobleEmitterServiceCharacteristicsDiscover);
}
this._rfduinoService = null;
if (this._peripheral) {
this._peripheral.removeAllListeners(k.OBCINobleEmitterPeripheralConnect);
this._peripheral.removeAllListeners(k.OBCINobleEmitterPeripheralDisconnect);
this._peripheral.removeAllListeners(k.OBCINobleEmitterPeripheralServicesDiscover);
}
this._peripheral = null;
if (!this.manualDisconnect) {
// this.autoReconnect();
}
if (this.options.verbose) console.log(`Private disconnect clean up`);
this.emit('close');
};
/**
* Call to destroy the noble event emitters.
* @private
*/
Ganglion.prototype._nobleDestroy = function () {
if (noble) {
noble.removeAllListeners(k.OBCINobleEmitterStateChange);
noble.removeAllListeners(k.OBCINobleEmitterDiscover);
}
};
Ganglion.prototype._nobleConnect = function (peripheral) {
return new Promise((resolve, reject) => {
if (this.isConnected()) return reject('already connected!');
this._peripheral = peripheral;
this._localName = peripheral.advertisement.localName;
// if (_.contains(_peripheral.advertisement.localName, rfduino.localNamePrefix)) {
// TODO: slice first 8 of localName and see if that is ganglion
// here is where we can capture the advertisement data from the rfduino and check to make sure its ours
if (this.options.verbose) console.log('Device is advertising \'' + this._peripheral.advertisement.localName + '\' service.');
// TODO: filter based on advertising name ie make sure we are looking for the right thing
// if (this.options.verbose) console.log("serviceUUID: " + this._peripheral.advertisement.serviceUuids);
this._peripheral.on(k.OBCINobleEmitterPeripheralConnect, () => {
// if (this.options.verbose) console.log("got connect event");
this._peripheral.discoverServices();
if (this.isSearching()) this._nobleScanStop();
});
this._peripheral.on(k.OBCINobleEmitterPeripheralDisconnect, () => {
if (this.options.verbose) console.log('Peripheral disconnected');
this._disconnected();
});
this._peripheral.on(k.OBCINobleEmitterPeripheralServicesDiscover, (services) => {
for (var i = 0; i < services.length; i++) {
if (services[i].uuid === k.SimbleeUuidService) {
this._rfduinoService = services[i];
// if (this.options.verbose) console.log("Found simblee Service");
break;
}
}
if (!this._rfduinoService) {
reject('Couldn\'t find the simblee service.');
}
this._rfduinoService.once(k.OBCINobleEmitterServiceCharacteristicsDiscover, (characteristics) => {
if (this.options.verbose) console.log('Discovered ' + characteristics.length + ' service characteristics');
for (var i = 0; i < characteristics.length; i++) {
// console.log(characteristics[i].uuid);
if (characteristics[i].uuid === k.SimbleeUuidReceive) {
if (this.options.verbose) console.log("Found receiveCharacteristicUUID");
this._receiveCharacteristic = characteristics[i];
}
if (characteristics[i].uuid === k.SimbleeUuidSend) {
if (this.options.verbose) console.log("Found sendCharacteristicUUID");
this._sendCharacteristic = characteristics[i];
}
}
if (this._receiveCharacteristic && this._sendCharacteristic) {
this._receiveCharacteristic.on(k.OBCINobleEmitterServiceRead, (data) => {
// TODO: handle all the data, both streaming and not
this._processBytes(data);
});
// if (this.options.verbose) console.log('Subscribing for data notifications');
this._receiveCharacteristic.notify(true);
this._connected = true;
this.emit(k.OBCIEmitterReady);
resolve();
} else {
reject('unable to set both receive and send characteristics!');
}
});
this._rfduinoService.discoverCharacteristics();
});
// if (this.options.verbose) console.log("Calling connect");
this._peripheral.connect((err) => {
if (err) {
if (this.options.verbose) console.log(`Unable to connect with error: ${err}`);
this._disconnected();
reject(err);
}
});
});
};
/**
* Call to add the noble event listeners.
* @private
*/
Ganglion.prototype._nobleInit = function () {
noble.on(k.OBCINobleEmitterStateChange, (state) => {
// TODO: send state change error to gui
// If the peripheral array is empty, do a scan to fill it.
if (state === k.OBCINobleStatePoweredOn) {
if (this.options.verbose) console.log('Bluetooth powered on');
this.emit(k.OBCIEmitterBlePoweredUp);
if (this.options.nobleScanOnPowerOn) {
this._nobleScanStart().catch((err) => {
console.log(err);
});
}
if (this.peripheralArray.length === 0) {
}
} else {
if (this.isSearching()) {
this._nobleScanStop().catch((err) => {
console.log(err);
});
}
}
});
noble.on(k.OBCINobleEmitterDiscover, this._nobleOnDeviceDiscoveredCallback.bind(this));
};
/**
* Event driven function called when a new device is discovered while scanning.
* @param peripheral {Object} Peripheral object from noble.
* @private
*/
Ganglion.prototype._nobleOnDeviceDiscoveredCallback = function (peripheral) {
// if(this.options.verbose) console.log(peripheral.advertisement);
this.peripheralArray.push(peripheral);
if (k.isPeripheralGanglion(peripheral)) {
if (this.options.verbose) console.log('Found ganglion!');
if (_.isUndefined(_.find(this.ganglionPeripheralArray,
(p) => {
return p.advertisement.localName === peripheral.advertisement.localName;
}))) {
this.ganglionPeripheralArray.push(peripheral);
}
this.emit(k.OBCIEmitterGanglionFound, peripheral);
}
};
Ganglion.prototype._nobleReady = function () {
return noble.state === k.OBCINobleStatePoweredOn;
};
/**
* Call to perform a scan to get a list of peripherals.
* @returns {global.Promise|Promise}
* @private
*/
Ganglion.prototype._nobleScanStart = function () {
return new Promise((resolve, reject) => {
if (this.isSearching()) return reject(k.OBCIErrorNobleAlreadyScanning);
if (!this._nobleReady()) return reject(k.OBCIErrorNobleNotInPoweredOnState);
this.peripheralArray = [];
noble.once(k.OBCINobleEmitterScanStart, () => {
if (this.options.verbose) console.log('Scan started');
this._scanning = true;
this.emit(k.OBCINobleEmitterScanStart);
resolve();
});
// Only look so simblee ble devices and allow duplicates (multiple ganglions)
// noble.startScanning([k.SimbleeUuidService], true);
noble.startScanning([], false);
});
};
/**
* Stop an active scan
* @return {global.Promise|Promise}
* @private
*/
Ganglion.prototype._nobleScanStop = function () {
return new Promise((resolve, reject) => {
if (!this.isSearching()) return reject(k.OBCIErrorNobleNotAlreadyScanning);
if (this.options.verbose) console.log(`Stopping scan`);
noble.once(k.OBCINobleEmitterScanStop, () => {
this._scanning = false;
this.emit(k.OBCINobleEmitterScanStop);
if (this.options.verbose) console.log('Scan stopped');
resolve();
});
// Stop noble from scanning
noble.stopScanning();
});
};
/**
* Route incoming data to proper functions
* @param data {Buffer} - Data buffer from noble Ganglion.
* @private
*/
Ganglion.prototype._processBytes = function (data) {
if (this.options.debug) openBCIUtils.debugBytes('<<', data);
this.lastPacket = data;
let byteId = parseInt(data[0]);
if (byteId <= k.OBCIGanglionByteId19Bit.max) {
this._processProcessSampleData(data);
} else {
switch (byteId) {
case k.OBCIGanglionByteIdMultiPacket:
this._processMultiBytePacket(data);
break;
case k.OBCIGanglionByteIdMultiPacketStop:
this._processMultiBytePacketStop(data);
break;
case k.OBCIGanglionByteIdImpedanceChannel1:
case k.OBCIGanglionByteIdImpedanceChannel2:
case k.OBCIGanglionByteIdImpedanceChannel3:
case k.OBCIGanglionByteIdImpedanceChannel4:
case k.OBCIGanglionByteIdImpedanceChannelReference:
this._processImpedanceData(data);
break;
default:
this._processOtherData(data);
}
}
};
/**
* Process an compressed packet of data.
* @param data {Buffer}
* Data packet buffer from noble.
* @private
*/
Ganglion.prototype._processCompressedData = function (data) {
// Save the packet counter
this._packetCounter = parseInt(data[0]);
// Decompress the buffer into array
if (this._packetCounter <= k.OBCIGanglionByteId18Bit.max) {
this._decompressSamples(ganglionSample.decompressDeltas18Bit(data.slice(k.OBCIGanglionPacket18Bit.dataStart, k.OBCIGanglionPacket18Bit.dataStop)));
switch (this._packetCounter % 10) {
case k.OBCIGanglionAccelAxisX:
this._accelArray[0] = this.options.sendCounts ? data.readInt8(k.OBCIGanglionPacket18Bit.auxByte - 1) : data.readInt8(k.OBCIGanglionPacket18Bit.auxByte - 1) * k.OBCIGanglionAccelScaleFactor;
break;
case k.OBCIGanglionAccelAxisY:
this._accelArray[1] = this.options.sendCounts ? data.readInt8(k.OBCIGanglionPacket18Bit.auxByte - 1) : data.readInt8(k.OBCIGanglionPacket18Bit.auxByte - 1) * k.OBCIGanglionAccelScaleFactor;
break;
case k.OBCIGanglionAccelAxisZ:
this._accelArray[2] = this.options.sendCounts ? data.readInt8(k.OBCIGanglionPacket18Bit.auxByte - 1) : data.readInt8(k.OBCIGanglionPacket18Bit.auxByte - 1) * k.OBCIGanglionAccelScaleFactor;
this.emit(k.OBCIEmitterAccelerometer, this._accelArray);
break;
default:
break;
}
const sample1 = this._buildSample(this._packetCounter * 2 - 1, this._decompressedSamples[1]);
this.emit(k.OBCIEmitterSample, sample1);
const sample2 = this._buildSample(this._packetCounter * 2, this._decompressedSamples[2]);
this.emit(k.OBCIEmitterSample, sample2);
} else {
this._decompressSamples(ganglionSample.decompressDeltas19Bit(data.slice(k.OBCIGanglionPacket19Bit.dataStart, k.OBCIGanglionPacket19Bit.dataStop)));
const sample1 = this._buildSample((this._packetCounter - 100) * 2 - 1, this._decompressedSamples[1]);
this.emit(k.OBCIEmitterSample, sample1);
const sample2 = this._buildSample((this._packetCounter - 100) * 2, this._decompressedSamples[2]);
this.emit(k.OBCIEmitterSample, sample2);
}
// Rotate the 0 position for next time
for (let i = 0; i < k.OBCINumberOfChannelsGanglion; i++) {
this._decompressedSamples[0][i] = this._decompressedSamples[2][i];
}
};
/**
* Process and emit an impedance value
* @param data {Buffer}
* @private
*/
Ganglion.prototype._processImpedanceData = function (data) {
if (this.options.debug) openBCIUtils.debugBytes('Impedance <<< ', data);
const byteId = parseInt(data[0]);
let channelNumber;
switch (byteId) {
case k.OBCIGanglionByteIdImpedanceChannel1:
channelNumber = 1;
break;
case k.OBCIGanglionByteIdImpedanceChannel2:
channelNumber = 2;
break;
case k.OBCIGanglionByteIdImpedanceChannel3:
channelNumber = 3;
break;
case k.OBCIGanglionByteIdImpedanceChannel4:
channelNumber = 4;
break;
case k.OBCIGanglionByteIdImpedanceChannelReference:
channelNumber = 0;
break;
}
let output = {
channelNumber: channelNumber,
impedanceValue: 0
};
let end = data.length;
while (_.isNaN(Number(data.slice(1, end))) && end !== 0) {
end--;
}
if (end !== 0) {
output.impedanceValue = Number(data.slice(1, end));
}
this.emit('impedance', output);
};
/**
* Used to stack multi packet buffers into the multi packet buffer. This is finally emitted when a stop packet byte id
* is received.
* @param data {Buffer}
* The multi packet buffer.
* @private
*/
Ganglion.prototype._processMultiBytePacket = function (data) {
if (this._multiPacketBuffer) {
this._multiPacketBuffer = Buffer.concat([this._multiPacketBuffer, data.slice(k.OBCIGanglionPacket19Bit.dataStart, k.OBCIGanglionPacket19Bit.dataStop)]);
} else {
this._multiPacketBuffer = data.slice(k.OBCIGanglionPacket19Bit.dataStart, k.OBCIGanglionPacket19Bit.dataStop);
}
};
/**
* Adds the `data` buffer to the multi packet buffer and emits the buffer as 'message'
* @param data {Buffer}
* The multi packet stop buffer.
* @private
*/
Ganglion.prototype._processMultiBytePacketStop = function (data) {
this._processMultiBytePacket(data);
this.emit(k.OBCIEmitterMessage, this._multiPacketBuffer);
this.destroyMultiPacketBuffer();
};
Ganglion.prototype._resetDroppedPacketSystem = function () {
this._packetCounter = -1;
this._firstPacket = true;
this._droppedPacketCounter = 0;
};
Ganglion.prototype._droppedPacket = function (droppedPacketNumber) {
this.emit(k.OBCIEmitterDroppedPacket, [droppedPacketNumber]);
this._droppedPacketCounter++;
};
/**
* Checks for dropped packets
* @param data {Buffer}
* @private
*/
Ganglion.prototype._processProcessSampleData = function(data) {
const curByteId = parseInt(data[0]);
const difByteId = curByteId - this._packetCounter;
if (this._firstPacket) {
this._firstPacket = false;
this._processRouteSampleData(data);
return;
}
// Wrap around situation
if (difByteId < 0) {
if (this._packetCounter <= k.OBCIGanglionByteId18Bit.max) {
if (this._packetCounter === k.OBCIGanglionByteId18Bit.max) {
if (curByteId !== k.OBCIGanglionByteIdUncompressed) {
this._droppedPacket(curByteId - 1);
}
} else {
let tempCounter = this._packetCounter + 1;
while (tempCounter <= k.OBCIGanglionByteId18Bit.max) {
this._droppedPacket(tempCounter);
tempCounter++;
}
}
} else if (this._packetCounter === k.OBCIGanglionByteId19Bit.max) {
if (curByteId !== k.OBCIGanglionByteIdUncompressed) {
this._droppedPacket(curByteId - 1);
}
} else {
let tempCounter = this._packetCounter + 1;
while (tempCounter <= k.OBCIGanglionByteId19Bit.max) {
this._droppedPacket(tempCounter);
tempCounter++;
}
}
} else if (difByteId > 1) {
if (this._packetCounter === k.OBCIGanglionByteIdUncompressed && curByteId === k.OBCIGanglionByteId19Bit.min) {
this._processRouteSampleData(data);
return;
} else {
let tempCounter = this._packetCounter + 1;
while (tempCounter < curByteId) {
this._droppedPacket(tempCounter);
tempCounter++;
}
}
}
this._processRouteSampleData(data);
};
Ganglion.prototype._processRouteSampleData = function(data) {
if (parseInt(data[0]) === k.OBCIGanglionByteIdUncompressed) {
this._processUncompressedData(data);
} else {
this._processCompressedData(data);
}
};
/**
* The default route when a ByteId is not recognized.
* @param data {Buffer}
* @private
*/
Ganglion.prototype._processOtherData = function (data) {
openBCIUtils.debugBytes('OtherData <<< ', data);
};
/**
* Process an uncompressed packet of data.
* @param data {Buffer}
* Data packet buffer from noble.
* @private
*/
Ganglion.prototype._processUncompressedData = function (data) {
let start = 1;
// Resets the packet counter back to zero
this._packetCounter = k.OBCIGanglionByteIdUncompressed; // used to find dropped packets
for (let i = 0; i < 4; i++) {
this._decompressedSamples[0][i] = interpret24bitAsInt32(data, start); // seed the decompressor
start += 3;
}
const newSample = this._buildSample(0, this._decompressedSamples[0]);
this.emit(k.OBCIEmitterSample, newSample);
};
module.exports = Ganglion;
function interpret24bitAsInt32 (byteArray, index) {
// little endian
var newInt = (
((0xFF & byteArray[index]) << 16) |
((0xFF & byteArray[index + 1]) << 8) |
(0xFF & byteArray[index + 2])
);
if ((newInt & 0x00800000) > 0) {
newInt |= 0xFF000000;
} else {
newInt &= 0x00FFFFFF;
}
return newInt;
}