node package manager

openbci-ganglion

Join the chat at https://gitter.im/OpenBCI/OpenBCI_NodeJS Build Status codecov Dependency Status npm js-semistandard-style

OpenBCI Node.js Ganglion SDK

A Node.js module for OpenBCI ~ written with love by Push The World!

We are proud to support all functionality of the Ganglion (4 channel). Push The World is actively developing and maintaining this module.

The purpose of this module is to get connected and start streaming as fast as possible.

Table of Contents:


  1. TL;DR
  2. Prerequisites
  3. Installation
  4. Ganglion
  5. General Overview
  6. SDK Reference Guide
* [Constructor](#constructor)
* [Methods](#method)
* [Events](#event)
* [Constants](#constants)
  1. Interfacing With Other Tools
  2. Developing
  3. Testing
  4. Contribute
  5. License
  6. Roadmap

TL;DR:

Get connected and start streaming right now

const Ganglion = require('openbci-ganglion').Ganglion;
const ganglion = new Ganglion();
ganglion.once('ganglionFound', (peripheral) => {
  // Stop searching for BLE devices once a ganglion is found. 
  ganglion.searchStop();
  ganglion.on('sample', (sample) => {
    /** Work with sample */
    console.log(sample.sampleNumber);
    for (let i = 0; i < ganglion.numberOfChannels(); i++) {
      console.log("Channel " + (+ 1) + "" + sample.channelData[i].toFixed(8) + " Volts.");
    }
  });
  ganglion.once('ready', () => {
    ganglion.streamStart();
  });
  ganglion.connect(peripheral);
});
// Start scanning for BLE devices 
ganglion.searchStart();

Prerequisites:

Please ensure Python 2.7 is installed for all OS.

macOS

Linux

  • Kernel version 3.6 or above
  • libbluetooth-dev

Windows 8+

See @don's set up guide on Bluetooth LE with Node.js and Noble on Windows.

Installation:

Install from npm:

npm install openbci-ganglion

About:

The Ganglion driver used by OpenBCI's Processing GUI and Electron Hub.

Check out the automatic tests written for it!

General Overview:

Initialization

Initializing the board:

const Ganglion = require('openbci-ganglion').Ganglion;
const ganglion = new Ganglion();

For initializing with options, such as verbose print outs:

const Ganglion = require('openbci-ganglion').Ganglion;
const ourBoard = new Ganglion({
  verbose: true
});

For initializing with callback, such as to catch errors on noble startup:

const Ganglion = require('openbci-ganglion').Ganglion;
const ourBoard = new Ganglion((error) => {
  if (error) {
    console.log("error", error);  
  } else {
    console.log("no error");
  }
});

For initializing with options and callback, such as verbose and to catch errors on noble startup:

const Ganglion = require('openbci-ganglion').Ganglion;
const ourBoard = new Ganglion({
  verbose: true
},(error) => {
  if (error) {
    console.log("error", error);  
  } else {
    console.log("no error");
  }
});

'ready' event

You MUST wait for the 'ready' event to be emitted before streaming/talking with the board. The ready happens asynchronously so installing the 'sample' listener and writing before the ready event might result in... nothing at all.

const Ganglion = require('openbci-ganglion').Ganglion;
const ourBoard = new Ganglion();
ourBoard.connect(portName).then(function(boardSerial) {
    ourBoard.on('ready',function() {
        /** Start streaming, reading registers, what ever your heart desires  */
    });
}).catch(function(err) {
    /** Handle connection errors */
});            

Sample properties:

  • sampleNumber (a Number between 0-255)
  • channelData (channel data indexed at 0 filled with floating point Numbers in Volts)
  • accelData (Array with X, Y, Z accelerometer values when new data available)
  • timeStamp (Number the boardTime plus the NTP calculated offset)

The power of this module is in using the sample emitter, to be provided with samples to do with as you wish.

To get a 'sample' event, you need to:

  1. Call .connect(localName | peripheral)
  2. Install the 'ready' event emitter on resolved promise
  3. In callback for 'ready' emitter, call streamStart()
  4. Install the 'sample' event emitter
const Ganglion = require('openbci-ganglion').Ganglion;
const ourBoard = new Ganglion();
ourBoard.connect(localName).then(function() {
    ourBoard.on('ready',function() {
        ourBoard.streamStart();
        ourBoard.on('sample',function(sample) {
            /** Work with sample */
        });
    });
}).catch(function(err) {
    /** Handle connection errors */
});            

Close the connection with .streamStop() and disconnect with .disconnect()

const Ganglion = require('openbci-ganglion').Ganglion;
const ourBoard = new Ganglion();
ourBoard.streamStop().then(ourBoard.disconnect());

See Reference Guide for a complete list of impedance tests.

SDK Reference Guide:


Constructor:

Ganglion (options, callback)

Create new instance of a Ganglion board.

options (optional)

Board optional configurations.

  • debug {Boolean} - Print out a raw dump of bytes sent and received (Default false)
  • nobleAutoStart {Boolean} - Automatically initialize noble. Subscribes to blue tooth state changes and such. (Default true)
  • nobleScanOnPowerOn {Boolean} - Start scanning for Ganglion BLE devices as soon as power turns on. (Default true)
  • sendCounts {Boolean} - Send integer raw counts instead of scaled floats. (Default false)
  • simulate {Boolean} - Full functionality, just mock data. Must attach Daisy module by setting simulatorDaisyModuleAttached to true in order to get 16 channels. (Default false)
  • simulatorBoardFailure {Boolean} - Simulates board communications failure. This occurs when the RFduino on the board is not polling the RFduino on the dongle. (Default false)
  • simulatorHasAccelerometer - {Boolean} - Sets simulator to send packets with accelerometer data. (Default true)
  • simulatorInjectAlpha - {Boolean} - Inject a 10Hz alpha wave in Channels 1 and 2 (Default true)
  • simulatorInjectLineNoise {String} - Injects line noise on channels. (3 Possible Options)
    • 60Hz - 60Hz line noise (Default) [America]
    • 50Hz - 50Hz line noise [Europe]
    • none - Do not inject line noise.
  • simulatorSampleRate {Number} - The sample rate to use for the simulator. Simulator will set to 125 if simulatorDaisyModuleAttached is set true. However, setting this option overrides that setting and this sample rate will be used. (Default is 250)
  • verbose {Boolean} - Print out useful debugging events (Default false)

Note, we have added support for either all lowercase OR camel case for the options, use whichever style you prefer.

callback (optional)

Callback function to catch errors. Returns only error if an error was encountered.

Methods:

.accelStart()

Used to enable the accelerometer. Will result in accelerometer packets arriving 10 times a second.

Note that the accelerometer is enabled by default.

Returns {Promise} - fulfilled once the command was sent to the board.

.accelStop()

Used to disable the accelerometer. Prevents accelerometer data packets from arriving.

Note that the accelerometer is enabled by default.

Returns {Promise} - fulfilled once the command was sent to the board.

.autoReconnect()

Used to start a scan if power is on. Useful if a connection is dropped.

.channelOff(channelNumber)

Turn off a specified channel

channelNumber

A number (1-4) specifying which channel you want to turn off.

Returns {Promise} - fulfilled once the command was sent to the board.

.channelOn(channelNumber)

Turn on a specified channel

channelNumber

A number (1-4) specifying which channel you want to turn on.

Returns {Promise} - fulfilled once the command was sent to the board.

.connect(portName)

The essential precursor method to be called initially to establish a ble connection to the OpenBCI ganglion board.

id {String | Object}

A string localName or peripheral (from noble) object.

Returns {Promise} - fulfilled by a successful connection to the board.

.destroyMultiPacketBuffer()

Destroys the multi packet buffer. The mulit packet buffer holds data from the multi packet messages.

.disconnect(stopStreaming)

Closes the connection to the board. Waits for stop streaming command to be sent if currently streaming.

stopStreaming {Boolean} (optional)

true if you want to stop streaming before disconnecting. (Default false)

Returns {Promise} - fulfilled by a successful close, rejected otherwise.

.getLocalName()

Gets the local name of the attached Ganglion device. This is only valid after .connect()

Returns {null|String} - The local name.

.getMutliPacketBuffer()

Get's the multi packet buffer.

Returns {null|Buffer} - Can be null if no multi packets received.

.impedanceStart()

Call to start testing impedance.

Returns {Promise} - that fulfills when all the commands are sent to the board.

.impedanceStop()

Call to stop testing impedance.

Returns {Promise} - that fulfills when all the commands are sent to the board.

.isConnected()

Checks if the driver is connected to a board.

Returns {Boolean} - true if connected

.isNobleReady()

Checks if bluetooth is powered on. Cannot start scanning till this is true.

Returns {Boolean} - true if bluetooth is powered on.

.isSearching()

Checks if noble is currently scanning. See .searchStart() and [.searchStop()](#method-search-stop)

Returns {Boolean} - true if searching.

.isStreaming()

Checks if the board is currently sending samples.

Returns {Boolean} - true if streaming

.numberOfChannels()

Get the current number of channels available to use. (i.e. 4).

Returns {Number} - The total number of available channels.

.printRegisterSettings()

Prints all register settings for the for board.

Returns {Promise} - Fulfilled if the command was sent to board.

.sampleRate()

Get the current sample rate.

Note: This is dependent on if you configured the board correctly on setup options. Specifically as a daisy.

Returns {Number} - The current sample rate.

.searchStart()

Call to make noble start scanning for Ganglions.

maxSearchTime {Number}

The amount of time to spend searching. (Default is 20 seconds)

Returns {Promise} - fulfilled if scan was started.

.searchStop()

Call to make noble stop scanning for Ganglions.

Returns {Promise} - fulfilled if scan was stopped.

.softReset()

Sends a soft reset command to the board.

Returns {Promise} - Fulfilled if the command was sent to board.

.streamStart()

Sends a start streaming command to the board.

Note, You must have called and fulfilled .connect() AND observed a 'ready' emitter before calling this method.

Returns {Promise} - fulfilled if the command was sent.

.streamStop()

Sends a stop streaming command to the board.

Note, You must have called and fulfilled .connect() AND observed a 'ready' emitter before calling this method.

Returns {Promise} - fulfilled if the command was sent.

.syntheticEnable()

Puts the board in synthetic data generation mode. Must call streamStart still.

Returns {Promise} - fulfilled if the command was sent.

.syntheticDisable()

Puts the board in synthetic data generation mode. Must call streamStart still.

Returns {Promise} - Indicating if the command was sent.

.write(data)

Used to send data to the board.

data {Array | Buffer | Number | String}

The data to write out.

Returns {Promise} - fulfilled if command was able to be sent.

Example

Sends a single character command to the board.

// ourBoard has fulfilled the promise on .connect() and 'ready' has been observed previously 
ourBoard.write('a');

Sends an array of bytes

// ourBoard has fulfilled the promise on .connect() and 'ready' has been observed previously 
ourBoard.write(['x','0','1','0','0','0','0','0','0','X']);

Call crazy? Go for it...

ourBoard.write('t');
ourBoard.write('a');
ourBoard.write('c');
ourBoard.write('o');

Events:

.on('accelerometer', callback)

Emitted when the module receives accelerometer data.

Returns an object with properties:

accelData {Array}

Array of floats for each dimension in g's.

NOTE: Only present if sendCounts is true.

accelDataCounts {Array}

Array of integers for each dimension in counts.

NOTE: Only present if sendCounts is false.

Example (if sendCounts is false):

{
  "accelData": [0.0, 0.0, 0.0, 0.0]
}

Example (if sendCounts is true):

{
  "accelDataCounts": [0, 0, 0, 0]
}

.on('droppedPacket', callback)

Emitted when a packet (or packets) are dropped. Returns an array.

.on('error', callback)

Emitted when there is an on the serial port.

.on('impedance', callback)

Emitted when there is a new impedance available.

Returns an object with properties:

channelNumber {Number}

The channel number: 1, 2, 3, 4 respectively and 0 for reference.

impedanceValue {Number}

The impedance in ohms.

Example:

{
  "channelNumber": 0,
  "impedanceValue": 0
}

.on('rawDataPacket', callback)

Emitted when there is a new raw data packet available.

.on('ready', callback)

Emitted when the board is in a ready to start streaming state.

.on('sample', callback)

Emitted when there is a new sample available.

Returns an object with properties:

channelData {Array}

Array of floats for each channel in volts..

NOTE: Only present if sendCounts is true.

channelDataCounts {Array}

Array of integers for each channel in counts.

NOTE: Only present if sendCounts is false.

sampleNumber {Number}

The sample number. Only goes up to 254.

timeStamp {Number}

The time the sample is packed up. Not accurate for ERP.

Example (if sendCounts is false):

{
  "channelData": [0.0, 0.0, 0.0, 0.0],
  "sampleNumber": 0,
  "timeStamp": 0
}

Example (if sendCounts is true):

{
  "channelDataCounts": [0, 0, 0, 0],
  "sampleNumber": 0,
  "timeStamp": 0
}

.on('scanStart', callback)

Emitted when a noble scan is started.

.on('scanStop', callback)

Emitted when a noble scan is stopped.

Interfacing With Other Tools:

LabStreamingLayer

LabStreamingLayer by SCCN is a stream management tool designed to time-synchronize multiple data streams, potentially from different sources, over a LAN network with millisecond accuracy (given configuration).

For example, a VR display device running a Unity simulation may, using the LSL4Unity library, emit string markers into LSL corresponding to events of interest (For the P300 ERP, this event would be the onset of an attended, unusual noise in a pattern of commonplace ones). The computer doing data collection via the OpenBCI_NodeJS library (potentially with 4ms accuracy) would then output into an LSL stream the EEG and AUX data. LSL can then synchronize the two clocks relative to each other before inputting into a different program or toolkit, like BCILAB for analysis to trigger responses in the Unity display.

This requires OpenBCI_NodeJS exporting data into LSL. Currently, there does not exist a pre-built NodeJS module for LSL, though LSL comes with tools that could possibly allow creation of one. In the meantime, the simpler route is to use a concurrent python script (driven by NodeJS module python-shell) to handoff the data to LSL for you, like so:

In your NodeJS code, before initializing/connecting to the OpenBCIBoard:

// Construct LSL Handoff Python Shell 
var PythonShell = require('python-shell');
var lsloutlet = new PythonShell('LslHandoff.py');
 
lsloutlet.on('message', function(message){
    console.log('LslOutlet: ' + message);
});
console.log('Python Shell Created for LSLHandoff');

In your NodeJS code, when reading samples:

st = sample.channelData.join(' ')
//getTime returns milliseconds since midnight 1970/01/01 
var s = ''+ sample.timeStamp + ''+ st
lsloutlet.send(s)

in LSLHandoff.py:

from pylsl import StreamInfo, StreamOutlet
info = StreamInfo('OpenBCI_EEG', 'EEG', 4, 200, 'float32', '[RANDOM NUMBER HERE]')
outlet = StreamOutlet(info)
while True:
    strSample = raw_input().split(': ',1)
    sample = map(float, strSample[1].split(' '))
    stamp = float(strSample[0])
 
    outlet.push_sample(sample, stamp)
    print('Pushed Sample At: ' + strSample[0])

AUX data would be done the same way in a separate LSL stream.

Developing:

Running:

npm install

Testing:

npm test

Contribute:

  1. Fork it!
  2. Branch off of development: git checkout development
  3. Create your feature branch: git checkout -b my-new-feature
  4. Make changes
  5. If adding a feature, please add test coverage.
  6. Ensure tests all pass. (npm test)
  7. Commit your changes: git commit -m 'Add some feature'
  8. Push to the branch: git push origin my-new-feature
  9. Submit a pull request. Make sure it is based off of the development branch when submitting! :D

License:

MIT