Inital Commit
Esse commit está contido em:
@@ -0,0 +1 @@
|
||||
{"extends": ["standard"], "parser": "babel-eslint"}
|
||||
@@ -0,0 +1,45 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# Adapted from OpenEXP
|
||||
.idea
|
||||
.DS_Store
|
||||
public
|
||||
|
||||
# Test output files
|
||||
myOutput.txt
|
||||
hardwareVoltageOutputAll.txt
|
||||
|
||||
# Local npm builds for testing end in .tgz
|
||||
*.tgz
|
||||
|
||||
# For git
|
||||
*.orig
|
||||
|
||||
# Text editor temporary files
|
||||
.*.sw* # vi/vim
|
||||
@@ -0,0 +1,21 @@
|
||||
test/
|
||||
test_mocks/
|
||||
|
||||
# MIRRORED FROM .gitignore PLEASE MAINTAIN
|
||||
logs
|
||||
*.log
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
lib-cov
|
||||
coverage
|
||||
.grunt
|
||||
.lock-wscript
|
||||
build/Release
|
||||
node_modules
|
||||
.idea
|
||||
.DS_Store
|
||||
public
|
||||
myOutput.txt
|
||||
*.tgz
|
||||
openBCISerialFormat
|
||||
@@ -0,0 +1,33 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "4.2.3"
|
||||
- "5.11.0"
|
||||
- "6.0"
|
||||
- "6.1"
|
||||
- "6.2"
|
||||
- "6.3"
|
||||
- "6.4"
|
||||
- "6.5"
|
||||
- "6.6"
|
||||
- "6.7"
|
||||
- "6.8"
|
||||
- "7.0"
|
||||
- "7.1"
|
||||
|
||||
before_install:
|
||||
# before_install code came from here: https://github.com/node-inspector/node-inspector/issues/776
|
||||
- sudo apt-get install python-software-properties
|
||||
- sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install gcc-5 g++-5
|
||||
- sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-5 80 --slave /usr/bin/g++ g++ /usr/bin/g++-5
|
||||
- sudo update-alternatives --set gcc /usr/bin/gcc-5
|
||||
# https://docs.travis-ci.com/user/customizing-the-build
|
||||
# https://www.howtoinstall.co/en/ubuntu/trusty/libbluetooth-dev
|
||||
- npm install -g node-pre-gyp-github node-gyp
|
||||
- sudo apt-get install -qq bluetooth bluez libbluetooth-dev libudev-dev
|
||||
|
||||
install:
|
||||
- npm install --all
|
||||
script:
|
||||
- npm run test-cov
|
||||
+1145
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
@@ -0,0 +1,3 @@
|
||||
# 0.1.0
|
||||
|
||||
Initial release
|
||||
@@ -0,0 +1,79 @@
|
||||
const Ganglion = require('../../index').Ganglion;
|
||||
const k = require('../../openBCIConstants');
|
||||
const verbose = true;
|
||||
var ganglion = new Ganglion({
|
||||
// debug: true,
|
||||
sendCounts: true,
|
||||
verbose: verbose
|
||||
});
|
||||
|
||||
function errorFunc (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
ganglion.once(k.OBCIEmitterGanglionFound, (peripheral) => {
|
||||
ganglion.searchStop().catch(errorFunc);
|
||||
|
||||
ganglion.on('sample', (sample) => {
|
||||
/** Work with sample */
|
||||
console.log(sample.sampleNumber);
|
||||
// for (var i = 0; i < ganglion.numberOfChannels(); i++) {
|
||||
// console.log("Channel " + (i + 1) + ": " + sample.channelData[i].toFixed(8) + " Volts.");
|
||||
// }
|
||||
});
|
||||
|
||||
ganglion.on('message', (message) => {
|
||||
console.log('message: ', message.toString());
|
||||
});
|
||||
|
||||
ganglion.once('ready', () => {
|
||||
ganglion.syntheticEnable()
|
||||
.then(() => {
|
||||
return ganglion.streamStart();
|
||||
})
|
||||
.catch(errorFunc);
|
||||
console.log('ready');
|
||||
});
|
||||
|
||||
ganglion.connect(peripheral).catch(errorFunc);
|
||||
});
|
||||
|
||||
ganglion.searchStart().catch(errorFunc);
|
||||
|
||||
function exitHandler (options, err) {
|
||||
if (options.cleanup) {
|
||||
if (verbose) console.log('clean');
|
||||
// console.log(connectedPeripheral)
|
||||
ganglion.manualDisconnect = true;
|
||||
ganglion.disconnect();
|
||||
}
|
||||
if (err) console.log(err.stack);
|
||||
if (options.exit) {
|
||||
if (verbose) console.log('exit');
|
||||
if (ganglion.isConnected()) {
|
||||
ganglion.disconnect()
|
||||
.then(() => {
|
||||
process.exit();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (verbose) console.log(err);
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// do something when app is closing
|
||||
process.on('exit', exitHandler.bind(null, {
|
||||
cleanup: true
|
||||
}));
|
||||
|
||||
// catches ctrl+c event
|
||||
process.on('SIGINT', exitHandler.bind(null, {
|
||||
exit: true
|
||||
}));
|
||||
|
||||
// catches uncaught exceptions
|
||||
process.on('uncaughtException', exitHandler.bind(null, {
|
||||
exit: true
|
||||
}));
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
swagger: '2.0'
|
||||
info:
|
||||
version: 1.0.0
|
||||
title: OpenBCI Ganglion API
|
||||
################################################################################
|
||||
# PATHS #
|
||||
################################################################################
|
||||
|
||||
paths:
|
||||
/s:
|
||||
get:
|
||||
description: Scan for a list of BLE devices
|
||||
responses:
|
||||
200:
|
||||
description: List of ports
|
||||
407:
|
||||
description: No ports found
|
||||
409:
|
||||
description: Already scanning
|
||||
/c:
|
||||
put:
|
||||
description: Open a BLE connection
|
||||
parameters:
|
||||
- name: port
|
||||
in: query
|
||||
description: The name of the BLE device to connect to.
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Port name connected to
|
||||
402:
|
||||
description: Unable to open connection to device
|
||||
405:
|
||||
description: Device not found
|
||||
408:
|
||||
description: Already connected to a device
|
||||
/d:
|
||||
delete:
|
||||
description: Close an open BLE connection
|
||||
responses:
|
||||
200:
|
||||
description: Closed connection
|
||||
400:
|
||||
description: No open BLE device
|
||||
401:
|
||||
description: Unable to disconnect from device, try again later.
|
||||
/k:
|
||||
put:
|
||||
description: Send a command to an open BLE device
|
||||
parameters:
|
||||
- name: command
|
||||
in: query
|
||||
description: The command(s) to send to the BLE device
|
||||
type: string
|
||||
required: true
|
||||
responses:
|
||||
200:
|
||||
description: Command sent
|
||||
400:
|
||||
description: No open BLE device
|
||||
406:
|
||||
description: Command not recognized
|
||||
/q:
|
||||
get:
|
||||
description: Get the connection status
|
||||
responses:
|
||||
200:
|
||||
description: true if connected false if not
|
||||
/e:
|
||||
post:
|
||||
description: Error message from
|
||||
parameters:
|
||||
- name: code
|
||||
in: query
|
||||
description: The error code
|
||||
type: integer
|
||||
required: true
|
||||
|
||||
- name: msg
|
||||
in: query
|
||||
description: The plain text error message
|
||||
type: string
|
||||
|
||||
responses:
|
||||
200:
|
||||
description: Error message
|
||||
/t:
|
||||
post:
|
||||
description: Data from the OpenBCI Ganglion board
|
||||
responses:
|
||||
200:
|
||||
description: Good data packet
|
||||
500:
|
||||
description: Bad data packet
|
||||
/i:
|
||||
post:
|
||||
description: Impedance Data from the OpenBCI Ganglion board
|
||||
parameters:
|
||||
- name: channelNumber
|
||||
description: The channel number, either 1, 2, 3, 4 or 0 for reference
|
||||
in: res
|
||||
type: integer
|
||||
required: true
|
||||
- name: impedance value
|
||||
description: The impedance value in ohms
|
||||
in: res
|
||||
type: integer
|
||||
required: true
|
||||
/l:
|
||||
post:
|
||||
description: Log message from the node
|
||||
parameters:
|
||||
- name: msg
|
||||
in: query
|
||||
description: The plain test log message
|
||||
type: string
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports.Cyton = require('./openBCICyton');
|
||||
module.exports.Ganglion = require('./openBCIGanglion');
|
||||
module.exports.Constants = require('./openBCIConstants');
|
||||
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* Created by ajk on 12/16/15.
|
||||
* Purpose: This file folds all the constants for the
|
||||
* OpenBCI Board
|
||||
*/
|
||||
'use strict';
|
||||
/** Turning channels off */
|
||||
const obciChannelOff1 = '1';
|
||||
const obciChannelOff2 = '2';
|
||||
const obciChannelOff3 = '3';
|
||||
const obciChannelOff4 = '4';
|
||||
|
||||
/** Turn channels on */
|
||||
const obciChannelOn1 = '!';
|
||||
const obciChannelOn2 = '@';
|
||||
const obciChannelOn3 = '#';
|
||||
const obciChannelOn4 = '$';
|
||||
|
||||
/** SD card Commands */
|
||||
const obciSDLogForHour1 = 'G';
|
||||
const obciSDLogForHour2 = 'H';
|
||||
const obciSDLogForHour4 = 'J';
|
||||
const obciSDLogForHour12 = 'K';
|
||||
const obciSDLogForHour24 = 'L';
|
||||
const obciSDLogForMin5 = 'A';
|
||||
const obciSDLogForMin15 = 'S';
|
||||
const obciSDLogForMin30 = 'F';
|
||||
const obciSDLogForSec14 = 'a';
|
||||
const obciSDLogStop = 'j';
|
||||
|
||||
/** SD Card String Commands */
|
||||
const obciStringSDHour1 = '1hour';
|
||||
const obciStringSDHour2 = '2hour';
|
||||
const obciStringSDHour4 = '4hour';
|
||||
const obciStringSDHour12 = '12hour';
|
||||
const obciStringSDHour24 = '24hour';
|
||||
const obciStringSDMin5 = '5min';
|
||||
const obciStringSDMin15 = '15min';
|
||||
const obciStringSDMin30 = '30min';
|
||||
const obciStringSDSec14 = '14sec';
|
||||
|
||||
/** Stream Data Commands */
|
||||
const obciStreamStart = 'b';
|
||||
const obciStreamStop = 's';
|
||||
|
||||
/** Miscellaneous */
|
||||
const obciMiscQueryRegisterSettings = '?';
|
||||
const obciMiscSoftReset = 'v';
|
||||
|
||||
/** Possible number of channels */
|
||||
const obciNumberOfChannelsGanglion = 4;
|
||||
|
||||
/** Possible OpenBCI board types */
|
||||
const obciBoardGanglion = 'ganglion';
|
||||
|
||||
/** Possible Simulator Line Noise injections */
|
||||
const obciSimulatorLineNoiseHz60 = '60Hz';
|
||||
const obciSimulatorLineNoiseHz50 = '50Hz';
|
||||
const obciSimulatorLineNoiseNone = 'none';
|
||||
|
||||
/** Possible Simulator Fragmentation modes */
|
||||
const obciSimulatorFragmentationRandom = 'random';
|
||||
const obciSimulatorFragmentationFullBuffers = 'fullBuffers';
|
||||
const obciSimulatorFragmentationOneByOne = 'oneByOne';
|
||||
const obciSimulatorFragmentationNone = 'none';
|
||||
|
||||
/** Possible Sample Rates */
|
||||
const obciSampleRate200 = 200;
|
||||
|
||||
/** Errors */
|
||||
const errorNobleAlreadyScanning = 'Scan already under way';
|
||||
const errorNobleNotAlreadyScanning = 'No scan started';
|
||||
const errorNobleNotInPoweredOnState = 'Please turn blue tooth on.';
|
||||
const errorInvalidByteLength = 'Invalid Packet Byte Length';
|
||||
const errorInvalidByteStart = 'Invalid Start Byte';
|
||||
const errorInvalidByteStop = 'Invalid Stop Byte';
|
||||
const errorInvalidType = 'Invalid Type';
|
||||
const errorTimeSyncIsNull = "'this.sync.curSyncObj' must not be null";
|
||||
const errorTimeSyncNoComma = 'Missed the time sync sent confirmation. Try sync again';
|
||||
const errorUndefinedOrNullInput = 'Undefined or Null Input';
|
||||
|
||||
/** Max Master Buffer Size */
|
||||
const obciMasterBufferSize = 4096;
|
||||
|
||||
/** Impedance */
|
||||
const obciImpedanceTextBad = 'bad';
|
||||
const obciImpedanceTextNone = 'none';
|
||||
const obciImpedanceTextGood = 'good';
|
||||
const obciImpedanceTextInit = 'init';
|
||||
const obciImpedanceTextOk = 'ok';
|
||||
|
||||
const obciImpedanceThresholdGoodMin = 0;
|
||||
const obciImpedanceThresholdGoodMax = 5000;
|
||||
const obciImpedanceThresholdOkMin = 5001;
|
||||
const obciImpedanceThresholdOkMax = 10000;
|
||||
const obciImpedanceThresholdBadMin = 10001;
|
||||
const obciImpedanceThresholdBadMax = 1000000;
|
||||
|
||||
const obciImpedanceSeriesResistor = 2200; // There is a 2.2 k Ohm series resistor that must be subtracted
|
||||
|
||||
/** Simulator */
|
||||
const obciSimulatorPortName = 'OpenBCISimulator';
|
||||
|
||||
/** Parse */
|
||||
const obciParseFailure = 'Failure';
|
||||
const obciParseSuccess = 'Success';
|
||||
|
||||
/** Simulator Board Configurations */
|
||||
const obciSimulatorRawAux = 'rawAux';
|
||||
const obciSimulatorStandard = 'standard';
|
||||
|
||||
/** Emitters */
|
||||
const obciEmitterBlePoweredUp = 'blePoweredOn';
|
||||
const obciEmitterClose = 'close';
|
||||
const obciEmitterDroppedPacket = 'droppedPacket';
|
||||
const obciEmitterError = 'error';
|
||||
const obciEmitterGanglionFound = 'ganglionFound';
|
||||
const obciEmitterImpedance = 'impedance';
|
||||
const obciEmitterMessage = 'message';
|
||||
const obciEmitterQuery = 'query';
|
||||
const obciEmitterRawDataPacket = 'rawDataPacket';
|
||||
const obciEmitterReady = 'ready';
|
||||
const obciEmitterSample = 'sample';
|
||||
const obciEmitterSynced = 'synced';
|
||||
|
||||
/** Ganglion */
|
||||
const obciGanglionBleSearchTime = 20000; // ms
|
||||
const obciGanglionByteIdRawData = 0;
|
||||
const obciGanglionByteIdSampleMax = 127;
|
||||
const obciGanglionByteIdSampleMin = 1;
|
||||
const obciGanglionByteIdAccel = 128;
|
||||
const obciGanglionByteIdImpedanceChannel1 = 129;
|
||||
const obciGanglionByteIdImpedanceChannel2 = 130;
|
||||
const obciGanglionByteIdImpedanceChannel3 = 131;
|
||||
const obciGanglionByteIdImpedanceChannel4 = 132;
|
||||
const obciGanglionByteIdImpedanceChannelReference = 133;
|
||||
const obciGanglionByteIdMultiPacket = 134;
|
||||
const obciGanglionByteIdMultiPacketStop = 135;
|
||||
const obciGanglionPacketSize = 20;
|
||||
const obciGanglionSamplesPerPacket = 2;
|
||||
const obciGanglionPacket = {
|
||||
byteId: 0,
|
||||
dataStart: 1,
|
||||
dataStop: 20
|
||||
};
|
||||
const obciGanglionMCP3912Gain = 1.0; // assumed gain setting for MCP3912. NEEDS TO BE ADJUSTABLE JM
|
||||
const obciGanglionMCP3912Vref = 1.2; // reference voltage for ADC in MCP3912 set in hardware
|
||||
const obciGanglionPrefix = 'Ganglion';
|
||||
const obciGanglionSyntheticDataEnable = 't';
|
||||
const obciGanglionSyntheticDataDisable = 'T';
|
||||
const obciGanglionImpedanceStart = 'z';
|
||||
const obciGanglionImpedanceStop = 'Z';
|
||||
const obciGanglionScaleFactorPerCountVolts = obciGanglionMCP3912Vref / (8388607.0 * obciGanglionMCP3912Gain * 1.5 * 51.0);
|
||||
|
||||
/** Simblee */
|
||||
const simbleeUuidService = 'fe84';
|
||||
const simbleeUuidReceive = '2d30c082f39f4ce6923f3484ea480596';
|
||||
const simbleeUuidSend = '2d30c083f39f4ce6923f3484ea480596';
|
||||
const simbleeUuidDisconnect = '2d30c084f39f4ce6923f3484ea480596';
|
||||
|
||||
/** Noble */
|
||||
const obciNobleEmitterPeripheralConnect = 'connect';
|
||||
const obciNobleEmitterPeripheralDisconnect = 'disconnect';
|
||||
const obciNobleEmitterPeripheralDiscover = 'discover';
|
||||
const obciNobleEmitterPeripheralServicesDiscover = 'servicesDiscover';
|
||||
const obciNobleEmitterServiceCharacteristicsDiscover = 'characteristicsDiscover';
|
||||
const obciNobleEmitterServiceRead = 'read';
|
||||
const obciNobleEmitterDiscover = 'discover';
|
||||
const obciNobleEmitterScanStart = 'scanStart';
|
||||
const obciNobleEmitterScanStop = 'scanStop';
|
||||
const obciNobleEmitterStateChange = 'stateChange';
|
||||
const obciNobleStatePoweredOn = 'poweredOn';
|
||||
|
||||
module.exports = {
|
||||
/** Turning channels off */
|
||||
OBCIChannelOff1: obciChannelOff1,
|
||||
OBCIChannelOff2: obciChannelOff2,
|
||||
OBCIChannelOff3: obciChannelOff3,
|
||||
OBCIChannelOff4: obciChannelOff4,
|
||||
/**
|
||||
* Purpose: To get the proper command to turn a channel off
|
||||
* @param channelNumber - A number (1-16) of the desired channel
|
||||
* @returns {Promise}
|
||||
*/
|
||||
commandChannelOff: function (channelNumber) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
switch (channelNumber) {
|
||||
case 1:
|
||||
resolve(obciChannelOff1);
|
||||
break;
|
||||
case 2:
|
||||
resolve(obciChannelOff2);
|
||||
break;
|
||||
case 3:
|
||||
resolve(obciChannelOff3);
|
||||
break;
|
||||
case 4:
|
||||
resolve(obciChannelOff4);
|
||||
break;
|
||||
default:
|
||||
reject('Error [commandChannelOff]: Invalid Channel Number');
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
/** Turning channels on */
|
||||
OBCIChannelOn1: obciChannelOn1,
|
||||
OBCIChannelOn2: obciChannelOn2,
|
||||
OBCIChannelOn3: obciChannelOn3,
|
||||
OBCIChannelOn4: obciChannelOn4,
|
||||
commandChannelOn: function (channelNumber) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
switch (channelNumber) {
|
||||
case 1:
|
||||
resolve(obciChannelOn1);
|
||||
break;
|
||||
case 2:
|
||||
resolve(obciChannelOn2);
|
||||
break;
|
||||
case 3:
|
||||
resolve(obciChannelOn3);
|
||||
break;
|
||||
case 4:
|
||||
resolve(obciChannelOn4);
|
||||
break;
|
||||
default:
|
||||
reject('Error [commandChannelOn]: Invalid Channel Number');
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
/** SD card Commands */
|
||||
OBCISDLogForHour1: obciSDLogForHour1,
|
||||
OBCISDLogForHour2: obciSDLogForHour2,
|
||||
OBCISDLogForHour4: obciSDLogForHour4,
|
||||
OBCISDLogForHour12: obciSDLogForHour12,
|
||||
OBCISDLogForHour24: obciSDLogForHour24,
|
||||
OBCISDLogForMin5: obciSDLogForMin5,
|
||||
OBCISDLogForMin15: obciSDLogForMin15,
|
||||
OBCISDLogForMin30: obciSDLogForMin30,
|
||||
OBCISDLogForSec14: obciSDLogForSec14,
|
||||
OBCISDLogStop: obciSDLogStop,
|
||||
/** SD Card String Commands */
|
||||
OBCIStringSDHour1: obciStringSDHour1,
|
||||
OBCIStringSDHour2: obciStringSDHour2,
|
||||
OBCIStringSDHour4: obciStringSDHour4,
|
||||
OBCIStringSDHour12: obciStringSDHour12,
|
||||
OBCIStringSDHour24: obciStringSDHour24,
|
||||
OBCIStringSDMin5: obciStringSDMin5,
|
||||
OBCIStringSDMin15: obciStringSDMin15,
|
||||
OBCIStringSDMin30: obciStringSDMin30,
|
||||
OBCIStringSDSec14: obciStringSDSec14,
|
||||
/**
|
||||
* @description Converts a sd string into the proper setting.
|
||||
* @param stringCommand {String} - The length of time you want to record to the SD for.
|
||||
* @returns {Promise} The command to send to the Board, returns an error on improper `stringCommand`
|
||||
*/
|
||||
sdSettingForString: (stringCommand) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
switch (stringCommand) {
|
||||
case obciStringSDHour1:
|
||||
resolve(obciSDLogForHour1);
|
||||
break;
|
||||
case obciStringSDHour2:
|
||||
resolve(obciSDLogForHour2);
|
||||
break;
|
||||
case obciStringSDHour4:
|
||||
resolve(obciSDLogForHour4);
|
||||
break;
|
||||
case obciStringSDHour12:
|
||||
resolve(obciSDLogForHour12);
|
||||
break;
|
||||
case obciStringSDHour24:
|
||||
resolve(obciSDLogForHour24);
|
||||
break;
|
||||
case obciStringSDMin5:
|
||||
resolve(obciSDLogForMin5);
|
||||
break;
|
||||
case obciStringSDMin15:
|
||||
resolve(obciSDLogForMin15);
|
||||
break;
|
||||
case obciStringSDMin30:
|
||||
resolve(obciSDLogForMin30);
|
||||
break;
|
||||
case obciStringSDSec14:
|
||||
resolve(obciSDLogForSec14);
|
||||
break;
|
||||
default:
|
||||
reject(new Error(TypeError));
|
||||
break;
|
||||
|
||||
}
|
||||
});
|
||||
},
|
||||
/** Stream Data Commands */
|
||||
OBCIStreamStart: obciStreamStart,
|
||||
OBCIStreamStop: obciStreamStop,
|
||||
/** Miscellaneous */
|
||||
OBCIMiscQueryRegisterSettings: obciMiscQueryRegisterSettings,
|
||||
OBCIMiscSoftReset: obciMiscSoftReset,
|
||||
/** Possible number of channels */
|
||||
OBCINumberOfChannelsGanglion: obciNumberOfChannelsGanglion,
|
||||
/** Possible OpenBCI board types */
|
||||
OBCIBoardGanglion: obciBoardGanglion,
|
||||
/** Possible Sample Rates */
|
||||
OBCISampleRate200: obciSampleRate200,
|
||||
/** Errors */
|
||||
OBCIErrorNobleAlreadyScanning: errorNobleAlreadyScanning,
|
||||
OBCIErrorNobleNotAlreadyScanning: errorNobleNotAlreadyScanning,
|
||||
OBCIErrorNobleNotInPoweredOnState: errorNobleNotInPoweredOnState,
|
||||
OBCIErrorInvalidByteLength: errorInvalidByteLength,
|
||||
OBCIErrorInvalidByteStart: errorInvalidByteStart,
|
||||
OBCIErrorInvalidByteStop: errorInvalidByteStop,
|
||||
OBCIErrorInvalidType: errorInvalidType,
|
||||
OBCIErrorTimeSyncIsNull: errorTimeSyncIsNull,
|
||||
OBCIErrorTimeSyncNoComma: errorTimeSyncNoComma,
|
||||
OBCIErrorUndefinedOrNullInput: errorUndefinedOrNullInput,
|
||||
/** Max Master Buffer Size */
|
||||
OBCIMasterBufferSize: obciMasterBufferSize,
|
||||
/** Impedance Setter Maker */
|
||||
getImpedanceSetter: impedanceSetter,
|
||||
/** Impedance */
|
||||
OBCIImpedanceTextBad: obciImpedanceTextBad,
|
||||
OBCIImpedanceTextGood: obciImpedanceTextGood,
|
||||
OBCIImpedanceTextInit: obciImpedanceTextInit,
|
||||
OBCIImpedanceTextOk: obciImpedanceTextOk,
|
||||
OBCIImpedanceTextNone: obciImpedanceTextNone,
|
||||
OBCIImpedanceThresholdBadMax: obciImpedanceThresholdBadMax,
|
||||
OBCIImpedanceSeriesResistor: obciImpedanceSeriesResistor,
|
||||
getTextForRawImpedance: (value) => {
|
||||
if (value > obciImpedanceThresholdGoodMin && value < obciImpedanceThresholdGoodMax) {
|
||||
return obciImpedanceTextGood;
|
||||
} else if (value > obciImpedanceThresholdOkMin && value < obciImpedanceThresholdOkMax) {
|
||||
return obciImpedanceTextOk;
|
||||
} else if (value > obciImpedanceThresholdBadMin && value < obciImpedanceThresholdBadMax) {
|
||||
return obciImpedanceTextBad;
|
||||
} else {
|
||||
return obciImpedanceTextNone;
|
||||
}
|
||||
},
|
||||
/** Simulator */
|
||||
OBCISimulatorPortName: obciSimulatorPortName,
|
||||
/** Possible Simulator Line Noise injections */
|
||||
OBCISimulatorLineNoiseHz60: obciSimulatorLineNoiseHz60,
|
||||
OBCISimulatorLineNoiseHz50: obciSimulatorLineNoiseHz50,
|
||||
OBCISimulatorLineNoiseNone: obciSimulatorLineNoiseNone,
|
||||
/** Possible Simulator Fragmentation modes */
|
||||
OBCISimulatorFragmentationRandom: obciSimulatorFragmentationRandom,
|
||||
OBCISimulatorFragmentationFullBuffers: obciSimulatorFragmentationFullBuffers,
|
||||
OBCISimulatorFragmentationOneByOne: obciSimulatorFragmentationOneByOne,
|
||||
OBCISimulatorFragmentationNone: obciSimulatorFragmentationNone,
|
||||
/** Parse */
|
||||
OBCIParseFailure: obciParseFailure,
|
||||
OBCIParseSuccess: obciParseSuccess,
|
||||
/** Simulator Board Configurations */
|
||||
OBCISimulatorRawAux: obciSimulatorRawAux,
|
||||
OBCISimulatorStandard: obciSimulatorStandard,
|
||||
getVersionNumber,
|
||||
/** Emitters */
|
||||
OBCIEmitterBlePoweredUp: obciEmitterBlePoweredUp,
|
||||
OBCIEmitterClose: obciEmitterClose,
|
||||
OBCIEmitterDroppedPacket: obciEmitterDroppedPacket,
|
||||
OBCIEmitterError: obciEmitterError,
|
||||
OBCIEmitterGanglionFound: obciEmitterGanglionFound,
|
||||
OBCIEmitterImpedance: obciEmitterImpedance,
|
||||
OBCIEmitterMessage: obciEmitterMessage,
|
||||
OBCIEmitterQuery: obciEmitterQuery,
|
||||
OBCIEmitterRawDataPacket: obciEmitterRawDataPacket,
|
||||
OBCIEmitterReady: obciEmitterReady,
|
||||
OBCIEmitterSample: obciEmitterSample,
|
||||
OBCIEmitterSynced: obciEmitterSynced,
|
||||
/** Ganglion */
|
||||
OBCIGanglionBleSearchTime: obciGanglionBleSearchTime,
|
||||
OBCIGanglionByteIdRawData: obciGanglionByteIdRawData,
|
||||
OBCIGanglionByteIdSampleMax: obciGanglionByteIdSampleMax,
|
||||
OBCIGanglionByteIdSampleMin: obciGanglionByteIdSampleMin,
|
||||
OBCIGanglionByteIdAccel: obciGanglionByteIdAccel,
|
||||
OBCIGanglionByteIdImpedanceChannel1: obciGanglionByteIdImpedanceChannel1,
|
||||
OBCIGanglionByteIdImpedanceChannel2: obciGanglionByteIdImpedanceChannel2,
|
||||
OBCIGanglionByteIdImpedanceChannel3: obciGanglionByteIdImpedanceChannel3,
|
||||
OBCIGanglionByteIdImpedanceChannel4: obciGanglionByteIdImpedanceChannel4,
|
||||
OBCIGanglionByteIdImpedanceChannelReference: obciGanglionByteIdImpedanceChannelReference,
|
||||
OBCIGanglionByteIdMultiPacket: obciGanglionByteIdMultiPacket,
|
||||
OBCIGanglionByteIdMultiPacketStop: obciGanglionByteIdMultiPacketStop,
|
||||
OBCIGanglionMCP3912Gain: obciGanglionMCP3912Gain, // assumed gain setting for MCP3912. NEEDS TO BE ADJUSTABLE JM
|
||||
OBCIGanglionMCP3912Vref: obciGanglionMCP3912Vref, // reference voltage for ADC in MCP3912 set in hardware
|
||||
OBCIGanglionPacketSize: obciGanglionPacketSize,
|
||||
OBCIGanglionPacket: obciGanglionPacket,
|
||||
OBCIGanglionPrefix: obciGanglionPrefix,
|
||||
OBCIGanglionSamplesPerPacket: obciGanglionSamplesPerPacket,
|
||||
OBCIGanglionSyntheticDataEnable: obciGanglionSyntheticDataEnable,
|
||||
OBCIGanglionSyntheticDataDisable: obciGanglionSyntheticDataDisable,
|
||||
OBCIGanglionImpedanceStart: obciGanglionImpedanceStart,
|
||||
OBCIGanglionImpedanceStop: obciGanglionImpedanceStop,
|
||||
OBCIGanglionScaleFactorPerCountVolts: obciGanglionScaleFactorPerCountVolts,
|
||||
/** Simblee */
|
||||
SimbleeUuidService: simbleeUuidService,
|
||||
SimbleeUuidReceive: simbleeUuidReceive,
|
||||
SimbleeUuidSend: simbleeUuidSend,
|
||||
SimbleeUuidDisconnect: simbleeUuidDisconnect,
|
||||
/** Noble */
|
||||
OBCINobleEmitterPeripheralConnect: obciNobleEmitterPeripheralConnect,
|
||||
OBCINobleEmitterPeripheralDisconnect: obciNobleEmitterPeripheralDisconnect,
|
||||
OBCINobleEmitterPeripheralDiscover: obciNobleEmitterPeripheralDiscover,
|
||||
OBCINobleEmitterPeripheralServicesDiscover: obciNobleEmitterPeripheralServicesDiscover,
|
||||
OBCINobleEmitterServiceCharacteristicsDiscover: obciNobleEmitterServiceCharacteristicsDiscover,
|
||||
OBCINobleEmitterServiceRead: obciNobleEmitterServiceRead,
|
||||
OBCINobleEmitterDiscover: obciNobleEmitterDiscover,
|
||||
OBCINobleEmitterScanStart: obciNobleEmitterScanStart,
|
||||
OBCINobleEmitterScanStop: obciNobleEmitterScanStop,
|
||||
OBCINobleEmitterStateChange: obciNobleEmitterStateChange,
|
||||
OBCINobleStatePoweredOn: obciNobleStatePoweredOn
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This function is used to extract the major version from a github
|
||||
* version string.
|
||||
* @returns {Number} The major version number
|
||||
*/
|
||||
function getVersionNumber (versionStr) {
|
||||
return Number(versionStr[1]);
|
||||
}
|
||||
@@ -0,0 +1,877 @@
|
||||
'use strict';
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const _ = require('lodash');
|
||||
const noble = require('noble');
|
||||
const util = require('util');
|
||||
// Local imports
|
||||
const utils = require('./openBCIGanglionUtils');
|
||||
const k = require('./openBCIConstants');
|
||||
const openBCIUtils = require('./openBCIUtils');
|
||||
const clone = require('clone');
|
||||
|
||||
const _options = {
|
||||
nobleAutoStart: true,
|
||||
nobleScanOnPowerOn: true,
|
||||
sendCounts: false,
|
||||
simulate: false,
|
||||
simulatorBoardFailure: false,
|
||||
simulatorHasAccelerometer: true,
|
||||
simulatorInternalClockDrift: 0,
|
||||
simulatorInjectAlpha: true,
|
||||
simulatorInjectLineNoise: [k.OBCISimulatorLineNoiseHz60, k.OBCISimulatorLineNoiseHz50, k.OBCISimulatorLineNoiseNone],
|
||||
simulatorSampleRate: 200,
|
||||
verbose: false,
|
||||
debug: false
|
||||
};
|
||||
|
||||
function Ganglion (options) {
|
||||
if (!(this instanceof Ganglion)) {
|
||||
return new Ganglion(options);
|
||||
}
|
||||
|
||||
options = (typeof options !== 'function') && options || {};
|
||||
let opts = {};
|
||||
|
||||
/** Configuring Options */
|
||||
let o;
|
||||
for (o in _options) {
|
||||
var userOption = (o in options) ? o : o.toLowerCase();
|
||||
var userValue = options[userOption];
|
||||
delete options[userOption];
|
||||
|
||||
if (typeof _options[o] === 'object') {
|
||||
// an array specifying a list of choices
|
||||
// if the choice is not in the list, the first one is defaulted to
|
||||
|
||||
if (_options[o].indexOf(userValue) !== -1) {
|
||||
opts[o] = userValue;
|
||||
} else {
|
||||
opts[o] = _options[o][0];
|
||||
}
|
||||
} else {
|
||||
// anything else takes the user value if provided, otherwise is a default
|
||||
|
||||
if (userValue !== undefined) {
|
||||
opts[o] = userValue;
|
||||
} else {
|
||||
opts[o] = _options[o];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (o in options) throw new Error('"' + o + '" is not a valid option');
|
||||
|
||||
// Set to global options object
|
||||
this.options = clone(opts);
|
||||
|
||||
/** Private Properties (keep alphabetical) */
|
||||
this._connected = false;
|
||||
this._decompressedSamples = new Array(3);
|
||||
this._droppedPacketCounter = 0;
|
||||
this._lastDroppedPacket = null;
|
||||
this._lastPacket = null;
|
||||
this._localName = null;
|
||||
this._multiPacketBuffer = null;
|
||||
this._packetCounter = 0;
|
||||
this._peripheral = null;
|
||||
this._scanning = false;
|
||||
this._sendCharacteristic = null;
|
||||
this._streaming = false;
|
||||
|
||||
/** Public Properties (keep alphabetical) */
|
||||
this.peripheralArray = [];
|
||||
this.ganglionPeripheralArray = [];
|
||||
this.previousPeripheralArray = [];
|
||||
this.manualDisconnect = false;
|
||||
|
||||
/** Initializations */
|
||||
if (this.options.nobleAutoStart) this._nobleInit(); // It get's the noble going
|
||||
for (var i = 0; i < 3; i++) {
|
||||
this._decompressedSamples[i] = [0, 0, 0, 0];
|
||||
}
|
||||
}
|
||||
|
||||
// This allows us to use the emitter class freely outside of the module
|
||||
util.inherits(Ganglion, EventEmitter);
|
||||
|
||||
/**
|
||||
* @description The essential precursor method to be called initially to establish a
|
||||
* ble connection to the OpenBCI ganglion board.
|
||||
* @param id {String | Object} - a string local name or peripheral object
|
||||
* @returns {Promise} if the board was able to connect.
|
||||
* @author AJ Keller (@pushtheworldllc)
|
||||
*/
|
||||
Ganglion.prototype.connect = function (id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (_.isString(id)) {
|
||||
utils.getPeripheralWithLocalName(this.ganglionPeripheralArray, id)
|
||||
.then((p) => {
|
||||
this._nobleConnect(p);
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
} else if (_.isObject(id)) {
|
||||
this._nobleConnect(id)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
} else {
|
||||
reject(k.OBCIErrorInvalidByteLength);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Called once when for any reason the ble connection is no longer open.
|
||||
* @private
|
||||
*/
|
||||
Ganglion.prototype._disconnected = function () {
|
||||
this._streaming = false;
|
||||
|
||||
// Clean up _noble
|
||||
// TODO: Figure out how to fire function on process ending from inside module
|
||||
// noble.removeListener('discover', this._nobleOnDeviceDiscoveredCallback);
|
||||
|
||||
if (this._peripheral) {
|
||||
this._peripheral.removeAllListeners('servicesDiscover');
|
||||
this._peripheral.removeAllListeners('connect');
|
||||
this._peripheral.removeAllListeners('disconnect');
|
||||
}
|
||||
|
||||
// _peripheral = null;
|
||||
if (this.options.verbose) console.log('Disconnected');
|
||||
if (!this.manualDisconnect) {
|
||||
this.autoReconnect();
|
||||
}
|
||||
|
||||
this.emit('close');
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Closes the serial port. Waits for stop streaming command to
|
||||
* be sent if currently streaming.
|
||||
* @param stopStreaming {Boolean} (optional) - True if you want to stop streaming before disconnecting.
|
||||
* @returns {Promise} - fulfilled by a successful close of the serial port object, rejected otherwise.
|
||||
* @author AJ Keller (@pushtheworldllc)
|
||||
*/
|
||||
Ganglion.prototype.disconnect = function (stopStreaming) {
|
||||
if (!this.isConnected()) return Promise.reject('no board connected');
|
||||
|
||||
// no need for timeout here; streamStop already performs a delay
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (stopStreaming) {
|
||||
if (this.isStreaming()) {
|
||||
if (this.options.verbose) console.log('stop streaming');
|
||||
return this.streamStop();
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// serial emitting 'close' will call _disconnected
|
||||
if (this._peripheral) {
|
||||
this._peripheral.disconnect((err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reject('no peripheral to disconnect');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Checks if the driver is connected to a board.
|
||||
* @returns {boolean} - True if connected.
|
||||
*/
|
||||
Ganglion.prototype.isConnected = function () {
|
||||
return this._connected;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Checks if the board is currently sending samples.
|
||||
* @returns {boolean} - True if streaming.
|
||||
*/
|
||||
Ganglion.prototype.isStreaming = function () {
|
||||
return this._streaming;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Checks if noble is currently scanning.
|
||||
* @returns {boolean} - True if streaming.
|
||||
*/
|
||||
Ganglion.prototype.isSearching = function () {
|
||||
return this._scanning;
|
||||
};
|
||||
|
||||
Ganglion.prototype.getLocalName = function () {
|
||||
return this._localName;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Sends a start streaming command to the board.
|
||||
* @returns {Promise} indicating if the signal was able to be sent.
|
||||
* Note: You must have successfully connected to an OpenBCI board using the connect
|
||||
* method. Just because the signal was able to be sent to the board, does not
|
||||
* mean the board will start streaming.
|
||||
* @author AJ Keller (@pushtheworldllc)
|
||||
*/
|
||||
Ganglion.prototype.streamStart = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.isStreaming()) return reject('Error [.streamStart()]: Already streaming');
|
||||
this._streaming = true;
|
||||
this.write(k.OBCIStreamStart)
|
||||
.then(() => {
|
||||
if (this.options.verbose) console.log('Sent stream start to board.');
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Sends a stop streaming command to the board.
|
||||
* @returns {Promise} indicating if the signal was able to be sent.
|
||||
* Note: You must have successfully connected to an OpenBCI board using the connect
|
||||
* method. Just because the signal was able to be sent to the board, does not
|
||||
* mean the board stopped streaming.
|
||||
* @author AJ Keller (@pushtheworldllc)
|
||||
*/
|
||||
Ganglion.prototype.streamStop = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isStreaming()) return reject('Error [.streamStop()]: No stream to stop');
|
||||
this._streaming = false;
|
||||
this.write(k.OBCIStreamStop)
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Puts the board in synthetic data generation mode.
|
||||
* @returns {Promise} indicating if the signal was able to be sent.
|
||||
* Note: You must have successfully connected to an OpenBCI board using the connect
|
||||
* method. Just because the signal was able to be sent to the board, does not
|
||||
* mean the board will start streaming.
|
||||
* @author AJ Keller (@pushtheworldllc)
|
||||
*/
|
||||
Ganglion.prototype.syntheticEnable = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.write(k.OBCIGanglionSyntheticDataEnable)
|
||||
.then(() => {
|
||||
if (this.options.verbose) console.log('Enabled synthetic data mode.');
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Sends a stop streaming command to the board.
|
||||
* @returns {Promise} indicating if the signal was able to be sent.
|
||||
* Note: You must have successfully connected to an OpenBCI board using the connect
|
||||
* method. Just because the signal was able to be sent to the board, does not
|
||||
* mean the board stopped streaming.
|
||||
* @author AJ Keller (@pushtheworldllc)
|
||||
*/
|
||||
Ganglion.prototype.syntheticDisable = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.write(k.OBCIGanglionSyntheticDataDisable)
|
||||
.then(() => {
|
||||
if (this.options.verbose) console.log('Disabled synthetic data mode.');
|
||||
resolve();
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Should be used to send data to the board
|
||||
* @param data {Buffer | String} - The data to write out
|
||||
* @returns {Promise} if signal was able to be sent
|
||||
* @author AJ Keller (@pushtheworldllc)
|
||||
*/
|
||||
Ganglion.prototype.write = function (data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this._sendCharacteristic) {
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
data = new Buffer(data);
|
||||
}
|
||||
if (this.options.debug) openBCIUtils.debugBytes('>>>', data);
|
||||
this._sendCharacteristic.write(data);
|
||||
resolve();
|
||||
} else {
|
||||
reject('Send characteristic not set, please call connect method');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description This function is used as a convenience method to determine how many
|
||||
* channels the current board is using.
|
||||
* @returns {Number} A number
|
||||
* Note: This is dependent on if you configured the board correctly on setup options
|
||||
* @author AJ Keller (@pushtheworldllc)
|
||||
*/
|
||||
Ganglion.prototype.numberOfChannels = function () {
|
||||
return k.OBCINumberOfChannelsGanglion;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description List available peripherals so the user can choose a device when not
|
||||
* automatically found.
|
||||
* @returns {Promise} - On fulfill will contain a ganglion.
|
||||
*/
|
||||
Ganglion.prototype.searchStart = function (maxSearchTime) {
|
||||
const searchTime = maxSearchTime || k.OBCIGanglionBleSearchTime;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._searchTimeout = setTimeout(() => {
|
||||
this._nobleScanStop().catch(reject);
|
||||
reject('Timeout: Unable to find Ganglion');
|
||||
}, searchTime);
|
||||
|
||||
this._nobleScanStart().catch((err) => {
|
||||
if (err !== k.OBCIErrorNobleAlreadyScanning) { // If it's already scanning
|
||||
clearTimeout(this._searchTimeout);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Called to end a search.
|
||||
*/
|
||||
Ganglion.prototype.searchStop = function () {
|
||||
return this._nobleScanStop();
|
||||
};
|
||||
|
||||
/**
|
||||
* Event driven function called when a new device is discovered while scanning.
|
||||
* @param peripheral {Object} Peripheral object from noble.
|
||||
* @private
|
||||
*/
|
||||
Ganglion.prototype._nobleOnDeviceDiscoveredCallback = function (peripheral) {
|
||||
// if(this.options.verbose) console.log(peripheral.advertisement);
|
||||
this.peripheralArray.push(peripheral);
|
||||
if (utils.isPeripheralGanglion(peripheral)) {
|
||||
if (this.options.verbose) console.log('Found ganglion!');
|
||||
if (_.isUndefined(_.find(this.ganglionPeripheralArray, (p) => {
|
||||
return p.advertisement.localName === peripheral.advertisement.localName;
|
||||
}))) {
|
||||
this.ganglionPeripheralArray.push(peripheral);
|
||||
}
|
||||
this.emit(k.OBCIEmitterGanglionFound, peripheral);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Call to add the noble event listeners.
|
||||
* @private
|
||||
*/
|
||||
Ganglion.prototype._nobleInit = function () {
|
||||
noble.on(k.OBCINobleEmitterStateChange, (state) => {
|
||||
// TODO: send state change error to gui
|
||||
|
||||
// If the peripheral array is empty, do a scan to fill it.
|
||||
if (state === k.OBCINobleStatePoweredOn) {
|
||||
if (this.options.verbose) console.log('Bluetooth powered on');
|
||||
this.emit(k.OBCIEmitterBlePoweredUp);
|
||||
if (this.options.nobleScanOnPowerOn) {
|
||||
this._nobleScanStart().catch((err) => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
if (this.peripheralArray.length === 0) {
|
||||
}
|
||||
} else {
|
||||
if (this.isSearching()) {
|
||||
this._nobleScanStop().catch((err) => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
noble.on(k.OBCINobleEmitterDiscover, this._nobleOnDeviceDiscoveredCallback.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* Call to destroy the noble event emitters.
|
||||
* @private
|
||||
*/
|
||||
Ganglion.prototype._nobleDestroy = function () {
|
||||
noble.removeAllListeners(k.OBCINobleEmitterStateChange);
|
||||
noble.removeAllListeners(k.OBCINobleEmitterDiscover);
|
||||
};
|
||||
|
||||
Ganglion.prototype._nobleReady = function () {
|
||||
return noble.state === k.OBCINobleStatePoweredOn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Call to perform a scan to get a list of peripherals.
|
||||
* @returns {global.Promise|Promise}
|
||||
* @private
|
||||
*/
|
||||
Ganglion.prototype._nobleScanStart = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.isSearching()) return reject(k.OBCIErrorNobleAlreadyScanning);
|
||||
if (!this._nobleReady()) return reject(k.OBCIErrorNobleNotInPoweredOnState);
|
||||
|
||||
this.peripheralArray = [];
|
||||
noble.once(k.OBCINobleEmitterScanStart, () => {
|
||||
if (this.options.verbose) console.log('Scan started');
|
||||
this._scanning = true;
|
||||
resolve();
|
||||
});
|
||||
// Only look so simblee ble devices and allow duplicates (multiple ganglions)
|
||||
// noble.startScanning([k.SimbleeUuidService], true);
|
||||
noble.startScanning([], false);
|
||||
});
|
||||
};
|
||||
|
||||
Ganglion.prototype._nobleScanStop = function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.isSearching()) return reject(k.OBCIErrorNobleNotAlreadyScanning);
|
||||
if (this.options.verbose) console.log(`Stopping scan`);
|
||||
|
||||
noble.once(k.OBCINobleEmitterScanStop, () => {
|
||||
this._scanning = false;
|
||||
if (this.options.verbose) console.log('Scan stopped');
|
||||
resolve();
|
||||
});
|
||||
// Stop noble from scanning
|
||||
noble.stopScanning();
|
||||
});
|
||||
};
|
||||
|
||||
Ganglion.prototype._nobleConnect = function (peripheral) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.isConnected()) return reject('already connected!');
|
||||
|
||||
this._peripheral = peripheral;
|
||||
this._localName = peripheral.advertisement.localName;
|
||||
// if (_.contains(_peripheral.advertisement.localName, rfduino.localNamePrefix)) {
|
||||
// TODO: slice first 8 of localName and see if that is ganglion
|
||||
// here is where we can capture the advertisement data from the rfduino and check to make sure its ours
|
||||
if (this.options.verbose) console.log('Device is advertising \'' + this._peripheral.advertisement.localName + '\' service.');
|
||||
// TODO: filter based on advertising name ie make sure we are looking for the right thing
|
||||
// if (this.options.verbose) console.log("serviceUUID: " + this._peripheral.advertisement.serviceUuids);
|
||||
|
||||
this._peripheral.on(k.OBCINobleEmitterPeripheralConnect, () => {
|
||||
// if (this.options.verbose) console.log("got connect event");
|
||||
this._peripheral.discoverServices();
|
||||
if (this._scanning) this._nobleScanStop();
|
||||
});
|
||||
|
||||
this._peripheral.on(k.OBCINobleEmitterPeripheralDisconnect, () => {
|
||||
this._disconnected();
|
||||
});
|
||||
|
||||
this._peripheral.on(k.OBCINobleEmitterPeripheralServicesDiscover, (services) => {
|
||||
let rfduinoService;
|
||||
|
||||
for (var i = 0; i < services.length; i++) {
|
||||
if (services[i].uuid === k.SimbleeUuidService) {
|
||||
rfduinoService = services[i];
|
||||
// if (this.options.verbose) console.log("Found simblee Service");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rfduinoService) {
|
||||
reject('Couldn\'t find the simblee service.');
|
||||
}
|
||||
|
||||
rfduinoService.on(k.OBCINobleEmitterServiceCharacteristicsDiscover, (characteristics) => {
|
||||
// if (this.options.verbose) console.log('Discovered ' + characteristics.length + ' service characteristics');
|
||||
var receiveCharacteristic;
|
||||
|
||||
for (var i = 0; i < characteristics.length; i++) {
|
||||
// console.log(characteristics[i].uuid);
|
||||
if (characteristics[i].uuid === k.SimbleeUuidReceive) {
|
||||
receiveCharacteristic = characteristics[i];
|
||||
}
|
||||
if (characteristics[i].uuid === k.SimbleeUuidSend) {
|
||||
// if (this.options.verbose) console.log("Found sendCharacteristicUUID");
|
||||
this._sendCharacteristic = characteristics[i];
|
||||
if (this.options.verbose) console.log('connected');
|
||||
this._connected = true;
|
||||
this.emit(k.OBCIEmitterReady);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
if (receiveCharacteristic) {
|
||||
receiveCharacteristic.on(k.OBCINobleEmitterServiceRead, (data) => {
|
||||
// TODO: handle all the data, both streaming and not
|
||||
this._processBytes(data);
|
||||
});
|
||||
|
||||
// if (this.options.verbose) console.log('Subscribing for data notifications');
|
||||
receiveCharacteristic.notify(true);
|
||||
}
|
||||
});
|
||||
|
||||
rfduinoService.discoverCharacteristics();
|
||||
});
|
||||
|
||||
// if (this.options.verbose) console.log("Calling connect");
|
||||
|
||||
this._peripheral.connect((err) => {
|
||||
if (err) {
|
||||
if (this.options.verbose) console.log(`Unable to connect with error: ${err}`);
|
||||
this._connected = false;
|
||||
this._peripheral = null;
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Ganglion.prototype.autoReconnect = function () {
|
||||
// TODO: send back reconnect status, or reconnect fail
|
||||
if (noble.state === k.OBCINobleStatePoweredOn) {
|
||||
this._nobleScanStart();
|
||||
} else {
|
||||
console.warn('BLE not AVAILABLE');
|
||||
}
|
||||
};
|
||||
|
||||
Ganglion.prototype.decompressSamples = function (receivedDeltas) {
|
||||
// add the delta to the previous value
|
||||
for (let i = 1; i < 3; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
this._decompressedSamples[i][j] = this._decompressedSamples[i - 1][j] - receivedDeltas[i - 1][j];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ganglion.prototype.decompressDeltas = function (buffer) {
|
||||
let D = new Array(k.OBCIGanglionSamplesPerPacket); // 2
|
||||
D[0] = [0, 0, 0, 0];
|
||||
D[1] = [0, 0, 0, 0];
|
||||
|
||||
let receivedDeltas = [];
|
||||
for (let i = 0; i < k.OBCIGanglionSamplesPerPacket; i++) {
|
||||
receivedDeltas.push([0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
let miniBuf;
|
||||
|
||||
// Sample 1 - Channel 1
|
||||
miniBuf = new Buffer(
|
||||
[
|
||||
(buffer[0] >> 5),
|
||||
((buffer[0] & 0x1F) << 3) | (buffer[1] >> 5),
|
||||
((buffer[1] & 0x1F) << 3) | (buffer[2] >> 5)
|
||||
]
|
||||
);
|
||||
receivedDeltas[0][0] = utils.convert19bitAsInt32(miniBuf);
|
||||
|
||||
// Sample 1 - Channel 2
|
||||
miniBuf = new Buffer(
|
||||
[
|
||||
(buffer[2] & 0x1F) >> 2,
|
||||
(buffer[2] << 6) | (buffer[3] >> 2),
|
||||
(buffer[3] << 6) | (buffer[4] >> 2)
|
||||
]);
|
||||
// miniBuf = new Buffer([(buffer[2] & 0x1F), buffer[3], buffer[4] >> 2]);
|
||||
receivedDeltas[0][1] = utils.convert19bitAsInt32(miniBuf);
|
||||
|
||||
// Sample 1 - Channel 3
|
||||
miniBuf = new Buffer(
|
||||
[
|
||||
((buffer[4] & 0x03) << 1) | (buffer[5] >> 7),
|
||||
((buffer[5] & 0x7F) << 1) | (buffer[6] >> 7),
|
||||
((buffer[6] & 0x7F) << 1) | (buffer[7] >> 7)
|
||||
]);
|
||||
receivedDeltas[0][2] = utils.convert19bitAsInt32(miniBuf);
|
||||
|
||||
// Sample 1 - Channel 4
|
||||
miniBuf = new Buffer(
|
||||
[
|
||||
((buffer[7] & 0x7F) >> 4),
|
||||
((buffer[7] & 0x0F) << 4) | (buffer[8] >> 4),
|
||||
((buffer[8] & 0x0F) << 4) | (buffer[9] >> 4)
|
||||
]);
|
||||
receivedDeltas[0][3] = utils.convert19bitAsInt32(miniBuf);
|
||||
|
||||
// Sample 2 - Channel 1
|
||||
miniBuf = new Buffer(
|
||||
[
|
||||
((buffer[9] & 0x0F) >> 1),
|
||||
(buffer[9] << 7) | (buffer[10] >> 1),
|
||||
(buffer[10] << 7) | (buffer[11] >> 1)
|
||||
]);
|
||||
receivedDeltas[1][0] = utils.convert19bitAsInt32(miniBuf);
|
||||
|
||||
// Sample 2 - Channel 2
|
||||
miniBuf = new Buffer(
|
||||
[
|
||||
((buffer[11] & 0x01) << 2) | (buffer[12] >> 6),
|
||||
(buffer[12] << 2) | (buffer[13] >> 6),
|
||||
(buffer[13] << 2) | (buffer[14] >> 6)
|
||||
]);
|
||||
receivedDeltas[1][1] = utils.convert19bitAsInt32(miniBuf);
|
||||
|
||||
// Sample 2 - Channel 3
|
||||
miniBuf = new Buffer(
|
||||
[
|
||||
((buffer[14] & 0x38) >> 3),
|
||||
((buffer[14] & 0x07) << 5) | ((buffer[15] & 0xF8) >> 3),
|
||||
((buffer[15] & 0x07) << 5) | ((buffer[16] & 0xF8) >> 3)
|
||||
]);
|
||||
receivedDeltas[1][2] = utils.convert19bitAsInt32(miniBuf);
|
||||
|
||||
// Sample 2 - Channel 4
|
||||
miniBuf = new Buffer([(buffer[16] & 0x07), buffer[17], buffer[18]]);
|
||||
receivedDeltas[1][3] = utils.convert19bitAsInt32(miniBuf);
|
||||
|
||||
return receivedDeltas;
|
||||
};
|
||||
|
||||
Ganglion.prototype._processBytes = function (data) {
|
||||
if (this.options.debug) openBCIUtils.debugBytes('<<', data);
|
||||
this.lastPacket = data;
|
||||
let byteId = parseInt(data[0]);
|
||||
if (byteId <= k.OBCIGanglionByteIdSampleMax) {
|
||||
if (byteId === k.OBCIGanglionByteIdRawData) {
|
||||
this._processUncompressedData(data);
|
||||
} else {
|
||||
this._processCompressedData(data);
|
||||
}
|
||||
} else {
|
||||
switch (byteId) {
|
||||
case k.OBCIGanglionByteIdAccel:
|
||||
this._processAccel(data);
|
||||
break;
|
||||
case k.OBCIGanglionByteIdMultiPacket:
|
||||
this._processMultiBytePacket(data);
|
||||
break;
|
||||
case k.OBCIGanglionByteIdMultiPacketStop:
|
||||
this._processMultiBytePacketStop(data);
|
||||
break;
|
||||
case k.OBCIGanglionByteIdImpedanceChannel1:
|
||||
case k.OBCIGanglionByteIdImpedanceChannel2:
|
||||
case k.OBCIGanglionByteIdImpedanceChannel3:
|
||||
case k.OBCIGanglionByteIdImpedanceChannel4:
|
||||
case k.OBCIGanglionByteIdImpedanceChannelReference:
|
||||
this._processImpedanceData(data);
|
||||
break;
|
||||
default:
|
||||
this._processOtherData(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ganglion.prototype._processAccel = function (data) {
|
||||
openBCIUtils.debugBytes('Accel <<< ', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Process an compressed packet of data.
|
||||
* @param data {Buffer}
|
||||
* Data packet buffer from noble.
|
||||
* @private
|
||||
*/
|
||||
Ganglion.prototype._processCompressedData = function (data) {
|
||||
// check for dropped packet
|
||||
if (parseInt(data[0]) - this._packetCounter !== 1) {
|
||||
this.lastDroppedPacket = parseInt(data[0]); // - 2;
|
||||
// var retryString = "&"+dropped;
|
||||
// var reset = Buffer.from(retryString);
|
||||
// _sendCharacteristic.write(reset);
|
||||
this._droppedPacketCounter++;
|
||||
this.emit(k.OBCIEmitterDroppedPacket, [parseInt(data[0]) - 1]);
|
||||
if (this.options.verbose) console.error('\t>>>PACKET DROP<<< ' + this._packetCounter + ' ' + this.lastDroppedPacket + ' ' + this._droppedPacketCounter);
|
||||
}
|
||||
|
||||
let buffer = data.slice(k.OBCIGanglionPacket.dataStart, k.OBCIGanglionPacket.dataStop);
|
||||
|
||||
if (k.getVersionNumber(process.version) >= 6) {
|
||||
// From introduced in node version 6.x.x
|
||||
buffer = Buffer.from(buffer);
|
||||
} else {
|
||||
buffer = new Buffer(buffer);
|
||||
}
|
||||
|
||||
// Decompress the buffer into array
|
||||
this.decompressSamples(this.decompressDeltas(buffer));
|
||||
|
||||
this._packetCounter = parseInt(data[0]);
|
||||
|
||||
const sample1 = this._buildSample(this._packetCounter * 2 - 1, this._decompressedSamples[1]);
|
||||
this.emit(k.OBCIEmitterSample, sample1);
|
||||
|
||||
const sample2 = this._buildSample(this._packetCounter * 2, this._decompressedSamples[2]);
|
||||
this.emit(k.OBCIEmitterSample, sample2);
|
||||
|
||||
// Rotate the 0 position for next time
|
||||
for (let i = 0; i < k.OBCINumberOfChannelsGanglion; i++) {
|
||||
this._decompressedSamples[0][i] = this._decompressedSamples[2][i];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Process and emit an impedance value
|
||||
* @param data {Buffer}
|
||||
* @private
|
||||
*/
|
||||
Ganglion.prototype._processImpedanceData = function (data) {
|
||||
if (this.options.verbose) openBCIUtils.debugBytes('Impedance <<< ', data);
|
||||
const byteId = parseInt(data[0]);
|
||||
let channelNumber;
|
||||
switch (byteId) {
|
||||
case k.OBCIGanglionByteIdImpedanceChannel1:
|
||||
channelNumber = 1;
|
||||
break;
|
||||
case k.OBCIGanglionByteIdImpedanceChannel2:
|
||||
channelNumber = 2;
|
||||
break;
|
||||
case k.OBCIGanglionByteIdImpedanceChannel3:
|
||||
channelNumber = 3;
|
||||
break;
|
||||
case k.OBCIGanglionByteIdImpedanceChannel4:
|
||||
channelNumber = 4;
|
||||
break;
|
||||
case k.OBCIGanglionByteIdImpedanceChannelReference:
|
||||
channelNumber = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
let output = {
|
||||
channelNumber: channelNumber,
|
||||
impedanceValue: 0
|
||||
};
|
||||
|
||||
let end = data.length;
|
||||
|
||||
while (_.isNaN(Number(data.slice(1, end))) && end !== 0) {
|
||||
end--;
|
||||
}
|
||||
|
||||
if (end !== 0) {
|
||||
output.impedanceValue = Number(data.slice(1, end));
|
||||
}
|
||||
|
||||
this.emit('impedance', output);
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to stack multi packet buffers into the multi packet buffer. This is finally emitted when a stop packet byte id
|
||||
* is received.
|
||||
* @param data {Buffer}
|
||||
* The multi packet buffer.
|
||||
* @private
|
||||
*/
|
||||
Ganglion.prototype._processMultiBytePacket = function (data) {
|
||||
if (this._multiPacketBuffer) {
|
||||
this._multiPacketBuffer = Buffer.concat([this._multiPacketBuffer, data.slice(k.OBCIGanglionPacket.dataStart, k.OBCIGanglionPacket.dataStop)]);
|
||||
} else {
|
||||
this._multiPacketBuffer = data.slice(k.OBCIGanglionPacket.dataStart, k.OBCIGanglionPacket.dataStop);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds the `data` buffer to the multi packet buffer and emits the buffer as 'message'
|
||||
* @param data {Buffer}
|
||||
* The multi packet stop buffer.
|
||||
* @private
|
||||
*/
|
||||
Ganglion.prototype._processMultiBytePacketStop = function (data) {
|
||||
this._processMultiBytePacket(data);
|
||||
this.emit(k.OBCIEmitterMessage, this._multiPacketBuffer);
|
||||
this.destroyMultiPacketBuffer();
|
||||
};
|
||||
|
||||
/**
|
||||
* The default route when a ByteId is not recognized.
|
||||
* @param data {Buffer}
|
||||
* @private
|
||||
*/
|
||||
Ganglion.prototype._processOtherData = function (data) {
|
||||
openBCIUtils.debugBytes('OtherData <<< ', data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Process an uncompressed packet of data.
|
||||
* @param data {Buffer}
|
||||
* Data packet buffer from noble.
|
||||
* @private
|
||||
*/
|
||||
Ganglion.prototype._processUncompressedData = function (data) {
|
||||
let start = 1;
|
||||
|
||||
// Resets the packet counter back to zero
|
||||
this._packetCounter = k.OBCIGanglionByteIdRawData; // used to find dropped packets
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this._decompressedSamples[0][i] = interpret24bitAsInt32(data, start); // seed the decompressor
|
||||
start += 3;
|
||||
}
|
||||
|
||||
const newSample = this._buildSample(0, this._decompressedSamples[0]);
|
||||
this.emit(k.OBCIEmitterSample, newSample);
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroys the multi packet buffer.
|
||||
*/
|
||||
Ganglion.prototype.destroyMultiPacketBuffer = function () {
|
||||
this._multiPacketBuffer = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get's the multi packet buffer.
|
||||
* @return {null|Buffer} - Can be null if no multi packets recieved.
|
||||
*/
|
||||
Ganglion.prototype.getMutliPacketBuffer = function () {
|
||||
return this._multiPacketBuffer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a sample object from an array and sample number.
|
||||
* @param sampleNumber
|
||||
* @param rawData
|
||||
* @return {{sampleNumber: *}}
|
||||
* @private
|
||||
*/
|
||||
Ganglion.prototype._buildSample = function (sampleNumber, rawData) {
|
||||
let sample = {
|
||||
sampleNumber: sampleNumber
|
||||
};
|
||||
if (this.options.sendCounts) {
|
||||
sample['channelDataCounts'] = rawData;
|
||||
} else {
|
||||
sample['channelData'] = [];
|
||||
for (let j = 0; j < k.OBCINumberOfChannelsGanglion; j++) {
|
||||
sample.channelData.push(rawData[j] * k.OBCIGanglionScaleFactorPerCountVolts);
|
||||
}
|
||||
}
|
||||
return sample;
|
||||
};
|
||||
|
||||
module.exports = Ganglion;
|
||||
|
||||
function interpret24bitAsInt32 (byteArray, index) {
|
||||
// little endian
|
||||
var newInt = (
|
||||
((0xFF & byteArray[index]) << 16) |
|
||||
((0xFF & byteArray[index + 1]) << 8) |
|
||||
(0xFF & byteArray[index + 2])
|
||||
);
|
||||
if ((newInt & 0x00800000) > 0) {
|
||||
newInt |= 0xFF000000;
|
||||
} else {
|
||||
newInt &= 0x00FFFFFF;
|
||||
}
|
||||
return newInt;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// const dgram = require('dgram');
|
||||
// const gaussian = require('gaussian');
|
||||
// const k = require('openBCIContants');
|
||||
|
||||
// let stream;
|
||||
|
||||
// var startStream = () => {
|
||||
// const intervalInMS = 1000 / k.OBCISampleRate200;
|
||||
// let sampleNumber = 0;
|
||||
// let sampleGenerator = randomSample(4, k.OBCISampleRate200, true, `60Hz`);
|
||||
//
|
||||
// var getSample = sampleNumber => {
|
||||
// let arr = getArrayFromSample(sampleGenerator(sampleNumber));
|
||||
// // console.log(`${sampleNumber},${arr[0].toString()},${arr[1].toString()},${arr[2].toString()},${arr[3].toString()}`);
|
||||
// return `${sampleNumber},${arr[0].toString()},${arr[1].toString()},${arr[2].toString()},${arr[3].toString()}`;
|
||||
// };
|
||||
//
|
||||
// stream = setInterval(() => {
|
||||
// let sampArray = getSample(sampleNumber);
|
||||
// // Send the packet to all clients
|
||||
// this.emit('read', samp);
|
||||
// // Increment the sample number
|
||||
// sampleNumber++;
|
||||
// }, intervalInMS);
|
||||
// };
|
||||
|
||||
/**
|
||||
* @description Mainly used by the simulator to convert a randomly generated sample into a std OpenBCI V3 Packet
|
||||
* @param sample - A sample object
|
||||
* @returns {Array}
|
||||
*/
|
||||
// function getArrayFromSample (sample) {
|
||||
// var array = [];
|
||||
// for (var i = 0; i < 4; i++) {
|
||||
// array.push(Math.floor(interpret24bitAsInt32(floatTo3ByteBuffer(sample.channelData[i]))));
|
||||
// }
|
||||
// return array;
|
||||
// }
|
||||
|
||||
// function floatTo3ByteBuffer (float) {
|
||||
// var intBuf = new Buffer(3); // 3 bytes for 24 bits
|
||||
// intBuf.fill(0); // Fill the buffer with 0s
|
||||
//
|
||||
// var temp = float / (k.OBCIGanglionMCP3912Vref / k.OBCIGanglionMCP3912Gain / (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 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}
|
||||
// */
|
||||
// var 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);
|
||||
//
|
||||
// // 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 => {
|
||||
// let sample = newSample();
|
||||
// for (var i = 0; i < numberOfChannels; i++) { // channels are 0 indexed
|
||||
// // This produces white noise
|
||||
// let 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:
|
||||
// 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;
|
||||
// break;
|
||||
// }
|
||||
// /**
|
||||
// * 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;
|
||||
// }
|
||||
//
|
||||
// return sample;
|
||||
// };
|
||||
// };
|
||||
//
|
||||
// function newSample (sampleNumber) {
|
||||
// if (sampleNumber || sampleNumber === 0) {
|
||||
// if (sampleNumber > 255) {
|
||||
// sampleNumber = 255;
|
||||
// }
|
||||
// } else {
|
||||
// sampleNumber = 0;
|
||||
// }
|
||||
// return {
|
||||
// sampleNumber: sampleNumber,
|
||||
// channelData: []
|
||||
// };
|
||||
// }
|
||||
@@ -0,0 +1,162 @@
|
||||
'use strict';
|
||||
const k = require('./openBCIConstants');
|
||||
const _ = require('underscore');
|
||||
|
||||
module.exports = {
|
||||
getPeripheralLocalNames,
|
||||
getPeripheralWithLocalName,
|
||||
/**
|
||||
* Converts a special ganglion 19 bit compressed number
|
||||
* The compressions uses the LSB, bit 1, as the signed bit, instead of using
|
||||
* the MSB. Therefore you must not look to the MSB for a sign extension, one
|
||||
* must look to the LSB, and the same rules applies, if it's a 1, then it's a
|
||||
* negative and if it's 0 then it's a positive number.
|
||||
* @param threeByteBuffer {Buffer}
|
||||
* A 3-byte buffer with only 19 bits of actual data.
|
||||
* @return {number} A signed integer.
|
||||
*/
|
||||
convert19bitAsInt32: (threeByteBuffer) => {
|
||||
let prefix = 0;
|
||||
|
||||
if (threeByteBuffer[2] & 0x01 > 0) {
|
||||
// console.log('\t\tNegative number')
|
||||
prefix = 0b1111111111111;
|
||||
}
|
||||
|
||||
return (prefix << 19) | (threeByteBuffer[0] << 16) | (threeByteBuffer[1] << 8) | threeByteBuffer[2];
|
||||
},
|
||||
/**
|
||||
* @description Very safely checks to see if the noble peripheral is a
|
||||
* ganglion by way of checking the local name property.
|
||||
*/
|
||||
isPeripheralGanglion: (peripheral) => {
|
||||
if (peripheral) {
|
||||
if (peripheral.hasOwnProperty('advertisement')) {
|
||||
if (peripheral.advertisement !== null && peripheral.advertisement.hasOwnProperty('localName')) {
|
||||
if (peripheral.advertisement.localName !== undefined && peripheral.advertisement.localName !== null) {
|
||||
if (peripheral.advertisement.localName.indexOf(k.OBCIGanglionPrefix) > -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
sampleAccel: () => {
|
||||
return new Buffer([k.OBCIGanglionByteIdAccel, 0, 0, 1, 0, 0, 2, 0, 0, 3]);
|
||||
},
|
||||
sampleCompressedData: (sampleNumber) => {
|
||||
return new Buffer(
|
||||
[
|
||||
sampleNumber, // 0
|
||||
0b00000000, // 0
|
||||
0b00000000, // 1
|
||||
0b00000000, // 2
|
||||
0b00000000, // 3
|
||||
0b00001000, // 4
|
||||
0b00000000, // 5
|
||||
0b00000101, // 6
|
||||
0b00000000, // 7
|
||||
0b00000000, // 8
|
||||
0b01001000, // 9
|
||||
0b00000000, // 10
|
||||
0b00001001, // 11
|
||||
0b11110000, // 12
|
||||
0b00000001, // 13
|
||||
0b10110000, // 14
|
||||
0b00000000, // 15
|
||||
0b00110000, // 16
|
||||
0b00000000, // 17
|
||||
0b00001000 // 18
|
||||
]);
|
||||
},
|
||||
sampleImpedanceChannel1: () => {
|
||||
return new Buffer([k.OBCIGanglionByteIdImpedanceChannel1, 0, 0, 1]);
|
||||
},
|
||||
sampleImpedanceChannel2: () => {
|
||||
return new Buffer([k.OBCIGanglionByteIdImpedanceChannel2, 0, 0, 1]);
|
||||
},
|
||||
sampleImpedanceChannel3: () => {
|
||||
return new Buffer([k.OBCIGanglionByteIdImpedanceChannel3, 0, 0, 1]);
|
||||
},
|
||||
sampleImpedanceChannel4: () => {
|
||||
return new Buffer([k.OBCIGanglionByteIdImpedanceChannel4, 0, 0, 1]);
|
||||
},
|
||||
sampleImpedanceChannelReference: () => {
|
||||
return new Buffer([k.OBCIGanglionByteIdImpedanceChannelReference, 0, 0, 1]);
|
||||
},
|
||||
sampleMultiBytePacket: (data) => {
|
||||
const bufPre = new Buffer([k.OBCIGanglionByteIdMultiPacket]);
|
||||
return Buffer.concat([bufPre, data]);
|
||||
},
|
||||
sampleMultiBytePacketStop: (data) => {
|
||||
const bufPre = new Buffer([k.OBCIGanglionByteIdMultiPacketStop]);
|
||||
return Buffer.concat([bufPre, data]);
|
||||
},
|
||||
sampleOtherData: (data) => {
|
||||
const bufPre = new Buffer([255]);
|
||||
return Buffer.concat([bufPre, data]);
|
||||
},
|
||||
sampleUncompressedData: () => {
|
||||
return new Buffer(
|
||||
[
|
||||
0b00000000, // 0
|
||||
0b00000000, // 1
|
||||
0b00000000, // 2
|
||||
0b00000001, // 3
|
||||
0b00000000, // 4
|
||||
0b00000000, // 5
|
||||
0b00000010, // 6
|
||||
0b00000000, // 7
|
||||
0b00000000, // 8
|
||||
0b00000011, // 9
|
||||
0b00000000, // 10
|
||||
0b00000000, // 11
|
||||
0b00000100, // 12
|
||||
0b00000001, // 13
|
||||
0b00000010, // 14
|
||||
0b00000011, // 15
|
||||
0b00000100, // 16
|
||||
0b00000101, // 17
|
||||
0b00000110, // 18
|
||||
0b00000111 // 19
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Get a list of local names from an array of peripherals
|
||||
*/
|
||||
function getPeripheralLocalNames (pArray) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var list = [];
|
||||
_.each(pArray, perif => {
|
||||
list.push(perif.advertisement.localName);
|
||||
});
|
||||
if (list.length > 0) {
|
||||
return resolve(list);
|
||||
} else {
|
||||
return reject(`No peripherals discovered with prefix equal to ${k.OBCIGanglionPrefix}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Get a peripheral with a local name
|
||||
* @param `pArray` {Array} - Array of peripherals
|
||||
* @param `localName` {String} - The local name of the BLE device.
|
||||
*/
|
||||
function getPeripheralWithLocalName (pArray, localName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof (pArray) !== 'object') return reject(`pArray must be of type Object`);
|
||||
_.each(pArray, perif => {
|
||||
if (perif.advertisement.hasOwnProperty('localName')) {
|
||||
if (perif.advertisement.localName === localName) {
|
||||
return resolve(perif);
|
||||
}
|
||||
}
|
||||
});
|
||||
return reject(`No peripheral found with localName: ${localName}`);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
module.exports = {
|
||||
debugBytes
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Output passed bytes on the console as a hexdump, if enabled
|
||||
* @param prefix - label to show to the left of bytes
|
||||
* @param data - bytes to output, a buffer or string
|
||||
* @private
|
||||
*/
|
||||
function debugBytes (prefix, data) {
|
||||
if (typeof data === 'string') data = new Buffer(data);
|
||||
|
||||
console.log('Debug bytes:');
|
||||
|
||||
for (var j = 0; j < data.length;) {
|
||||
var hexPart = '';
|
||||
var ascPart = '';
|
||||
for (var end = Math.min(data.length, j + 16); j < end; ++j) {
|
||||
var byt = data[j];
|
||||
|
||||
var hex = ('0' + byt.toString(16)).slice(-2);
|
||||
hexPart += (((j & 0xf) === 0x8) ? ' ' : ' '); // puts an extra space 8 bytes in
|
||||
hexPart += hex;
|
||||
|
||||
var asc = (byt >= 0x20 && byt < 0x7f) ? String.fromCharCode(byt) : '.';
|
||||
ascPart += asc;
|
||||
}
|
||||
|
||||
// pad to fixed width for alignment
|
||||
hexPart = (hexPart + ' ').substring(0, 3 * 17);
|
||||
|
||||
console.log(prefix + ' ' + hexPart + '|' + ascPart + '|');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "openbci-ganglion",
|
||||
"version": "0.1.0",
|
||||
"description": "The official Node.js SDK for the OpenBCI Ganglion Biosensor Board.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "semistandard | snazzy && mocha test",
|
||||
"test-cov": "istanbul cover ./node_modules/mocha/bin/_mocha -- -R spec && codecov"
|
||||
},
|
||||
"keywords": [
|
||||
"openbci",
|
||||
"openbci-node",
|
||||
"ganglion"
|
||||
],
|
||||
"author": "AJ Keller <aj@pushtheworld.us> (www.openbci.com)",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal": "^1.0.0",
|
||||
"clone": "^2.0.0",
|
||||
"gaussian": "^1.0.0",
|
||||
"lodash": "^4.16.6",
|
||||
"mathjs": "^3.3.0",
|
||||
"noble": "^1.7.0",
|
||||
"performance-now": "^0.2.0",
|
||||
"sntp": "^2.0.0",
|
||||
"streamsearch": "^0.1.2",
|
||||
"underscore": "^1.8.3"
|
||||
},
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bluebird": "3.4.6",
|
||||
"chai": "^3.4.1",
|
||||
"chai-as-promised": "^5.2.0",
|
||||
"codecov": "^1.0.1",
|
||||
"istanbul": "^0.4.4",
|
||||
"mocha": "^3.0.2",
|
||||
"sandboxed-module": "^2.0.3",
|
||||
"semistandard": "^9.0.0",
|
||||
"sinon": "^1.17.2",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"snazzy": "^5.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/openbci/openbci_nodejs.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/openbci/openbci_nodejs/issues"
|
||||
},
|
||||
"homepage": "https://github.com/openbci/openbci_nodejs#readme",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
},
|
||||
"semistandard": {
|
||||
"globals": [
|
||||
"describe",
|
||||
"xdescribe",
|
||||
"context",
|
||||
"before",
|
||||
"beforeEach",
|
||||
"after",
|
||||
"afterEach",
|
||||
"it",
|
||||
"expect",
|
||||
"should"
|
||||
]
|
||||
}
|
||||
}
|
||||
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
@@ -0,0 +1,142 @@
|
||||
var timingEventsAsPromises = require('./timingEventsAsPromises');
|
||||
exports.BluebirdPromise = require('bluebird');
|
||||
exports.PromiseIgnored = global.Promise;
|
||||
|
||||
// Enable bluebird for all promise usage during tests only
|
||||
// Fails tests for issues bluebird finds
|
||||
// Exports a function to list all promises (getPendingPromises)
|
||||
// Exports a function to verify no promises pending within a timeout (noPendingPromises)
|
||||
|
||||
exports.BluebirdPromise.config({
|
||||
// TODO: wForgottenReturn is disabled because timingEventsAsPromises triggers it; find a workaround
|
||||
warnings: { wForgottenReturn: false },
|
||||
longStackTraces: true,
|
||||
monitoring: true,
|
||||
cancellation: true
|
||||
});
|
||||
|
||||
// nextTick conveniently not instrumented by timingEventsAsPromises
|
||||
exports.BluebirdPromise.setScheduler(process.nextTick);
|
||||
|
||||
// unhandled rejections become test failures
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
if (!(reason instanceof Error)) {
|
||||
reason = new Error('unhandled promise rejection: ' + reason);
|
||||
} else {
|
||||
reason.message = 'unhandled promise rejection: ' + reason.message;
|
||||
}
|
||||
process.nextTick(() => { throw reason; });
|
||||
});
|
||||
|
||||
// // warnings become test failures
|
||||
// process.on('warning', (warning) => {
|
||||
// var error = new Error(warning);
|
||||
// process.nextTick(() => { throw error; });
|
||||
// });
|
||||
|
||||
// provide access to all currently pending promises
|
||||
var pendingPromises = {};
|
||||
var promiseId = 0;
|
||||
var nested = 0;
|
||||
|
||||
function promiseCreationHandler (promise) {
|
||||
// promise created already resolved; ignore
|
||||
if (!promise.isPending()) return;
|
||||
|
||||
// need to create another promise to get access to the extended stack trace
|
||||
// nested detects if we are inside our own dummy promise
|
||||
++nested;
|
||||
if (nested === 1) {
|
||||
// not the dummy promise
|
||||
promise.___id = ++promiseId;
|
||||
// store promise details
|
||||
var error = new Error('Promise ' + promise.___id + ' is still pending');
|
||||
var entry = {
|
||||
promise: promise,
|
||||
id: promise.___id,
|
||||
error: error
|
||||
};
|
||||
pendingPromises[promise.___id] = entry;
|
||||
// extract stack trace by rejecting an error; bluebird fills in expanded stack
|
||||
exports.BluebirdPromise.reject(error).catch(error => {
|
||||
entry.error = error;
|
||||
entry.stack = error.stack;
|
||||
});
|
||||
} else {
|
||||
promise.___nested = nested;
|
||||
}
|
||||
--nested;
|
||||
}
|
||||
process.on('promiseCreated', promiseCreationHandler);
|
||||
|
||||
function promiseDoneHandler (promise) {
|
||||
if (promise.___nested) return;
|
||||
delete pendingPromises[promise.___id];
|
||||
}
|
||||
process.on('promiseFulfilled', promiseDoneHandler);
|
||||
process.on('promiseRejected', promiseDoneHandler);
|
||||
process.on('promiseResolved', promiseDoneHandler);
|
||||
process.on('promiseCancelled', promiseDoneHandler);
|
||||
|
||||
exports.getPendingPromises = function () {
|
||||
var ret = [];
|
||||
for (var promise in pendingPromises) {
|
||||
ret.push(pendingPromises[promise]);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
exports.noPendingPromises = function (milliseconds) {
|
||||
if (!milliseconds) milliseconds = 0;
|
||||
|
||||
return new exports.PromiseIgnored((resolve, reject) => {
|
||||
function waited100 () {
|
||||
var promises = exports.getPendingPromises();
|
||||
|
||||
if (promises.length === 0) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
if (milliseconds > 0) {
|
||||
milliseconds -= 100;
|
||||
return timingEventsAsPromises.setTimeoutIgnored(waited100, 100);
|
||||
}
|
||||
|
||||
// timed out, but promises remaining: cancel all
|
||||
|
||||
console.log(promises.length + ' promises still pending');
|
||||
|
||||
promises.forEach(promise => {
|
||||
promise.promise.cancel();
|
||||
});
|
||||
|
||||
// report one
|
||||
reject(promises[0].error);
|
||||
}
|
||||
|
||||
timingEventsAsPromises.setTimeoutIgnored(waited100, 0);
|
||||
});
|
||||
};
|
||||
|
||||
// now instrument the Promise object itself to always use a simplified version of bluebird
|
||||
// bluebird is composed inside a bare-bones Promise object providing only the official calls
|
||||
|
||||
global.Promise = function (handler) {
|
||||
this._promise = new exports.BluebirdPromise(handler);
|
||||
};
|
||||
|
||||
// compose class methods
|
||||
['all', 'race', 'reject', 'resolve'].forEach(classMethod => {
|
||||
global.Promise[classMethod] = function () {
|
||||
return exports.BluebirdPromise[classMethod].apply(exports.BluebirdPromise, [].slice.call(arguments));
|
||||
};
|
||||
Object.defineProperty(global.Promise[classMethod], 'name', { value: 'Promise.' + classMethod });
|
||||
});
|
||||
|
||||
// compose object methods
|
||||
['then', 'catch'].forEach(objectMethod => {
|
||||
global.Promise.prototype[objectMethod] = function () {
|
||||
return this._promise[objectMethod].apply(this._promise, [].slice.call(arguments));
|
||||
};
|
||||
Object.defineProperty(global.Promise.prototype[objectMethod], 'name', { value: 'Promise.' + objectMethod });
|
||||
});
|
||||
@@ -0,0 +1,352 @@
|
||||
'use strict';
|
||||
// const bluebirdChecks = require('./bluebirdChecks');
|
||||
const sinon = require('sinon');
|
||||
const chai = require('chai');
|
||||
const expect = chai.expect;
|
||||
const should = chai.should(); // eslint-disable-line no-unused-vars
|
||||
const Ganglion = require('../openBCIGanglion');
|
||||
const k = require('../openBCIConstants');
|
||||
const chaiAsPromised = require('chai-as-promised');
|
||||
const sinonChai = require('sinon-chai');
|
||||
const bufferEqual = require('buffer-equal');
|
||||
const utils = require('../openBCIGanglionUtils');
|
||||
const clone = require('clone');
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
chai.use(sinonChai);
|
||||
|
||||
describe('#ganglion', function () {
|
||||
const mockProperties = {
|
||||
nobleAutoStart: false,
|
||||
nobleScanOnPowerOn: false,
|
||||
simulate: false,
|
||||
simulatorBoardFailure: false,
|
||||
simulatorHasAccelerometer: true,
|
||||
simulatorInternalClockDrift: 0,
|
||||
simulatorInjectAlpha: true,
|
||||
simulatorInjectLineNoise: k.OBCISimulatorLineNoiseHz60,
|
||||
simulatorSampleRate: 200,
|
||||
verbose: false,
|
||||
debug: false,
|
||||
sendCounts: false
|
||||
};
|
||||
const expectedProperties = clone(mockProperties);
|
||||
const ganglion = new Ganglion(mockProperties);
|
||||
it('should have properties', function () {
|
||||
expect(ganglion.options).to.deep.equal(expectedProperties);
|
||||
});
|
||||
it('should return 4 channels', function () {
|
||||
expect(ganglion.numberOfChannels()).to.equal(4);
|
||||
});
|
||||
it('should extract the proper values for each channel', function () {
|
||||
let buffer = new Buffer(
|
||||
[
|
||||
0b00000000, // 0
|
||||
0b00000000, // 1
|
||||
0b00000000, // 2
|
||||
0b00000000, // 3
|
||||
0b00001000, // 4
|
||||
0b00000000, // 5
|
||||
0b00000101, // 6
|
||||
0b00000000, // 7
|
||||
0b00000000, // 8
|
||||
0b01001000, // 9
|
||||
0b00000000, // 10
|
||||
0b00001001, // 11
|
||||
0b11110000, // 12
|
||||
0b00000001, // 13
|
||||
0b10110000, // 14
|
||||
0b00000000, // 15
|
||||
0b00110000, // 16
|
||||
0b00000000, // 17
|
||||
0b00001000 // 18
|
||||
]);
|
||||
let expectedValue = [[0, 2, 10, 4], [262148, 507910, 393222, 8]];
|
||||
let actualValue = ganglion.decompressDeltas(buffer);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
(actualValue[0][i]).should.equal(expectedValue[0][i]);
|
||||
(actualValue[1][i]).should.equal(expectedValue[1][i]);
|
||||
}
|
||||
});
|
||||
it('should extract the proper values for each channel (neg test)', function () {
|
||||
let buffer = new Buffer(
|
||||
[
|
||||
0b11111111, // 0
|
||||
0b11111111, // 1
|
||||
0b10111111, // 2
|
||||
0b11111111, // 3
|
||||
0b11101111, // 4
|
||||
0b11111111, // 5
|
||||
0b11111100, // 6
|
||||
0b11111111, // 7
|
||||
0b11111111, // 8
|
||||
0b01011000, // 9
|
||||
0b00000000, // 10
|
||||
0b00001011, // 11
|
||||
0b00111110, // 12
|
||||
0b00111000, // 13
|
||||
0b11100000, // 14
|
||||
0b00000000, // 15
|
||||
0b00111111, // 16
|
||||
0b11110000, // 17
|
||||
0b00000001 // 18
|
||||
]);
|
||||
let expectedValue = [[-3, -5, -7, -11], [-262139, -198429, -262137, -4095]];
|
||||
let actualValue = ganglion.decompressDeltas(buffer);
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
(actualValue[0][i]).should.equal(expectedValue[0][i]);
|
||||
(actualValue[1][i]).should.equal(expectedValue[1][i]);
|
||||
}
|
||||
});
|
||||
it('should destroy the multi packet buffer', function () {
|
||||
ganglion.destroyMultiPacketBuffer();
|
||||
expect(ganglion.getMutliPacketBuffer()).to.equal(null);
|
||||
});
|
||||
it('should stack and emit one buffer from several multi packet buffer', function () {
|
||||
const bufMultPacket = new Buffer([k.OBCIGanglionByteIdMultiPacket]);
|
||||
const bufMultPacketStop = new Buffer([k.OBCIGanglionByteIdMultiPacketStop]);
|
||||
const buf1 = new Buffer('taco');
|
||||
const newBuffer1 = Buffer.concat([bufMultPacket, buf1]);
|
||||
ganglion._processMultiBytePacket(newBuffer1);
|
||||
expect(bufferEqual(ganglion.getMutliPacketBuffer(), buf1)).to.equal(true);
|
||||
|
||||
const buf2 = new Buffer('vegas');
|
||||
const newBuffer2 = Buffer.concat([bufMultPacket, buf2]);
|
||||
ganglion._processMultiBytePacket(newBuffer2);
|
||||
expect(bufferEqual(ganglion.getMutliPacketBuffer(), Buffer.concat([buf1, buf2])));
|
||||
|
||||
const bufStop = new Buffer('hola');
|
||||
const newBufferStop = Buffer.concat([bufMultPacketStop, bufStop]);
|
||||
let messageEventCalled = false;
|
||||
ganglion.once('message', (data) => {
|
||||
expect(bufferEqual(data, Buffer.concat([buf1, buf2, bufStop]))).to.equal(true);
|
||||
messageEventCalled = true;
|
||||
});
|
||||
ganglion._processMultiBytePacketStop(newBufferStop);
|
||||
expect(ganglion.getMutliPacketBuffer()).to.equal(null);
|
||||
expect(messageEventCalled).to.equal(true);
|
||||
|
||||
ganglion.once('message', (data) => {
|
||||
expect(bufferEqual(data, bufStop)).to.equal(true);
|
||||
});
|
||||
ganglion._processMultiBytePacketStop(newBufferStop);
|
||||
expect(ganglion.getMutliPacketBuffer()).to.equal(null);
|
||||
});
|
||||
it('should be able to just get one packet buffer message', function () {
|
||||
const bufStop = new Buffer('hola');
|
||||
const bufMultPacketStop = new Buffer([k.OBCIGanglionByteIdMultiPacketStop]);
|
||||
const newBufferStop = Buffer.concat([bufMultPacketStop, bufStop]);
|
||||
let messageEventCalled = false;
|
||||
ganglion.once('message', (data) => {
|
||||
expect(bufferEqual(data, bufStop)).to.equal(true);
|
||||
messageEventCalled = true;
|
||||
});
|
||||
ganglion._processMultiBytePacketStop(newBufferStop);
|
||||
expect(ganglion.getMutliPacketBuffer()).to.equal(null);
|
||||
expect(messageEventCalled).to.equal(true);
|
||||
});
|
||||
describe('_processBytes', function () {
|
||||
let funcSpyAccel;
|
||||
let funcSpyCompressedData;
|
||||
let funcSpyImpedanceData;
|
||||
let funcSpyMultiBytePacket;
|
||||
let funcSpyMultiBytePacketStop;
|
||||
let funcSpyOtherData;
|
||||
let funcSpyUncompressedData;
|
||||
|
||||
before(function () {
|
||||
// Put watchers on all functions
|
||||
funcSpyAccel = sinon.spy(ganglion, '_processAccel');
|
||||
funcSpyCompressedData = sinon.spy(ganglion, '_processCompressedData');
|
||||
funcSpyImpedanceData = sinon.spy(ganglion, '_processImpedanceData');
|
||||
funcSpyMultiBytePacket = sinon.spy(ganglion, '_processMultiBytePacket');
|
||||
funcSpyMultiBytePacketStop = sinon.spy(ganglion, '_processMultiBytePacketStop');
|
||||
funcSpyOtherData = sinon.spy(ganglion, '_processOtherData');
|
||||
funcSpyUncompressedData = sinon.spy(ganglion, '_processUncompressedData');
|
||||
});
|
||||
beforeEach(function () {
|
||||
funcSpyAccel.reset();
|
||||
funcSpyCompressedData.reset();
|
||||
funcSpyImpedanceData.reset();
|
||||
funcSpyMultiBytePacket.reset();
|
||||
funcSpyMultiBytePacketStop.reset();
|
||||
funcSpyOtherData.reset();
|
||||
funcSpyUncompressedData.reset();
|
||||
});
|
||||
it('should route accel packet', function () {
|
||||
ganglion._processBytes(utils.sampleAccel());
|
||||
funcSpyAccel.should.have.been.calledOnce;
|
||||
});
|
||||
it('should route compressed data packet', function () {
|
||||
ganglion._processBytes(utils.sampleCompressedData(3));
|
||||
funcSpyCompressedData.should.have.been.calledOnce;
|
||||
});
|
||||
it('should route impedance channel 1 packet', function () {
|
||||
ganglion._processBytes(utils.sampleImpedanceChannel1());
|
||||
funcSpyImpedanceData.should.have.been.calledOnce;
|
||||
});
|
||||
it('should route impedance channel 2 packet', function () {
|
||||
ganglion._processBytes(utils.sampleImpedanceChannel2());
|
||||
funcSpyImpedanceData.should.have.been.calledOnce;
|
||||
});
|
||||
it('should route impedance channel 3 packet', function () {
|
||||
ganglion._processBytes(utils.sampleImpedanceChannel3());
|
||||
funcSpyImpedanceData.should.have.been.calledOnce;
|
||||
});
|
||||
it('should route impedance channel 4 packet', function () {
|
||||
ganglion._processBytes(utils.sampleImpedanceChannel4());
|
||||
funcSpyImpedanceData.should.have.been.calledOnce;
|
||||
});
|
||||
it('should route impedance channel reference packet', function () {
|
||||
ganglion._processBytes(utils.sampleImpedanceChannelReference());
|
||||
funcSpyImpedanceData.should.have.been.calledOnce;
|
||||
});
|
||||
it('should route multi packet data', function () {
|
||||
ganglion._processBytes(utils.sampleMultiBytePacket(new Buffer('taco')));
|
||||
funcSpyMultiBytePacket.should.have.been.calledOnce;
|
||||
});
|
||||
it('should route multi packet stop data', function () {
|
||||
ganglion._processBytes(utils.sampleMultiBytePacketStop(new Buffer('taco')));
|
||||
funcSpyMultiBytePacketStop.should.have.been.calledOnce;
|
||||
});
|
||||
it('should route other data packet', function () {
|
||||
ganglion._processBytes(utils.sampleOtherData(new Buffer('blah')));
|
||||
funcSpyOtherData.should.have.been.calledOnce;
|
||||
});
|
||||
it('should route uncompressed data packet', function () {
|
||||
ganglion._processBytes(utils.sampleUncompressedData());
|
||||
funcSpyUncompressedData.should.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
it('should emit impedance value', function() {
|
||||
let expectedImpedanceValue = 1099;
|
||||
const payloadBuf = new Buffer(`${expectedImpedanceValue}${k.OBCIGanglionImpedanceStop}`);
|
||||
let totalEvents = 0;
|
||||
let runningEventCount = 0;
|
||||
|
||||
// Channel 1
|
||||
totalEvents++;
|
||||
let expectedChannelNumber = 1;
|
||||
let impPre = new Buffer([k.OBCIGanglionByteIdImpedanceChannel1]);
|
||||
let expectedReturnValue = {
|
||||
channelNumber: expectedChannelNumber,
|
||||
impedanceValue: expectedImpedanceValue
|
||||
};
|
||||
let dataBuf = Buffer.concat([impPre, payloadBuf]);
|
||||
ganglion.once('impedance', (actualImpedanceValue) => {
|
||||
expect(actualImpedanceValue).to.deep.equal(expectedReturnValue);
|
||||
runningEventCount++;
|
||||
});
|
||||
ganglion._processImpedanceData(dataBuf);
|
||||
|
||||
// Channel 2
|
||||
totalEvents++;
|
||||
expectedChannelNumber = 2;
|
||||
impPre[0] = k.OBCIGanglionByteIdImpedanceChannel2;
|
||||
expectedReturnValue = {
|
||||
channelNumber: expectedChannelNumber,
|
||||
impedanceValue: expectedImpedanceValue
|
||||
};
|
||||
dataBuf = Buffer.concat([impPre, payloadBuf]);
|
||||
ganglion.once('impedance', (actualImpedanceValue) => {
|
||||
expect(actualImpedanceValue).to.deep.equal(expectedReturnValue);
|
||||
runningEventCount++;
|
||||
});
|
||||
ganglion._processImpedanceData(dataBuf);
|
||||
|
||||
// Channel 3
|
||||
totalEvents++;
|
||||
expectedChannelNumber = 3;
|
||||
impPre[0] = k.OBCIGanglionByteIdImpedanceChannel3;
|
||||
expectedReturnValue = {
|
||||
channelNumber: expectedChannelNumber,
|
||||
impedanceValue: expectedImpedanceValue
|
||||
};
|
||||
dataBuf = Buffer.concat([impPre, payloadBuf]);
|
||||
ganglion.once('impedance', (actualImpedanceValue) => {
|
||||
expect(actualImpedanceValue).to.deep.equal(expectedReturnValue);
|
||||
runningEventCount++;
|
||||
});
|
||||
ganglion._processImpedanceData(dataBuf);
|
||||
|
||||
// Channel 4
|
||||
totalEvents++;
|
||||
expectedChannelNumber = 4;
|
||||
impPre[0] = k.OBCIGanglionByteIdImpedanceChannel4;
|
||||
expectedReturnValue = {
|
||||
channelNumber: expectedChannelNumber,
|
||||
impedanceValue: expectedImpedanceValue
|
||||
};
|
||||
dataBuf = Buffer.concat([impPre, payloadBuf]);
|
||||
ganglion.once('impedance', (actualImpedanceValue) => {
|
||||
expect(actualImpedanceValue).to.deep.equal(expectedReturnValue);
|
||||
runningEventCount++;
|
||||
});
|
||||
ganglion._processImpedanceData(dataBuf);
|
||||
|
||||
// Channel Reference
|
||||
totalEvents++;
|
||||
expectedChannelNumber = 0;
|
||||
impPre[0] = k.OBCIGanglionByteIdImpedanceChannelReference;
|
||||
expectedReturnValue = {
|
||||
channelNumber: expectedChannelNumber,
|
||||
impedanceValue: expectedImpedanceValue
|
||||
};
|
||||
dataBuf = Buffer.concat([impPre, payloadBuf]);
|
||||
ganglion.once('impedance', (actualImpedanceValue) => {
|
||||
expect(actualImpedanceValue).to.deep.equal(expectedReturnValue);
|
||||
runningEventCount++;
|
||||
});
|
||||
ganglion._processImpedanceData(dataBuf);
|
||||
|
||||
// Makes sure the correct amount of events were called.
|
||||
expect(runningEventCount).to.equal(totalEvents)
|
||||
});
|
||||
});
|
||||
|
||||
xdescribe('#noble', function () {
|
||||
xdescribe('#_nobleInit', function () {
|
||||
it('should emit powered on', function (done) {
|
||||
const ganglion = new Ganglion({
|
||||
verbose: true,
|
||||
nobleAutoStart: false,
|
||||
nobleScanOnPowerOn: false
|
||||
});
|
||||
ganglion.once(k.OBCIEmitterBlePoweredUp, () => {
|
||||
// Able to get powered up thing
|
||||
done();
|
||||
});
|
||||
ganglion._nobleInit();
|
||||
});
|
||||
});
|
||||
describe('#_nobleScan', function () {
|
||||
const searchTime = k.OBCIGanglionBleSearchTime * 2;
|
||||
|
||||
this.timeout(searchTime + 1000);
|
||||
it('gets peripherals', function (done) {
|
||||
const ganglion = new Ganglion({
|
||||
verbose: true,
|
||||
nobleScanOnPowerOn: false
|
||||
});
|
||||
|
||||
const doScan = () => {
|
||||
ganglion._nobleScan(searchTime)
|
||||
.then((list) => {
|
||||
console.log('listPeripherals', list);
|
||||
if (list) done();
|
||||
})
|
||||
.catch((err) => {
|
||||
done(err);
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
if (ganglion._nobleReady()) {
|
||||
doScan();
|
||||
} else {
|
||||
ganglion.on('blePoweredOn', doScan());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
'use strict';
|
||||
// const sinon = require('sinon');
|
||||
const chai = require('chai');
|
||||
const should = chai.should(); // eslint-disable-line no-unused-vars
|
||||
const expect = chai.expect;
|
||||
const assert = chai.assert;
|
||||
const utils = require('../openBCIGanglionUtils');
|
||||
const k = require('../openBCIConstants');
|
||||
|
||||
var chaiAsPromised = require('chai-as-promised');
|
||||
var sinonChai = require('sinon-chai');
|
||||
chai.use(chaiAsPromised);
|
||||
chai.use(sinonChai);
|
||||
|
||||
var getListOfPeripheralsOfSize = (perifsToMake) => {
|
||||
perifsToMake = perifsToMake || 3;
|
||||
|
||||
let output = [];
|
||||
|
||||
for (var i = 0; i < perifsToMake; i++) {
|
||||
output.push({
|
||||
advertisement: {
|
||||
localName: makeLocalName(i),
|
||||
txPowerLevel: undefined,
|
||||
manufacturerData: undefined,
|
||||
serviceData: [],
|
||||
serviceUuids: []
|
||||
}
|
||||
});
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
var makeLocalName = (num) => {
|
||||
let localName = `${k.OBCIGanglionPrefix}-00`;
|
||||
if (num < 10) {
|
||||
localName = `${localName}0${num}`;
|
||||
} else {
|
||||
localName = `${localName}${num}`;
|
||||
}
|
||||
return localName;
|
||||
};
|
||||
|
||||
describe('openBCIGanglionUtils', function () {
|
||||
describe('#convert19bitAsInt32', function () {
|
||||
it('converts a small positive number', function () {
|
||||
const buf1 = new Buffer([0x00, 0x06, 0x90]); // 0x000690 === 1680
|
||||
const num = utils.convert19bitAsInt32(buf1);
|
||||
assert.equal(num, 1680);
|
||||
});
|
||||
it('converts a small positive number', function () {
|
||||
const buf1 = new Buffer([0x00, 0x06, 0x90]); // 0x000690 === 1680
|
||||
const num = utils.convert19bitAsInt32(buf1);
|
||||
assert.equal(num, 1680);
|
||||
});
|
||||
it('converts a large positive number', function () {
|
||||
const buf1 = new Buffer([0x02, 0xC0, 0x00]); // 0x02C001 === 180225
|
||||
const num = utils.convert19bitAsInt32(buf1);
|
||||
assert.equal(num, 180224);
|
||||
});
|
||||
it('converts a small negative number', function () {
|
||||
const buf1 = new Buffer([0xFF, 0xFF, 0xFF]); // 0xFFFFFF === -1
|
||||
const num = utils.convert19bitAsInt32(buf1);
|
||||
num.should.be.approximately(-1, 1);
|
||||
});
|
||||
it('converts a large negative number', function () {
|
||||
const buf1 = new Buffer([0x04, 0xA1, 0x01]); // 0x04A101 === -220927
|
||||
const num = utils.convert19bitAsInt32(buf1);
|
||||
num.should.be.approximately(-220927, 1);
|
||||
});
|
||||
});
|
||||
describe('#getPeripheralLocalNames', function () {
|
||||
it('should resolve a list of localNames from an array of peripherals', function (done) {
|
||||
let numPerifs = 3;
|
||||
let perifs = getListOfPeripheralsOfSize(numPerifs);
|
||||
utils.getPeripheralLocalNames(perifs).then(list => {
|
||||
expect(list.length).to.equal(numPerifs);
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
expect(list[i]).to.equal(makeLocalName(i));
|
||||
}
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
it('should reject if array is empty', function (done) {
|
||||
utils.getPeripheralLocalNames([]).should.be.rejected.and.notify(done);
|
||||
});
|
||||
});
|
||||
describe('#getPeripheralWithLocalName', function () {
|
||||
it('should resovle a peripheral with local name', function (done) {
|
||||
let numOfPerifs = 4;
|
||||
let perifs = getListOfPeripheralsOfSize(numOfPerifs);
|
||||
// console.log('perifs', perifs)
|
||||
let goodName = makeLocalName(numOfPerifs - 1); // Will be in the list
|
||||
// console.log(`goodName: ${goodName}`)
|
||||
utils.getPeripheralWithLocalName(perifs, goodName).should.be.fulfilled.and.notify(done);
|
||||
});
|
||||
it('should reject if local name is not in perif list', function (done) {
|
||||
let numOfPerifs = 4;
|
||||
let perifs = getListOfPeripheralsOfSize(numOfPerifs);
|
||||
let badName = makeLocalName(numOfPerifs + 2); // Garuenteed to not be in the list
|
||||
utils.getPeripheralWithLocalName(perifs, badName).should.be.rejected.and.notify(done);
|
||||
});
|
||||
it('should reject if pArray is not array local name is not in perif list', function (done) {
|
||||
let badName = makeLocalName(1); // Garuenteed to not be in the list
|
||||
utils.getPeripheralWithLocalName(badName).should.be.rejected.and.notify(done);
|
||||
});
|
||||
});
|
||||
describe('#isPeripheralGanglion', function () {
|
||||
it('should return true when proper localName', function () {
|
||||
let list = getListOfPeripheralsOfSize(1);
|
||||
let perif = list[0];
|
||||
expect(utils.isPeripheralGanglion(perif)).to.equal(true);
|
||||
});
|
||||
it('should return false when incorrect localName', function () {
|
||||
let list = getListOfPeripheralsOfSize(1);
|
||||
let perif = list[0];
|
||||
perif.advertisement.localName = 'burrito';
|
||||
expect(utils.isPeripheralGanglion(perif)).to.equal(false);
|
||||
});
|
||||
it('should return false when bad object', function () {
|
||||
expect(utils.isPeripheralGanglion({})).to.equal(false);
|
||||
});
|
||||
it('should return false if nothing input', function () {
|
||||
expect(utils.isPeripheralGanglion()).to.equal(false);
|
||||
});
|
||||
it('should return false if undfined unput input', function () {
|
||||
let list = getListOfPeripheralsOfSize(1);
|
||||
let perif = list[0];
|
||||
perif.advertisement.localName = undefined;
|
||||
expect(utils.isPeripheralGanglion(perif)).to.equal(false);
|
||||
});
|
||||
it('should return false when missing advertisement object', function () {
|
||||
let list = getListOfPeripheralsOfSize(1);
|
||||
let perif = list[0];
|
||||
perif.advertisement = null;
|
||||
expect(utils.isPeripheralGanglion(perif)).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
// Converts timing events to use promises, to gain bluebird's checks
|
||||
|
||||
function instrumentTimingEvents (module, type) {
|
||||
// replaces module[type] with a function that does the same thing but uses a promise
|
||||
// assumes the first argument will be to a callback function
|
||||
|
||||
// if this a setSomething function with a clearSomething partner, the partner will also be instrumented
|
||||
var setName = type;
|
||||
var clearName = null;
|
||||
if (type.substring(0, 3) === 'set') {
|
||||
clearName = 'clear' + type.substring(3);
|
||||
}
|
||||
|
||||
if (!module[setName]) return;
|
||||
|
||||
// store original functions
|
||||
var originalSet = module[setName];
|
||||
var originalClear = module[clearName];
|
||||
exports[setName + 'Ignored'] = originalSet;
|
||||
|
||||
// dictionary to store promise details for each call
|
||||
var events = {};
|
||||
|
||||
// setAsPromise() is the brunt of the function. It replaces the previous global function,
|
||||
// and sets up a promise to resolve when the callback is called
|
||||
var eventCount = 0;
|
||||
var setAsPromise = function (callback) {
|
||||
var args = [].slice.call(arguments);
|
||||
|
||||
var eventNum = ++eventCount;
|
||||
var eventHandle;
|
||||
|
||||
if (setAsPromise._ignoreCount > 0) {
|
||||
--setAsPromise._ignoreCount;
|
||||
|
||||
return originalSet.apply(this, args);
|
||||
}
|
||||
|
||||
// actual callback is replaced by promise resolve
|
||||
args[0] = function () {
|
||||
if (!events[eventNum]) throw new Error(setName + ' ' + eventNum + ' disappeared');
|
||||
events[eventNum].resolve([].slice.call(arguments));
|
||||
};
|
||||
|
||||
eventHandle = originalSet.apply(this, args) || eventNum;
|
||||
|
||||
// this portion is a function so that setInterval may be handled via recursion
|
||||
function dispatch () {
|
||||
var handlerDetails = { handle: eventHandle };
|
||||
|
||||
var promise = new Promise((resolve, reject) => {
|
||||
handlerDetails.resolve = resolve;
|
||||
handlerDetails.reject = reject;
|
||||
});
|
||||
|
||||
handlerDetails.promise = promise;
|
||||
events[eventNum] = handlerDetails;
|
||||
|
||||
promise.then(function (argumentArray) {
|
||||
if (type !== 'setInterval') {
|
||||
delete events[eventNum];
|
||||
} else {
|
||||
dispatch();
|
||||
}
|
||||
|
||||
// call original handler
|
||||
callback.apply(this, argumentArray);
|
||||
}, () => {
|
||||
originalClear(eventHandle);
|
||||
delete events[eventNum];
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
dispatch();
|
||||
|
||||
return eventNum;
|
||||
};
|
||||
|
||||
// actually replace functions with instrumented ones
|
||||
setAsPromise._ignoreCount = 0;
|
||||
module[setName] = setAsPromise;
|
||||
Object.defineProperty(setAsPromise, 'name', { value: setName + 'AsPromise' });
|
||||
|
||||
if (clearName) {
|
||||
module[clearName] = (eventNum) => {
|
||||
if (!events[eventNum]) {
|
||||
originalClear(eventNum);
|
||||
} else {
|
||||
events[eventNum].reject(new Error('cleared'));
|
||||
}
|
||||
};
|
||||
Object.defineProperty(module[clearName], 'name', { value: clearName + 'AsPromise' });
|
||||
}
|
||||
}
|
||||
|
||||
instrumentTimingEvents(global, 'setTimeout');
|
||||
instrumentTimingEvents(global, 'setInterval');
|
||||
instrumentTimingEvents(global, 'setImmediate');
|
||||
// // Possible TODO: nextTick needs some exceptions included to prevent infinite recursion
|
||||
// instrumentTimingEvents(process, 'nextTick');
|
||||
|
||||
// the next call to the passed function should not be promisified
|
||||
// may be queued multiple times
|
||||
exports.ignoreOnce = (ignored) => {
|
||||
ignored._ignoreCount ++;
|
||||
};
|
||||
Referência em uma Nova Issue
Bloquear um usuário