1292 linhas
55 KiB
JavaScript
1292 linhas
55 KiB
JavaScript
'use strict';
|
|
var gaussian = require('gaussian'),
|
|
k = require('./openBCIConstants'),
|
|
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
|
|
|
|
// For computing Goertzel Algorithm
|
|
// See: http://www.embedded.com/design/configurable-systems/4024443/The-Goertzel-Algorithm
|
|
// In the tutorial cited above, GOERTZEL_BLOCK_SIZE is referred to as N
|
|
const GOERTZEL_BLOCK_SIZE = 62;
|
|
const GOERTZEL_K_250 = Math.floor(0.5 + ((GOERTZEL_BLOCK_SIZE * k.OBCILeadOffFrequencyHz) / k.OBCISampleRate250));
|
|
const GOERTZEL_W_250 = ((2 * Math.PI) / GOERTZEL_BLOCK_SIZE) * GOERTZEL_K_250;
|
|
const GOERTZEL_COEFF_250 = 2 * Math.cos(GOERTZEL_W_250);
|
|
// TODO: Add support for 16 channel Daisy board
|
|
var oddSample = {};
|
|
|
|
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 - 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 {Promise} - A sample object
|
|
*/
|
|
parseRawPacketStandard: (dataBuf,channelSettingsArray,convertAuxToAccel) => {
|
|
const defaultChannelSettingsArray = k.channelSettingsArrayInit(k.OBCINumberOfChannelsDefault);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
if (dataBuf === undefined || dataBuf === null) reject('Error [parseRawPacket]: dataBuf must be defined.');
|
|
// Verify proper start byte
|
|
if (dataBuf[0] != k.OBCIByteStart) reject('Error [parseRawPacket]: Invalid start byte of ' + dataBuf[0].toString(16) + ' expected ' + k.OBCIByteStart.toString(16));
|
|
// 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;
|
|
|
|
if (convertAuxToAccel) {
|
|
parsePacketStandardAccel(dataBuf,channelSettingsArray).then(sampleObject => {
|
|
resolve(sampleObject);
|
|
}).catch(err => reject(err));
|
|
} else {
|
|
parsePacketStandardRawAux(dataBuf,channelSettingsArray).then(sampleObject => {
|
|
resolve(sampleObject);
|
|
}).catch(err => reject(err));
|
|
}
|
|
|
|
});
|
|
},
|
|
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,
|
|
/**
|
|
* @description Calculate the impedance for one channel only.
|
|
* @param sampleObject - Standard OpenBCI sample object
|
|
* @param channelNumber - Number, the channel you want to calculate impedance for.
|
|
* @returns {Promise} - Fulfilled with impedance value for the specified channel.
|
|
* @author AJ Keller
|
|
*/
|
|
impedanceCalculationForChannel: (sampleObject,channelNumber) => {
|
|
const sqrt2 = Math.sqrt(2);
|
|
return new Promise((resolve,reject) => {
|
|
if(sampleObject === undefined || sampleObject === null) reject('Sample Object cannot be null or undefined');
|
|
if(sampleObject.channelData === undefined || sampleObject.channelData === null) reject('Channel cannot be null or undefined');
|
|
if(channelNumber < 1 || channelNumber > k.OBCINumberOfChannelsDefault) reject('Channel number invalid.');
|
|
|
|
var index = channelNumber - 1;
|
|
|
|
if (sampleObject.channelData[index] < 0) {
|
|
sampleObject.channelData[index] *= -1;
|
|
}
|
|
var impedance = (sqrt2 * sampleObject.channelData[index]) / k.OBCILeadOffDriveInAmps;
|
|
//if (index === 0) console.log("Voltage: " + (sqrt2*sampleObject.channelData[index]) + " leadoff amps: " + k.OBCILeadOffDriveInAmps + " impedance: " + impedance);
|
|
resolve(impedance);
|
|
});
|
|
},
|
|
/**
|
|
* @description Calculate the impedance for all channels.
|
|
* @param sampleObject - Standard OpenBCI sample object
|
|
* @returns {Promise} - Fulfilled with impedances for the sample
|
|
* @author AJ Keller
|
|
*/
|
|
impedanceCalculationForAllChannels: sampleObject => {
|
|
const sqrt2 = Math.sqrt(2);
|
|
return new Promise((resolve,reject) => {
|
|
if(sampleObject === undefined || sampleObject === null) reject('Sample Object cannot be null or undefined');
|
|
if(sampleObject.channelData === undefined || sampleObject.channelData === null) reject('Channel cannot be null or undefined');
|
|
|
|
var sampleImpedances = [];
|
|
var numChannels = sampleObject.channelData.length;
|
|
for (var index = 0;index < numChannels; index++) {
|
|
if (sampleObject.channelData[index] < 0) {
|
|
sampleObject.channelData[index] *= -1;
|
|
}
|
|
var impedance = (sqrt2 * sampleObject.channelData[index]) / k.OBCILeadOffDriveInAmps;
|
|
sampleImpedances.push(impedance);
|
|
|
|
//if (index === 0) console.log("Voltage: " + (sqrt2*sampleObject.channelData[index]) + " leadoff amps: " + k.OBCILeadOffDriveInAmps + " impedance: " + impedance);
|
|
}
|
|
|
|
sampleObject.impedances = sampleImpedances;
|
|
|
|
resolve(sampleObject);
|
|
});
|
|
},
|
|
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} - True if you want to inject noise
|
|
* @param lineNoise {String} - 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) => {
|
|
var self = this;
|
|
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,
|
|
/**
|
|
* @description Use the Goertzel algorithm to calculate impedances
|
|
* @param sample - a sample with channelData Array
|
|
* @param goertzelObj - An object that was created by a call to this.goertzelNewObject()
|
|
* @returns {Array} - Returns an array if finished computing
|
|
*/
|
|
goertzelProcessSample: (sample,goertzelObj) => {
|
|
// calculate the goertzel values for all channels
|
|
for (var i = 0; i < goertzelObj.numberOfChannels; i++) {
|
|
var q0 = GOERTZEL_COEFF_250 * goertzelObj.q1[i] - goertzelObj.q2[i] + sample.channelData[i];
|
|
goertzelObj.q2[i] = goertzelObj.q1[i];
|
|
goertzelObj.q1[i] = q0;
|
|
|
|
//console.log('Q1: ' + goertzelObj.q1[i] + ' Q2: ' + goertzelObj.q2[i]);
|
|
}
|
|
|
|
|
|
// Increment the index counter
|
|
goertzelObj.index++;
|
|
|
|
|
|
// Have we iterated more times then block size?
|
|
if (goertzelObj.index > GOERTZEL_BLOCK_SIZE) {
|
|
var impedanceArray = [];
|
|
for (var j = 0; j < goertzelObj.numberOfChannels; j++) {
|
|
// Calculate the magnitude of the voltage
|
|
var q1SQRD = goertzelObj.q1[j] * goertzelObj.q1[j];
|
|
var q2SQRD = goertzelObj.q2[j] * goertzelObj.q2[j];
|
|
var lastPart = goertzelObj.q1[j] * goertzelObj.q2[j] * GOERTZEL_COEFF_250;
|
|
|
|
//console.log('Chan ' + j + ', Q1^2: ' + q1SQRD + ', Q2^2: ' + q2SQRD + ', Last Part: ' + lastPart);
|
|
|
|
var voltage = Math.sqrt((goertzelObj.q1[j] * goertzelObj.q1[j]) + (goertzelObj.q2[j] * goertzelObj.q2[j]) - goertzelObj.q1[j] * goertzelObj.q2[j] * GOERTZEL_COEFF_250);
|
|
|
|
// Calculate the impedance r = v/i
|
|
var impedance = voltage / k.OBCILeadOffDriveInAmps;
|
|
// Push the impedance into the final array
|
|
impedanceArray.push(impedance);
|
|
|
|
// Reset the goertzel variables to get ready for the next iteration
|
|
goertzelObj.q1[j] = 0;
|
|
goertzelObj.q2[j] = 0;
|
|
}
|
|
|
|
// Reset the goertzel index counter
|
|
goertzelObj.index = 0;
|
|
|
|
// Pass out the impedance array
|
|
return impedanceArray;
|
|
} else {
|
|
// This reject is really just for debugging
|
|
return;
|
|
}
|
|
},
|
|
goertzelNewObject: numberOfChannels => {
|
|
// Object to help calculate the goertzel
|
|
var q1 = [];
|
|
var q2 = [];
|
|
for (var i = 0; i < numberOfChannels; i++) {
|
|
q1.push(0);
|
|
q2.push(0);
|
|
}
|
|
return {
|
|
q1: q1,
|
|
q2: q2,
|
|
index: 0,
|
|
numberOfChannels: numberOfChannels
|
|
}
|
|
},
|
|
GOERTZEL_BLOCK_SIZE,
|
|
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,
|
|
/**
|
|
* @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 i = 1; i < (newSampleNumber - previousSampleNumber); i++) {
|
|
missedPacketArray.push(previousSampleNumber + i);
|
|
}
|
|
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,
|
|
timeSyncSent: 0,
|
|
timeSyncSentConfirmation: 0,
|
|
timeSyncSetPacket: 0,
|
|
timeRoundTrip: 0,
|
|
timeTransmission: 0,
|
|
timeOffset: 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 {Promise} - Fulfilled with a sample object that has form:
|
|
* {
|
|
* channelData: Array of floats
|
|
* accelData: Array of floats of accel data
|
|
* sampleNumber: a Number that is the sample
|
|
* }
|
|
*/
|
|
function parsePacketStandardAccel(dataBuf, channelSettingsArray) {
|
|
return new Promise((resolve, reject) => {
|
|
if (dataBuf.byteLength != k.OBCIPacketSize) reject(`Error [parsePacketStandardAccel]: input buffer must be ${k.OBCIPacketSize} bytes!`);
|
|
|
|
var sampleObject = {};
|
|
// Need build the standard sample object
|
|
getDataArrayAccel(dataBuf.slice(k.OBCIPacketPositionStartAux,k.OBCIPacketPositionStopAux+1))
|
|
.then(accelData => {
|
|
sampleObject.accelData = accelData;
|
|
return getChannelDataArray(dataBuf, channelSettingsArray);
|
|
})
|
|
.then(channelSettingArray => {
|
|
sampleObject.channelData = channelSettingArray;
|
|
// Get the raw aux values
|
|
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];
|
|
resolve(sampleObject);
|
|
})
|
|
.catch(err => {
|
|
console.log(err);
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @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) {
|
|
return new Promise((resolve, reject) => {
|
|
if (dataBuf.byteLength != k.OBCIPacketSize) reject("Error [parsePacketStandardAccel]: input buffer must be " + k.OBCIPacketSize + " bytes!");
|
|
|
|
var sampleObject = {};
|
|
// Need build the standard sample object
|
|
getChannelDataArray(dataBuf, channelSettingsArray)
|
|
.then(channelSettingArray => {
|
|
// 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));
|
|
}
|
|
// Store the channel data
|
|
sampleObject.channelData = channelSettingArray;
|
|
// 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];
|
|
resolve(sampleObject);
|
|
})
|
|
.catch(err => {
|
|
//console.log(err);
|
|
reject(err);
|
|
});
|
|
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @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 {Promise} - Fulfills with 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
|
|
|
|
return new Promise((resolve, reject) => {
|
|
// The sample object we are going to build
|
|
var sampleObject = {};
|
|
|
|
if (dataBuf.byteLength != k.OBCIPacketSize) reject("Error [parsePacketTimeSyncedAccel]: input buffer must be " + k.OBCIPacketSize + " bytes!");
|
|
|
|
// 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];
|
|
|
|
getFromTimePacketTime(dataBuf)
|
|
.then(boardTime => {
|
|
sampleObject.boardTime = boardTime;
|
|
sampleObject.timeStamp = boardTime + boardOffsetTime;
|
|
return getFromTimePacketRawAux(dataBuf);
|
|
})
|
|
.then(auxDataBuffer => {
|
|
sampleObject.auxData = auxDataBuffer;
|
|
return getFromTimePacketAccel(dataBuf, accelArray);
|
|
})
|
|
.then(accelArrayFilled => {
|
|
if (accelArrayFilled) {
|
|
sampleObject.accelData = accelArray;
|
|
}
|
|
return getChannelDataArray(dataBuf, channelSettingsArray);
|
|
})
|
|
.then(channelDataArray => {
|
|
sampleObject.channelData = channelDataArray;
|
|
resolve(sampleObject);
|
|
})
|
|
.catch(err => {
|
|
reject(err);
|
|
});
|
|
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @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 {Promise} - Fulfills with 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
|
|
return new Promise((resolve, reject) => {
|
|
// The sample object we are going to build
|
|
var sampleObject = {};
|
|
|
|
if (dataBuf.byteLength != k.OBCIPacketSize) reject("Error [parsePacketTimeSyncedRawAux]: input buffer must be " + k.OBCIPacketSize + " bytes!");
|
|
|
|
// 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];
|
|
|
|
getFromTimePacketTime(dataBuf)
|
|
.then(boardTime => {
|
|
sampleObject.boardTime = boardTime;
|
|
sampleObject.timeStamp = boardTime + boardOffsetTime;
|
|
return getFromTimePacketRawAux(dataBuf);
|
|
})
|
|
.then(auxDataBuffer => {
|
|
sampleObject.auxData = auxDataBuffer;
|
|
return getChannelDataArray(dataBuf, channelSettingsArray);
|
|
})
|
|
.then(channelDataArray => {
|
|
sampleObject.channelData = channelDataArray;
|
|
resolve(sampleObject);
|
|
})
|
|
.catch(err => {
|
|
reject(err);
|
|
});
|
|
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @description Extract a time from a time packet in ms.
|
|
* @param dataBuf - A raw packet with 33 bytes of data
|
|
* @returns {Promise} - Fulfills with 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
|
|
return new Promise((resolve, reject) => {
|
|
if (dataBuf.byteLength != k.OBCIPacketSize) reject("Error [getFromTimePacketTime]: input buffer must be " + k.OBCIPacketSize + " bytes!");
|
|
|
|
// Grab the time from the packet
|
|
resolve(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 {Promise} - Fulfills with 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
|
|
return new Promise((resolve, reject) => {
|
|
if (dataBuf.byteLength != k.OBCIPacketSize) reject("Error [getFromTimePacketAccel]: input buffer must be " + k.OBCIPacketSize + " bytes!");
|
|
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
|
|
resolve(false);
|
|
break;
|
|
case k.OBCIAccelAxisY:
|
|
accelArray[1] = sampleModule.interpret16bitAsInt32(dataBuf.slice(lastBytePosition, lastBytePosition + 2)) * SCALE_FACTOR_ACCEL; // slice is not inclusive on the right
|
|
resolve(false);
|
|
break;
|
|
case k.OBCIAccelAxisZ:
|
|
accelArray[2] = sampleModule.interpret16bitAsInt32(dataBuf.slice(lastBytePosition, lastBytePosition + 2)) * SCALE_FACTOR_ACCEL; // slice is not inclusive on the right
|
|
resolve(true);
|
|
break;
|
|
default:
|
|
resolve(false);
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @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) {
|
|
return new Promise((resolve, reject) => {
|
|
if (dataBuf.byteLength != k.OBCIPacketSize) reject("Error [getFromTimePacketRawAux]: input buffer must be " + k.OBCIPacketSize + " bytes!");
|
|
if (k.getVersionNumber(process.version) >= 6) {
|
|
resolve(Buffer.from(dataBuf.slice(k.OBCIPacketPositionTimeSyncAuxStart, k.OBCIPacketPositionTimeSyncAuxStop)));
|
|
} else {
|
|
resolve(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 {Promise} - Fulfilled with Array of floats 3 elements long
|
|
* @author AJ Keller (@pushtheworldllc)
|
|
*/
|
|
function getDataArrayAccel(dataBuf) {
|
|
return new Promise(resolve => {
|
|
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);
|
|
}
|
|
resolve(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 {Promise} - Fulfilled with Array filled with floats for each channel's voltage in VOLTS
|
|
* @author AJ Keller (@pushtheworldllc)
|
|
*/
|
|
function getChannelDataArray(dataBuf, channelSettingsArray) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!Array.isArray(channelSettingsArray)) reject('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")) reject(`Error [getChannelDataArray]: Invalid channel settings object at index ${i}`);
|
|
if (!k.isNumber(channelSettingsArray[i].gain)) reject('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)));
|
|
}
|
|
resolve(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.
|
|
*/
|
|
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 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) {
|
|
const comma = new Buffer(k.OBCISyncTimeSent);
|
|
if (dataBuffer) {
|
|
var bufferLength = dataBuffer.length;
|
|
switch (bufferLength) {
|
|
case 0:
|
|
return false;
|
|
break;
|
|
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:
|
|
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 {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
|
|
*/
|
|
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 {Buffer} - 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;
|
|
};
|