Arquivos
OpenBCI_NodeJS/openBCISample.js
2017-03-02 13:28:19 -05:00

1197 linhas
46 KiB
JavaScript

'use strict';
var gaussian = require('gaussian');
var k = require('./openBCIConstants');
var StreamSearch = require('streamsearch');
/** Constants for interpreting the EEG data */
// Reference voltage for ADC in ADS1299.
// Set by its hardware.
const ADS1299_VREF = 4.5;
// Scale factor for aux data
const SCALE_FACTOR_ACCEL = 0.002 / Math.pow(2, 4);
// X, Y, Z
const ACCEL_NUMBER_AXIS = 3;
// Default ADS1299 gains array
var sampleModule = {
/**
* @description This takes a 33 byte packet and converts it based on the last four bits.
* 0000 - Standard OpenBCI V3 Sample Packet
* @param dataBuf {Buffer} - A 33 byte buffer
* @param channelSettingsArray (optional) - An array of channel settings that is an Array that has shape similar to the one
* calling OpenBCIConstans.channelSettingsArrayInit(). The most important rule here is that it is
* Array of objects that have key-value pair {gain:NUMBER}
* @param convertAuxToAccel (optional) {Boolean} - Do you want to convert to g's? (Defaults to true)*
* @returns {Object} - A standard sample object.
*/
parseRawPacketStandard: (dataBuf, channelSettingsArray, convertAuxToAccel) => {
const defaultChannelSettingsArray = k.channelSettingsArrayInit(k.OBCINumberOfChannelsDefault);
// channelSettingsArray is optional, defaults to CHANNEL_SETTINGS_ARRAY_DEFAULT
channelSettingsArray = channelSettingsArray || defaultChannelSettingsArray;
// By default convert to g's
if (convertAuxToAccel === undefined || convertAuxToAccel === null) convertAuxToAccel = true;
// Check to make sure data is not null.
if (k.isUndefined(dataBuf) || k.isNull(dataBuf)) {
throw new Error(k.OBCIErrorUndefinedOrNullInput);
}
// Check to make sure the buffer is the right size.
if (dataBuf.byteLength !== k.OBCIPacketSize) {
throw new Error(k.OBCIErrorInvalidByteLength);
}
// Verify the correct stop byte.
if (dataBuf[0] !== k.OBCIByteStart) {
throw new Error(k.OBCIErrorInvalidByteStart);
}
if (convertAuxToAccel) {
return parsePacketStandardAccel(dataBuf, channelSettingsArray);
} else {
return parsePacketStandardRawAux(dataBuf, channelSettingsArray);
}
},
getRawPacketType,
getFromTimePacketAccel,
getFromTimePacketTime,
getFromTimePacketRawAux,
parsePacketTimeSyncedAccel,
parsePacketTimeSyncedRawAux,
/**
* @description Mainly used by the simulator to convert a randomly generated sample into a std OpenBCI V3 Packet
* @param sample - A sample object
* @returns {Buffer}
*/
convertSampleToPacketStandard: (sample) => {
var packetBuffer = new Buffer(k.OBCIPacketSize);
packetBuffer.fill(0);
// start byte
packetBuffer[0] = k.OBCIByteStart;
// sample number
packetBuffer[1] = sample.sampleNumber;
// channel data
for (var i = 0; i < k.OBCINumberOfChannelsDefault; i++) {
var threeByteBuffer = floatTo3ByteBuffer(sample.channelData[i]);
threeByteBuffer.copy(packetBuffer, 2 + (i * 3));
}
for (var j = 0; j < 3; j++) {
var twoByteBuffer = floatTo2ByteBuffer(sample.auxData[j]);
twoByteBuffer.copy(packetBuffer, (k.OBCIPacketSize - 1 - 6) + (i * 2));
}
// stop byte
packetBuffer[k.OBCIPacketSize - 1] = k.OBCIByteStop;
return packetBuffer;
},
/**
* @description Mainly used by the simulator to convert a randomly generated sample into a std OpenBCI V3 Packet
* @param sample - A sample object
* @param rawAux {Buffer} - A 6 byte long buffer to insert into raw buffer
* @returns {Buffer} - A 33 byte long buffer
*/
convertSampleToPacketRawAux: (sample, rawAux) => {
var packetBuffer = new Buffer(k.OBCIPacketSize);
packetBuffer.fill(0);
// start byte
packetBuffer[0] = k.OBCIByteStart;
// sample number
packetBuffer[1] = sample.sampleNumber;
// channel data
for (var i = 0; i < k.OBCINumberOfChannelsDefault; i++) {
var threeByteBuffer = floatTo3ByteBuffer(sample.channelData[i]);
threeByteBuffer.copy(packetBuffer, 2 + (i * 3));
}
// Write the raw aux bytes
rawAux.copy(packetBuffer, 26);
// stop byte
packetBuffer[k.OBCIPacketSize - 1] = makeTailByteFromPacketType(k.OBCIStreamPacketStandardRawAux);
return packetBuffer;
},
/**
* @description Mainly used by the simulator to convert a randomly generated sample into an accel time sync set buffer
* @param sample {Buffer} - A sample object
* @param time {Number} - The time to inject into the sample.
* @returns {Buffer} - A time sync accel packet
*/
convertSampleToPacketAccelTimeSyncSet: (sample, time) => {
var buf = convertSampleToPacketAccelTimeSynced(sample, time);
buf[k.OBCIPacketPositionStopByte] = makeTailByteFromPacketType(k.OBCIStreamPacketAccelTimeSyncSet);
return buf;
},
/**
* @description Mainly used by the simulator to convert a randomly generated sample into an accel time synced buffer
* @param sample {Buffer} - A sample object
* @param time {Number} - The time to inject into the sample.
* @returns {Buffer} - A time sync accel packet
*/
convertSampleToPacketAccelTimeSynced,
/**
* @description Mainly used by the simulator to convert a randomly generated sample into a raw aux time sync set packet
* @param sample {Buffer} - A sample object
* @param time {Number} - The time to inject into the sample.
* @param rawAux {Buffer} - 2 byte buffer to inject into sample
* @returns {Buffer} - A time sync raw aux packet
*/
convertSampleToPacketRawAuxTimeSyncSet: (sample, time, rawAux) => {
var buf = convertSampleToPacketRawAuxTimeSynced(sample, time, rawAux);
buf[k.OBCIPacketPositionStopByte] = makeTailByteFromPacketType(k.OBCIStreamPacketRawAuxTimeSyncSet);
return buf;
},
convertSampleToPacketRawAuxTimeSynced,
debugPrettyPrint: (sample) => {
if (sample === null || sample === undefined) {
console.log('== Sample is undefined ==');
} else {
console.log('-- Sample --');
console.log('---- Start Byte: ' + sample.startByte);
console.log('---- Sample Number: ' + sample.sampleNumber);
for (var i = 0; i < 8; i++) {
console.log('---- Channel Data ' + (i + 1) + ': ' + sample.channelData[i]);
}
if (sample.accelData) {
for (var j = 0; j < 3; j++) {
console.log('---- Accel Data ' + j + ': ' + sample.accelData[j]);
}
}
if (sample.auxData) {
console.log('---- Aux Data ' + sample.auxData);
}
console.log('---- Stop Byte: ' + sample.stopByte);
}
},
samplePrintHeader: () => {
return (
'All voltages in Volts!' +
'sampleNumber, channel1, channel2, channel3, channel4, channel5, channel6, channel7, channel8, aux1, aux2, aux3\n');
},
samplePrintLine: sample => {
return new Promise((resolve, reject) => {
if (sample === null || sample === undefined) reject('undefined sample');
resolve(
sample.sampleNumber + ',' +
sample.channelData[0].toFixed(8) + ',' +
sample.channelData[1].toFixed(8) + ',' +
sample.channelData[2].toFixed(8) + ',' +
sample.channelData[3].toFixed(8) + ',' +
sample.channelData[4].toFixed(8) + ',' +
sample.channelData[5].toFixed(8) + ',' +
sample.channelData[6].toFixed(8) + ',' +
sample.channelData[7].toFixed(8) + ',' +
sample.auxData[0].toFixed(8) + ',' +
sample.auxData[1].toFixed(8) + ',' +
sample.auxData[2].toFixed(8) + '\n'
);
});
},
floatTo3ByteBuffer,
floatTo2ByteBuffer,
interpret16bitAsInt32: twoByteBuffer => {
var prefix = 0;
if (twoByteBuffer[0] > 127) {
// console.log('\t\tNegative number')
prefix = 65535; // 0xFFFF
}
return (prefix << 16) | (twoByteBuffer[0] << 8) | twoByteBuffer[1];
},
interpret24bitAsInt32: threeByteBuffer => {
var prefix = 0;
if (threeByteBuffer[0] > 127) {
// console.log('\t\tNegative number')
prefix = 255;
}
return (prefix << 24) | (threeByteBuffer[0] << 16) | (threeByteBuffer[1] << 8) | threeByteBuffer[2];
},
impedanceArray: numberOfChannels => {
var impedanceArray = [];
for (var i = 0; i < numberOfChannels; i++) {
impedanceArray.push(newImpedanceObject(i + 1));
}
return impedanceArray;
},
impedanceObject: newImpedanceObject,
impedanceSummarize: singleInputObject => {
if (singleInputObject.raw > k.OBCIImpedanceThresholdBadMax) { // The case for no load (super high impedance)
singleInputObject.text = k.OBCIImpedanceTextNone;
} else {
singleInputObject.text = k.getTextForRawImpedance(singleInputObject.raw); // Get textual impedance
}
},
newSample,
/**
* @description Create a configurable function to return samples for a simulator. This implements 1/f filtering injection to create more brain like data.
* @param numberOfChannels {Number} - The number of channels in the sample... either 8 or 16
* @param sampleRateHz {Number} - The sample rate
* @param injectAlpha {Boolean} (optional) - True if you want to inject noise
* @param lineNoise {String} (optional) - A string that can be either:
* `60Hz` - 60Hz line noise (Default) (ex. __United States__)
* `50Hz` - 50Hz line noise (ex. __Europe__)
* `none` - Do not inject line noise.
*
* @returns {Function}
*/
randomSample: (numberOfChannels, sampleRateHz, injectAlpha, lineNoise) => {
const distribution = gaussian(0, 1);
const sineWaveFreqHz10 = 10;
const sineWaveFreqHz50 = 50;
const sineWaveFreqHz60 = 60;
const uVolts = 1000000;
var sinePhaseRad = new Array(numberOfChannels + 1); // prevent index error with '+1'
sinePhaseRad.fill(0);
var auxData = [0, 0, 0];
var accelCounter = 0;
// With 250Hz, every 10 samples, with 125Hz, every 5...
var samplesPerAccelRate = Math.floor(sampleRateHz / 25); // best to make this an integer
if (samplesPerAccelRate < 1) samplesPerAccelRate = 1;
// Init arrays to hold coefficients for each channel and init to 0
// This gives the 1/f filter memory on each iteration
var b0 = new Array(numberOfChannels).fill(0);
var b1 = new Array(numberOfChannels).fill(0);
var b2 = new Array(numberOfChannels).fill(0);
/**
* @description Use a 1/f filter
* @param previousSampleNumber {Number} - The previous sample number
*/
return previousSampleNumber => {
var sample = newSample();
var whiteNoise;
for (var i = 0; i < numberOfChannels; i++) { // channels are 0 indexed
// This produces white noise
whiteNoise = distribution.ppf(Math.random()) * Math.sqrt(sampleRateHz / 2) / uVolts;
switch (i) {
case 0: // Add 10Hz signal to channel 1... brainy
case 1:
if (injectAlpha) {
sinePhaseRad[i] += 2 * Math.PI * sineWaveFreqHz10 / sampleRateHz;
if (sinePhaseRad[i] > 2 * Math.PI) {
sinePhaseRad[i] -= 2 * Math.PI;
}
whiteNoise += (5 * Math.SQRT2 * Math.sin(sinePhaseRad[i])) / uVolts;
}
break;
default:
if (lineNoise === k.OBCISimulatorLineNoiseHz60) {
// If we're in murica we want to add 60Hz line noise
sinePhaseRad[i] += 2 * Math.PI * sineWaveFreqHz60 / sampleRateHz;
if (sinePhaseRad[i] > 2 * Math.PI) {
sinePhaseRad[i] -= 2 * Math.PI;
}
whiteNoise += (8 * Math.SQRT2 * Math.sin(sinePhaseRad[i])) / uVolts;
} else if (lineNoise === k.OBCISimulatorLineNoiseHz50) {
// add 50Hz line noise if we are not in america
sinePhaseRad[i] += 2 * Math.PI * sineWaveFreqHz50 / sampleRateHz;
if (sinePhaseRad[i] > 2 * Math.PI) {
sinePhaseRad[i] -= 2 * Math.PI;
}
whiteNoise += (8 * Math.SQRT2 * Math.sin(sinePhaseRad[i])) / uVolts;
}
}
/**
* See http://www.firstpr.com.au/dsp/pink-noise/ section "Filtering white noise to make it pink"
*/
b0[i] = 0.99765 * b0[i] + whiteNoise * 0.0990460;
b1[i] = 0.96300 * b1[i] + whiteNoise * 0.2965164;
b2[i] = 0.57000 * b2[i] + whiteNoise * 1.0526913;
sample.channelData[i] = b0[i] + b1[i] + b2[i] + whiteNoise * 0.1848;
}
if (previousSampleNumber === 255) {
sample.sampleNumber = 0;
} else {
sample.sampleNumber = previousSampleNumber + 1;
}
/**
* Sample rate of accelerometer is 25Hz... when the accelCounter hits the relative sample rate of the accel
* we will output a new accel value. The approach will be to consider that Z should be about 1 and X and Y
* should be somewhere around 0.
*/
if (accelCounter === samplesPerAccelRate) {
// Initialize a new array
var accelArray = [0, 0, 0];
// Calculate X
accelArray[0] = (Math.random() * 0.1 * (Math.random() > 0.5 ? -1 : 1));
// Calculate Y
accelArray[1] = (Math.random() * 0.1 * (Math.random() > 0.5 ? -1 : 1));
// Calculate Z, this is around 1
accelArray[2] = 1 - ((Math.random() * 0.4) * (Math.random() > 0.5 ? -1 : 1));
// Store the newly calculated value
sample.auxData = accelArray;
// Reset the counter
accelCounter = 0;
} else {
// Increment counter
accelCounter++;
// Store the default value
sample.auxData = auxData;
}
return sample;
};
},
scaleFactorAux: SCALE_FACTOR_ACCEL,
k,
/**
* Calculate the impedance
* @param sample {Object} - Standard sample
* @param impedanceTest {Object} - Impedance Object from openBCIBoard.js
* @return {null | Object} - Null if not enough samples have passed to calculate an accurate
*/
impedanceCalculateArray: (sample, impedanceTest) => {
impedanceTest.buffer.push(sample.channelData);
impedanceTest.count++;
if (impedanceTest.count >= impedanceTest.window) {
let output = [];
for (let i = 0; i < sample.channelData.length; i++) {
let max = 0.0; // sumSquared
for (let j = 0; j < impedanceTest.window; j++) {
if (impedanceTest.buffer[i][j] > max) {
max = impedanceTest.buffer[i][j];
}
}
let min = 0.0;
for (let j = 0; j < impedanceTest.window; j++) {
if (impedanceTest.buffer[i][j] < min) {
min = impedanceTest.buffer[i][j];
}
}
const vP2P = max - min; // peak to peak
output.push(vP2P / 2 / k.OBCILeadOffDriveInAmps);
}
impedanceTest.count = 0;
return output;
}
return null;
},
impedanceTestObjDefault: (impedanceTestObj) => {
let newObj = impedanceTestObj || {};
newObj['active'] = false;
newObj['buffer'] = [];
newObj['count'] = 0;
newObj['isTestingPInput'] = false;
newObj['isTestingNInput'] = false;
newObj['onChannel'] = 0;
newObj['sampleNumber'] = 0;
newObj['continuousMode'] = false;
newObj['impedanceForChannel'] = 0;
newObj['window'] = 40;
return newObj;
},
samplePacket: sampleNumber => {
return new Buffer([0xA0, sampleNumberNormalize(sampleNumber), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 0, 0, 1, 0, 2, makeTailByteFromPacketType(k.OBCIStreamPacketStandardAccel)]);
},
samplePacketReal: sampleNumber => {
return new Buffer([0xA0, sampleNumberNormalize(sampleNumber), 0x8F, 0xF2, 0x40, 0x8F, 0xDF, 0xF4, 0x90, 0x2B, 0xB6, 0x8F, 0xBF, 0xBF, 0x7F, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0x94, 0x25, 0x34, 0x20, 0xB6, 0x7D, 0, 0xE0, 0, 0xE0, 0x0F, 0x70, makeTailByteFromPacketType(k.OBCIStreamPacketStandardAccel)]);
},
samplePacketStandardRawAux: sampleNumber => {
return new Buffer([0xA0, sampleNumberNormalize(sampleNumber), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 1, 2, 3, 4, 5, makeTailByteFromPacketType(k.OBCIStreamPacketStandardRawAux)]);
},
samplePacketAccelTimeSyncSet: sampleNumber => {
return new Buffer([0xA0, sampleNumberNormalize(sampleNumber), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 1, 0, 0, 0, 1, makeTailByteFromPacketType(k.OBCIStreamPacketAccelTimeSyncSet)]);
},
samplePacketAccelTimeSynced: sampleNumber => {
return new Buffer([0xA0, sampleNumberNormalize(sampleNumber), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 1, 0, 0, 0, 1, makeTailByteFromPacketType(k.OBCIStreamPacketAccelTimeSynced)]);
},
samplePacketRawAuxTimeSyncSet: sampleNumber => {
return new Buffer([0xA0, sampleNumberNormalize(sampleNumber), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0x00, 0x01, 0, 0, 0, 1, makeTailByteFromPacketType(k.OBCIStreamPacketRawAuxTimeSyncSet)]);
},
samplePacketRawAuxTimeSynced: sampleNumber => {
return new Buffer([0xA0, sampleNumberNormalize(sampleNumber), 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0x00, 0x01, 0, 0, 0, 1, makeTailByteFromPacketType(k.OBCIStreamPacketRawAuxTimeSynced)]);
},
samplePacketUserDefined: () => {
return new Buffer([0xA0, 0x00, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, makeTailByteFromPacketType(k.OBCIStreamPacketUserDefinedType)]);
},
makeDaisySampleObject,
getChannelDataArray,
isEven,
isOdd,
countADSPresent,
doesBufferHaveEOT,
findV2Firmware,
isFailureInBuffer,
isSuccessInBuffer,
isTimeSyncSetConfirmationInBuffer,
makeTailByteFromPacketType,
isStopByte,
newSyncObject,
stripToEOTBuffer,
/**
* @description Checks to make sure the previous sample number is one less
* then the new sample number. Takes into account sample numbers wrapping
* around at 255.
* @param `previousSampleNumber` {Number} - An integer number of the previous
* sample number.
* @param `newSampleNumber` {Number} - An integer number of the new sample
* number.
* @returns {Array} - Returns null if there is no dropped packets, otherwise,
* or on a missed packet, an array of their packet numbers is returned.
*/
droppedPacketCheck: (previousSampleNumber, newSampleNumber) => {
if (previousSampleNumber === k.OBCISampleNumberMax && newSampleNumber === 0) {
return null;
}
if (newSampleNumber - previousSampleNumber === 1) {
return null;
}
var missedPacketArray = [];
if (previousSampleNumber > newSampleNumber) {
var numMised = k.OBCISampleNumberMax - previousSampleNumber;
for (var i = 0; i < numMised; i++) {
missedPacketArray.push(previousSampleNumber + i + 1);
}
previousSampleNumber = -1;
}
for (var j = 1; j < (newSampleNumber - previousSampleNumber); j++) {
missedPacketArray.push(previousSampleNumber + j);
}
return missedPacketArray;
}
};
module.exports = sampleModule;
function newImpedanceObject (channelNumber) {
return {
channel: channelNumber,
P: {
raw: -1,
text: k.OBCIImpedanceTextInit
},
N: {
raw: -1,
text: k.OBCIImpedanceTextInit
}
};
}
function newSyncObject () {
return {
boardTime: 0,
correctedTransmissionTime: false,
error: null,
timeSyncSent: 0,
timeSyncSentConfirmation: 0,
timeSyncSetPacket: 0,
timeRoundTrip: 0,
timeTransmission: 0,
timeOffset: 0,
timeOffsetMaster: 0,
valid: false
};
}
/**
* @description This method parses a 33 byte OpenBCI V3 packet and converts to a sample object
* @param dataBuf - 33 byte packet that has bytes:
* 0:[startByte] | 1:[sampleNumber] | 2:[Channel-1.1] | 3:[Channel-1.2] | 4:[Channel-1.3] | 5:[Channel-2.1] | 6:[Channel-2.2] | 7:[Channel-2.3] | 8:[Channel-3.1] | 9:[Channel-3.2] | 10:[Channel-3.3] | 11:[Channel-4.1] | 12:[Channel-4.2] | 13:[Channel-4.3] | 14:[Channel-5.1] | 15:[Channel-5.2] | 16:[Channel-5.3] | 17:[Channel-6.1] | 18:[Channel-6.2] | 19:[Channel-6.3] | 20:[Channel-7.1] | 21:[Channel-7.2] | 22:[Channel-7.3] | 23:[Channel-8.1] | 24:[Channel-8.2] | 25:[Channel-8.3] | 26:[Aux-1.1] | 27:[Aux-1.2] | 28:[Aux-2.1] | 29:[Aux-2.2] | 30:[Aux-3.1] | 31:[Aux-3.2] | 32:StopByte
* @param channelSettingsArray {Array} - An array of channel settings that is an Array that has shape similar to the one
* calling OpenBCIConstans.channelSettingsArrayInit(). The most important rule here is that it is
* Array of objects that have key-value pair {gain:NUMBER}
* @returns {object} Sample.
* `sample` Object with:
* {
* channelData: {Array}, // of floats
* accelData: {Array}, // of floats of accel data
* sampleNumber: {Number} // The sample number
* }
*/
function parsePacketStandardAccel (dataBuf, channelSettingsArray) {
var sampleObject = {};
sampleObject.accelData = getDataArrayAccel(dataBuf.slice(k.OBCIPacketPositionStartAux, k.OBCIPacketPositionStopAux + 1));
sampleObject.channelData = getChannelDataArray(dataBuf, channelSettingsArray);
if (k.getVersionNumber(process.version) >= 6) {
// From introduced in node version 6.x.x
sampleObject.auxData = Buffer.from(dataBuf.slice(k.OBCIPacketPositionStartAux, k.OBCIPacketPositionStopAux + 1));
} else {
sampleObject.auxData = new Buffer(dataBuf.slice(k.OBCIPacketPositionStartAux, k.OBCIPacketPositionStopAux + 1));
}
// Get the sample number
sampleObject.sampleNumber = dataBuf[k.OBCIPacketPositionSampleNumber];
// Get the start byte
sampleObject.startByte = dataBuf[0];
// Get the stop byte
sampleObject.stopByte = dataBuf[k.OBCIPacketPositionStopByte];
return sampleObject;
}
/**
* @description This method parses a 33 byte OpenBCI V3 packet and converts to a sample object
* @param dataBuf - 33 byte packet that has bytes:
* 0:[startByte] | 1:[sampleNumber] | 2:[Channel-1.1] | 3:[Channel-1.2] | 4:[Channel-1.3] | 5:[Channel-2.1] | 6:[Channel-2.2] | 7:[Channel-2.3] | 8:[Channel-3.1] | 9:[Channel-3.2] | 10:[Channel-3.3] | 11:[Channel-4.1] | 12:[Channel-4.2] | 13:[Channel-4.3] | 14:[Channel-5.1] | 15:[Channel-5.2] | 16:[Channel-5.3] | 17:[Channel-6.1] | 18:[Channel-6.2] | 19:[Channel-6.3] | 20:[Channel-7.1] | 21:[Channel-7.2] | 22:[Channel-7.3] | 23:[Channel-8.1] | 24:[Channel-8.2] | 25:[Channel-8.3] | 26:[Aux-1.1] | 27:[Aux-1.2] | 28:[Aux-2.1] | 29:[Aux-2.2] | 30:[Aux-3.1] | 31:[Aux-3.2] | 32:StopByte
* @param channelSettingsArray - An array of channel settings that is an Array that has shape similar to the one
* calling OpenBCIConstans.channelSettingsArrayInit(). The most important rule here is that it is
* Array of objects that have key-value pair {gain:NUMBER}
* @returns {Promise} - Fulfilled with a sample object that has form:
* {
* channelData: Array of floats
* auxData: 6 byte long buffer of raw aux data
* sampleNumber: a Number that is the sample
* }
*/
function parsePacketStandardRawAux (dataBuf, channelSettingsArray) {
var sampleObject = {};
// Store the channel data
sampleObject.channelData = getChannelDataArray(dataBuf, channelSettingsArray);
// Slice the buffer for the aux data
if (k.getVersionNumber(process.version) >= 6) {
// From introduced in node version 6.x.x
sampleObject.auxData = Buffer.from(dataBuf.slice(k.OBCIPacketPositionStartAux, k.OBCIPacketPositionStopAux + 1));
} else {
sampleObject.auxData = new Buffer(dataBuf.slice(k.OBCIPacketPositionStartAux, k.OBCIPacketPositionStopAux + 1));
}
// Get the sample number
sampleObject.sampleNumber = dataBuf[k.OBCIPacketPositionSampleNumber];
// Get the start byte
sampleObject.startByte = dataBuf[0];
// Get the stop byte
sampleObject.stopByte = dataBuf[k.OBCIPacketPositionStopByte];
return sampleObject;
}
/**
* @description Grabs an accel value from a raw but time synced packet. Important that this utilizes the fact that:
* X axis data is sent with every sampleNumber % 10 === 0
* Y axis data is sent with every sampleNumber % 10 === 1
* Z axis data is sent with every sampleNumber % 10 === 2
* @param dataBuf {Buffer} - The 33byte raw time synced accel packet
* @param channelSettingsArray {Array} - An array of channel settings that is an Array that has shape similar to the one
* calling OpenBCIConstans.channelSettingsArrayInit(). The most important rule here is that it is
* Array of objects that have key-value pair {gain:NUMBER}
* @param boardOffsetTime {Number} - The difference between board time and current time calculated with sync methods.
* @param accelArray {Array} - A 3 element array that allows us to have inter packet memory of x and y axis data and emit only on the z axis packets.
* @returns {Object} - A sample object. NOTE: Only has accelData if this is a Z axis packet.
*/
function parsePacketTimeSyncedAccel (dataBuf, channelSettingsArray, boardOffsetTime, accelArray) {
// Ths packet has 'A0','00'....,'AA','AA','FF','FF','FF','FF','C4'
// where the 'AA's form an accel 16bit num and 'FF's form a 32 bit time in ms
// The sample object we are going to build
var sampleObject = {};
// Get the sample number
sampleObject.sampleNumber = dataBuf[k.OBCIPacketPositionSampleNumber];
// Get the start byte
sampleObject.startByte = dataBuf[0];
// Get the stop byte
sampleObject.stopByte = dataBuf[k.OBCIPacketPositionStopByte];
// Get the board time
sampleObject.boardTime = getFromTimePacketTime(dataBuf);
sampleObject.timeStamp = sampleObject.boardTime + boardOffsetTime;
// Extract the aux data
sampleObject.auxData = getFromTimePacketRawAux(dataBuf);
// Grab the accelData only if `getFromTimePacketAccel` returns true.
if (getFromTimePacketAccel(dataBuf, accelArray)) {
sampleObject.accelData = accelArray;
}
// Grab the channel data.
sampleObject.channelData = getChannelDataArray(dataBuf, channelSettingsArray);
return sampleObject;
}
/**
* @description Grabs an accel value from a raw but time synced packet. Important that this utilizes the fact that:
* X axis data is sent with every sampleNumber % 10 === 0
* Y axis data is sent with every sampleNumber % 10 === 1
* Z axis data is sent with every sampleNumber % 10 === 2
* @param dataBuf {Buffer} - The 33byte raw time synced accel packet
* @param channelSettingsArray {Array} - An array of channel settings that is an Array that has shape similar to the one
* calling OpenBCIConstans.channelSettingsArrayInit(). The most important rule here is that it is
* Array of objects that have key-value pair {gain:NUMBER}
* @param boardOffsetTime {Number} - The difference between board time and current time calculated with sync methods.
* @returns {Object} - A sample object. NOTE: The aux data is placed in a 2 byte buffer
*/
function parsePacketTimeSyncedRawAux (dataBuf, channelSettingsArray, boardOffsetTime) {
// Ths packet has 'A0','00'....,'AA','AA','FF','FF','FF','FF','C4'
// where the 'AA's form an accel 16bit num and 'FF's form a 32 bit time in ms
if (dataBuf.byteLength !== k.OBCIPacketSize) {
throw new Error(k.OBCIErrorInvalidByteLength);
}
// The sample object we are going to build
var sampleObject = {};
// Get the sample number
sampleObject.sampleNumber = dataBuf[k.OBCIPacketPositionSampleNumber];
// Get the start byte
sampleObject.startByte = dataBuf[0];
// Get the stop byte
sampleObject.stopByte = dataBuf[k.OBCIPacketPositionStopByte];
// Get the board time
sampleObject.boardTime = getFromTimePacketTime(dataBuf);
sampleObject.timeStamp = sampleObject.boardTime + boardOffsetTime;
// Extract the aux data
sampleObject.auxData = getFromTimePacketRawAux(dataBuf);
// Grab the channel data.
sampleObject.channelData = getChannelDataArray(dataBuf, channelSettingsArray);
return sampleObject;
}
/**
* @description Extract a time from a time packet in ms.
* @param dataBuf - A raw packet with 33 bytes of data
* @returns {Number} - Board time in milli seconds
* @author AJ Keller (@pushtheworldllc)
*/
function getFromTimePacketTime (dataBuf) {
// Ths packet has 'A0','00'....,'00','00','FF','FF','FF','FF','C3' where the 'FF's are times
const lastBytePosition = k.OBCIPacketSize - 1; // This is 33, but 0 indexed would be 32 minus 1 for the stop byte and another two for the aux channel or the
if (dataBuf.byteLength !== k.OBCIPacketSize) {
throw new Error(k.OBCIErrorInvalidByteLength);
} else {
// Grab the time from the packet
return dataBuf.readUInt32BE(lastBytePosition - k.OBCIStreamPacketTimeByteSize);
}
}
/**
* @description Grabs an accel value from a raw but time synced packet. Important that this utilizes the fact that:
* X axis data is sent with every sampleNumber % 10 === 7
* Y axis data is sent with every sampleNumber % 10 === 8
* Z axis data is sent with every sampleNumber % 10 === 9
* @param dataBuf {Buffer} - The 33byte raw time synced accel packet
* @param accelArray {Array} - A 3 element array that allows us to have inter packet memory of x and y axis data and emit only on the z axis packets.
* @returns {boolean} - A boolean that is true only when the accel array is ready to be emitted... i.e. when this is a Z axis packet
*/
function getFromTimePacketAccel (dataBuf, accelArray) {
const accelNumBytes = 2;
const lastBytePosition = k.OBCIPacketSize - 1 - k.OBCIStreamPacketTimeByteSize - accelNumBytes; // This is 33, but 0 indexed would be 32 minus
if (dataBuf.byteLength !== k.OBCIPacketSize) {
throw new Error(k.OBCIErrorInvalidByteLength);
}
var sampleNumber = dataBuf[k.OBCIPacketPositionSampleNumber];
switch (sampleNumber % 10) { // The accelerometer is on a 25Hz sample rate, so every ten channel samples, we can get new data
case k.OBCIAccelAxisX:
accelArray[0] = sampleModule.interpret16bitAsInt32(dataBuf.slice(lastBytePosition, lastBytePosition + 2)) * SCALE_FACTOR_ACCEL; // slice is not inclusive on the right
return false;
case k.OBCIAccelAxisY:
accelArray[1] = sampleModule.interpret16bitAsInt32(dataBuf.slice(lastBytePosition, lastBytePosition + 2)) * SCALE_FACTOR_ACCEL; // slice is not inclusive on the right
return false;
case k.OBCIAccelAxisZ:
accelArray[2] = sampleModule.interpret16bitAsInt32(dataBuf.slice(lastBytePosition, lastBytePosition + 2)) * SCALE_FACTOR_ACCEL; // slice is not inclusive on the right
return true;
default:
return false;
}
}
/**
* @description Grabs a raw aux value from a raw but time synced packet.
* @param dataBuf {Buffer} - The 33byte raw time synced raw aux packet
* @returns {Promise} - Fulfills a 2 byte buffer
*/
function getFromTimePacketRawAux (dataBuf) {
if (dataBuf.byteLength !== k.OBCIPacketSize) {
throw new Error(k.OBCIErrorInvalidByteLength);
}
if (k.getVersionNumber(process.version) >= 6) {
return Buffer.from(dataBuf.slice(k.OBCIPacketPositionTimeSyncAuxStart, k.OBCIPacketPositionTimeSyncAuxStop));
} else {
return new Buffer(dataBuf.slice(k.OBCIPacketPositionTimeSyncAuxStart, k.OBCIPacketPositionTimeSyncAuxStop));
}
}
/**
* @description Takes a buffer filled with 3 16 bit integers from an OpenBCI device and converts based on settings
* of the MPU, values are in ?
* @param dataBuf - Buffer that is 6 bytes long
* @returns {Array} - Array of floats 3 elements long
* @author AJ Keller (@pushtheworldllc)
*/
function getDataArrayAccel (dataBuf) {
var accelData = [];
for (var i = 0; i < ACCEL_NUMBER_AXIS; i++) {
var index = i * 2;
accelData.push(sampleModule.interpret16bitAsInt32(dataBuf.slice(index, index + 2)) * SCALE_FACTOR_ACCEL);
}
return accelData;
}
/**
* @description Takes a buffer filled with 24 bit signed integers from an OpenBCI device with gain settings in
* channelSettingsArray[index].gain and converts based on settings of ADS1299... spits out an
* array of floats in VOLTS
* @param dataBuf {Buffer} - Buffer with 33 bit signed integers, number of elements is same as channelSettingsArray.length * 3
* @param channelSettingsArray {Array} - The channel settings array, see OpenBCIConstants.channelSettingsArrayInit() for specs
* @returns {Array} - Array filled with floats for each channel's voltage in VOLTS
* @author AJ Keller (@pushtheworldllc)
*/
function getChannelDataArray (dataBuf, channelSettingsArray) {
if (!Array.isArray(channelSettingsArray)) {
throw new Error('Error [getChannelDataArray]: Channel Settings must be an array!');
}
var channelData = [];
// Grab the sample number from the buffer
var sampleNumber = dataBuf[k.OBCIPacketPositionSampleNumber];
var daisy = channelSettingsArray.length > k.OBCINumberOfChannelsDefault;
// Channel data arrays are always 8 long
for (var i = 0; i < k.OBCINumberOfChannelsDefault; i++) {
if (!channelSettingsArray[i].hasOwnProperty('gain')) {
throw new Error(`Error [getChannelDataArray]: Invalid channel settings object at index ${i}`);
}
if (!k.isNumber(channelSettingsArray[i].gain)) {
throw new Error('Error [getChannelDataArray]: Property gain of channelSettingsObject not or type Number');
}
var scaleFactor = 0;
if (isEven(sampleNumber) && daisy) {
scaleFactor = ADS1299_VREF / channelSettingsArray[i + k.OBCINumberOfChannelsDefault].gain / (Math.pow(2, 23) - 1);
} else {
scaleFactor = ADS1299_VREF / channelSettingsArray[i].gain / (Math.pow(2, 23) - 1);
}
// Convert the three byte signed integer and convert it
channelData.push(scaleFactor * sampleModule.interpret24bitAsInt32(dataBuf.slice((i * 3) + k.OBCIPacketPositionChannelDataStart, (i * 3) + k.OBCIPacketPositionChannelDataStart + 3)));
}
return channelData;
}
function getRawPacketType (stopByte) {
return stopByte & 0xF;
}
/**
* @description This method is useful for normalizing sample numbers for fake sample packets. This is intended to be
* useful for the simulator and automated testing.
* @param sampleNumber {Number} - The sample number you want to assign to the packet
* @returns {Number} - The normalized input `sampleNumber` between 0-255
*/
function sampleNumberNormalize (sampleNumber) {
if (sampleNumber || sampleNumber === 0) {
if (sampleNumber > 255) {
sampleNumber = 255;
}
} else {
sampleNumber = 0x45;
}
return sampleNumber;
}
function newSample (sampleNumber) {
if (sampleNumber || sampleNumber === 0) {
if (sampleNumber > 255) {
sampleNumber = 255;
}
} else {
sampleNumber = 0;
}
return {
startByte: k.OBCIByteStart,
sampleNumber: sampleNumber,
channelData: [],
accelData: [],
auxData: null,
stopByte: k.OBCIByteStop,
boardTime: 0,
timeStamp: 0
};
}
/**
* @description Convert float number into three byte buffer. This is the opposite of .interpret24bitAsInt32()
* @param float - The number you want to convert
* @returns {Buffer} - 3-byte buffer containing the float
*/
function floatTo3ByteBuffer (float) {
var intBuf = new Buffer(3); // 3 bytes for 24 bits
intBuf.fill(0); // Fill the buffer with 0s
var temp = float / (ADS1299_VREF / 24 / (Math.pow(2, 23) - 1)); // Convert to counts
temp = Math.floor(temp); // Truncate counts number
// Move into buffer
intBuf[2] = temp & 255;
intBuf[1] = (temp & (255 << 8)) >> 8;
intBuf[0] = (temp & (255 << 16)) >> 16;
return intBuf;
}
/**
* @description Convert float number into three byte buffer. This is the opposite of .interpret24bitAsInt32()
* @param float - The number you want to convert
* @returns {buffer} - 3-byte buffer containing the float
*/
function floatTo2ByteBuffer (float) {
var intBuf = new Buffer(2); // 2 bytes for 16 bits
intBuf.fill(0); // Fill the buffer with 0s
var temp = float / SCALE_FACTOR_ACCEL; // Convert to counts
temp = Math.floor(temp); // Truncate counts number
// console.log('Num: ' + temp)
// Move into buffer
intBuf[1] = temp & 255;
intBuf[0] = (temp & (255 << 8)) >> 8;
return intBuf;
}
/**
* @description Used to make one sample object from two sample objects. The sample number of the new daisy sample will
* be the upperSampleObject's sample number divded by 2. This allows us to preserve consecutive sample numbers that
* flip over at 127 instead of 255 for an 8 channel. The daisySampleObject will also have one `channelData` array
* with 16 elements inside it, with the lowerSampleObject in the lower indices and the upperSampleObject in the
* upper set of indices. The auxData from both channels shall be captured in an object called `auxData` which
* contains two arrays referenced by keys `lower` and `upper` for the `lowerSampleObject` and `upperSampleObject`,
* respectively. The timestamps shall be averaged and moved into an object called `timestamp`. Further, the
* un-averaged timestamps from the `lowerSampleObject` and `upperSampleObject` shall be placed into an object called
* `_timestamps` which shall contain two keys `lower` and `upper` which contain the original timestamps for their
* respective sampleObjects.
* @param lowerSampleObject {Object} - Lower 8 channels with odd sample number
* @param upperSampleObject {Object} - Upper 8 channels with even sample number
* @returns {Object} - The new merged daisy sample object
*/
function makeDaisySampleObject (lowerSampleObject, upperSampleObject) {
var daisySampleObject = {};
daisySampleObject['channelData'] = lowerSampleObject.channelData.concat(upperSampleObject.channelData);
daisySampleObject['sampleNumber'] = Math.floor(upperSampleObject.sampleNumber / 2);
daisySampleObject['auxData'] = {
'lower': lowerSampleObject.auxData,
'upper': upperSampleObject.auxData
};
daisySampleObject['timestamp'] = (lowerSampleObject.timestamp + upperSampleObject.timestamp) / 2;
daisySampleObject['_timestamps'] = {
'lower': lowerSampleObject.timestamp,
'upper': upperSampleObject.timestamp
};
if (lowerSampleObject.accelData) {
daisySampleObject['accelData'] = lowerSampleObject.accelData;
} else if (upperSampleObject.accelData) {
daisySampleObject['accelData'] = upperSampleObject.accelData;
}
return daisySampleObject;
}
/**
* @description Used to test a number to see if it is even
* @param a {Number} - The number to test
* @returns {boolean} - True if `a` is even
*/
function isEven (a) {
return a % 2 === 0;
}
/**
* @description Used to test a number to see if it is odd
* @param a {Number} - The number to test
* @returns {boolean} - True if `a` is odd
*/
function isOdd (a) {
return a % 2 === 1;
}
/**
* @description Since we know exactly what this input will look like (See the hardware firmware) we can program this
* function with prior knowledge.
* @param dataBuffer {Buffer} - The buffer you want to parse.
* @return {Number} - The number of "ADS1299" present in the `dataBuffer`
*/
function countADSPresent (dataBuffer) {
const s = new StreamSearch(new Buffer('ADS1299'));
// Clear the buffer
s.reset();
// Push the new data buffer. This runs the search.
s.push(dataBuffer);
// Check and see if there is a match
return s.matches;
}
/**
* @description Searchs the buffer for a "$$$" or as we call an EOT
* @param dataBuffer - The buffer of some length to parse
* @returns {boolean} - True if the `$$$` was found.
*/
// TODO: StreamSearch is optimized to search incoming chunks of data, streaming in,
// but a new search is constructed here with every call. This is not making use
// of StreamSearch's optimizations; the object should be preserved between chunks,
// and only fed the new data. TODO: also check other uses of StreamSearch
function doesBufferHaveEOT (dataBuffer) {
const s = new StreamSearch(new Buffer(k.OBCIParseEOT));
// Clear the buffer
s.reset();
// Push the new data buffer. This runs the search.
s.push(dataBuffer);
// Check and see if there is a match
return s.matches >= 1;
}
/**
* @description Used to parse a soft reset response to determine if the board is running the v2 firmware
* @param dataBuffer {Buffer} - The data to parse
* @returns {boolean} - True if `v2`is indeed found in the `dataBuffer`
*/
function findV2Firmware (dataBuffer) {
const s = new StreamSearch(new Buffer(k.OBCIParseFirmware));
// Clear the buffer
s.reset();
// Push the new data buffer. This runs the search.
s.push(dataBuffer);
// Check and see if there is a match
return s.matches >= 1;
}
/**
* @description Used to parse a buffer for the word `Failure` that is acked back after private radio msg on failure
* @param dataBuffer {Buffer} - The buffer of some length to parse
* @returns {boolean} - True if `Failure` was found.
*/
function isFailureInBuffer (dataBuffer) {
const s = new StreamSearch(new Buffer(k.OBCIParseFailure));
// Clear the buffer
s.reset();
// Push the new data buffer. This runs the search.
s.push(dataBuffer);
// Check and see if there is a match
return s.matches >= 1;
}
/**
* @description Used to parse a buffer for the word `Success` that is acked back after private radio msg on success
* @param dataBuffer {Buffer} - The buffer of some length to parse
* @returns {boolean} - True if `Success` was found.
*/
function isSuccessInBuffer (dataBuffer) {
const s = new StreamSearch(new Buffer(k.OBCIParseSuccess));
// Clear the buffer
s.reset();
// Push the new data buffer. This runs the search.
s.push(dataBuffer);
// Check and see if there is a match
return s.matches >= 1;
}
/**
* @description Used to slice a buffer for the EOT '$$$'.
* @param dataBuffer {Buffer} - The buffer of some length to parse
* @returns {Buffer} - The remaining buffer.
*/
function stripToEOTBuffer (dataBuffer) {
let indexOfEOT = dataBuffer.indexOf(k.OBCIParseEOT);
if (indexOfEOT >= 0) {
indexOfEOT += k.OBCIParseEOT.length;
} else {
return dataBuffer;
}
if (indexOfEOT < dataBuffer.byteLength) {
if (k.getVersionNumber(process.version) >= 6) {
// From introduced in node version 6.x.x
return Buffer.from(dataBuffer.slice(indexOfEOT));
} else {
return new Buffer(dataBuffer.slice(indexOfEOT));
}
} else {
return null;
}
}
/**
* @description Used to parse a buffer for the `,` character that is acked back after a time sync request is sent
* @param dataBuffer {Buffer} - The buffer of some length to parse
* @returns {boolean} - True if the `,` was found.
*/
function isTimeSyncSetConfirmationInBuffer (dataBuffer) {
if (dataBuffer) {
var bufferLength = dataBuffer.length;
switch (bufferLength) {
case 0:
return false;
case 1:
return dataBuffer[0] === k.OBCISyncTimeSent.charCodeAt(0);
case 2:
// HEAD Byte at End
if (dataBuffer[1] === k.OBCIByteStart) {
return dataBuffer[0] === k.OBCISyncTimeSent.charCodeAt(0);
// TAIL byte in front
} else if (isStopByte((dataBuffer[0]))) {
return dataBuffer[1] === k.OBCISyncTimeSent.charCodeAt(0);
} else {
return false;
}
default:
if (dataBuffer[0] === k.OBCISyncTimeSent.charCodeAt(0) && dataBuffer[1] === k.OBCIByteStart) {
return true;
}
for (var i = 1; i < bufferLength; i++) {
// The base case (last one)
// console.log(i)
if (i === (bufferLength - 1)) {
if (isStopByte((dataBuffer[i - 1]))) {
return dataBuffer[i] === k.OBCISyncTimeSent.charCodeAt(0);
}
} else {
// Wedged
if (isStopByte(dataBuffer[i - 1]) && dataBuffer[i + 1] === k.OBCIByteStart) {
return dataBuffer[i] === k.OBCISyncTimeSent.charCodeAt(0);
// TAIL byte in front
}
}
}
return false;
}
}
}
/**
* @description Mainly used by the simulator to convert a randomly generated sample into a std OpenBCI V3 Packet
* @param sample {Object} - A sample object
* @param time {Number} - The time to inject into the sample.
* @param rawAux {Buffer} - 2 byte buffer to inject into sample
* @returns {Buffer} - A time sync raw aux packet
*/
function convertSampleToPacketRawAuxTimeSynced (sample, time, rawAux) {
var packetBuffer = new Buffer(k.OBCIPacketSize);
packetBuffer.fill(0);
// start byte
packetBuffer[0] = k.OBCIByteStart;
// sample number
packetBuffer[1] = sample.sampleNumber;
// channel data
for (var i = 0; i < k.OBCINumberOfChannelsDefault; i++) {
var threeByteBuffer = floatTo3ByteBuffer(sample.channelData[i]);
threeByteBuffer.copy(packetBuffer, 2 + (i * 3));
}
// Write the raw aux bytes
rawAux.copy(packetBuffer, 26);
// Write the time
packetBuffer.writeInt32BE(time, 28);
// stop byte
packetBuffer[k.OBCIPacketSize - 1] = makeTailByteFromPacketType(k.OBCIStreamPacketRawAuxTimeSynced);
return packetBuffer;
}
/**
* @description Mainly used by the simulator to convert a randomly generated sample into a std OpenBCI V3 Packet
* @param sample {Object} - A sample object
* @param time {Number} - The time to inject into the sample.
* @returns {Buffer} - A time sync accel packet
*/
function convertSampleToPacketAccelTimeSynced (sample, time) {
var packetBuffer = new Buffer(k.OBCIPacketSize);
packetBuffer.fill(0);
// start byte
packetBuffer[0] = k.OBCIByteStart;
// sample number
packetBuffer[1] = sample.sampleNumber;
// channel data
for (var i = 0; i < k.OBCINumberOfChannelsDefault; i++) {
var threeByteBuffer = floatTo3ByteBuffer(sample.channelData[i]);
threeByteBuffer.copy(packetBuffer, 2 + (i * 3));
}
packetBuffer.writeInt32BE(time, 28);
// stop byte
packetBuffer[k.OBCIPacketSize - 1] = makeTailByteFromPacketType(k.OBCIStreamPacketAccelTimeSynced);
return packetBuffer;
}
/**
* @description Converts a packet type {Number} into a OpenBCI stop byte
* @param type {Number} - The number to smash on to the stop byte. Must be 0-15,
* out of bounds input will result in a 0
* @return {Number} - A properly formatted OpenBCI stop byte
*/
function makeTailByteFromPacketType (type) {
if (type < 0 || type > 15) {
type = 0;
}
return k.OBCIByteStop | type;
}
/**
* @description Used to check and see if a byte adheres to the stop byte structure of 0xCx where x is the set of
* numbers from 0-F in hex of 0-15 in decimal.
* @param byte {Number} - The number to test
* @returns {boolean} - True if `byte` follows the correct form
* @author AJ Keller (@pushtheworldllc)
*/
function isStopByte (byte) {
return (byte & 0xF0) === k.OBCIByteStop;
}