Skip to content

openbci-archive/OpenBCI_NodeJS_Cyton

Repository files navigation

Build Status codecov Dependency Status npm js-semistandard-style

OpenBCI Node.js Cyton SDK

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

We are proud to support all functionality of the Cyton (8 and 16 Channel boards). For the Ganglion (4 channel) please visit OpenBCI_NodeJS_Ganglion. 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.

Python researcher or developer? Check out how easy it is to get access to the entire API in the Python example!

Table of Contents:


  1. Installation
  2. TL;DR
  3. Cyton (32bit Board)
  4. About
  5. General Overview
  6. SDK Reference Guide
  7. Interfacing With Other Tools
  8. Developing
  9. Testing
  10. Contribute
  11. License
  12. Roadmap

Installation:

npm install @openbci/cyton

serialport dependency

If you encounter this error when trying to run:

Error: The module '/path/to/your/project/node_modules/serialport/build/Release/serialport.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 48. This version of Node.js requires
NODE_MODULE_VERSION 51. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or`npm install`).

...the issue can be resolved by running:

npm rebuild --build-from-source

TL;DR:

Get connected and start streaming right now with the example code.

Cyton (8 and 16 channel boards)

const Cyton = require('@openbci/cyton');
const ourBoard = new Cyton();
ourBoard.connect(portName) // Port name is a serial port name, see `.listPorts()`
  .then(() => {
    ourBoard.streamStart();
    ourBoard.on('sample',(sample) => {
      /** Work with sample */
      for (let i = 0; i < ourBoard.numberOfChannels(); i++) {
        console.log("Channel " + (i + 1) + ": " + sample.channelData[i].toFixed(8) + " Volts.");
        // prints to the console
        //  "Channel 1: 0.00001987 Volts."
        //  "Channel 2: 0.00002255 Volts."
        //  ...
        //  "Channel 8: -0.00001875 Volts."
      }
    });
});

Cyton

About:

Want to know if the module really works? Check out some projects and organizations using it:

  • OpenEXP: an open-source desktop app for running experiments and collecting behavioral and physiological data.
  • Thinker: a project building the world's first brainwave-word database.
  • NeuroJS: a community dedicated to Neuroscience research using JavaScript, they have several great examples.

Still not satisfied it works?? Check out this detailed report that scientifically validates the output voltages of this module.

How are you still doubting and not using this already? Fine, go look at some of the 800 automatic tests written for it!

General Overview:

Python researcher or developer? Check out how easy it is to get access to the entire API in the Python example!

Initialization

Initializing the board:

const Cyton = require("@openbci/cyton");
const ourBoard = new Cyton();

Go checkout out the get streaming example!

For initializing with options, such as verbose print outs:

const Cyton = require("@openbci/cyton");
const ourBoard = new Cyton({
  verbose: true
});

Or if you don't have a board and want to use synthetic data:

const Cyton = require("@openbci/cyton");
const ourBoard = new Cyton({
  simulate: true
});

Have a daisy?:

var Cyton = require("@openbci/cyton");
var ourBoard = new Cyton({
  boardType: `daisy`,
  hardSet: true
});

Go checkout out the get streaming with daisy example!

Another useful way to start the simulator:

const Cyton = require("@openbci/cyton");
const k = require("@openbci/utilities").constants;
const ourBoard = new Cyton();
ourBoard
  .connect(k.OBCISimulatorPortName) // This will set `simulate` to true
  .then(boardSerial => {
    ourBoard.on("ready", () => {
      /** Start streaming, reading registers, what ever your heart desires  */
    });
  })
  .catch(err => {
    /** Handle connection errors */
  });

You can also start the simulator by sending .connect(portName) with portName equal to 'OpenBCISimulator'.

or if you are using ES6:

import Cyton from "@openbci/cyton";
import { constants } from "@openbci/utilities";
const ourBoard = new Cyton();
ourBoard.connect(constants.OBCISimulatorPortName);

To debug, it's amazing, do:

const Cyton = require("@openbci/cyton");
const ourBoard = new Cyton({
  debug: true
});

Go checkout out the debug example!

'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 Cyton = require("@openbci/cyton");
const ourBoard = new Cyton();
ourBoard
  .connect(portName)
  .then(function(boardSerial) {
    /** Start streaming, reading registers, what ever your heart desires  */
  })
  .catch(function(err) {
    /** Handle connection errors */
  });

Sample properties:

  • startByte (Number should be 0xA0)
  • sampleNumber (a Number between 0-255)
  • channelData (channel data indexed at 0 filled with floating point Numbers in Volts) if sendCounts is false
  • channelDataCounts (channel data indexed at 0 filled with un scaled interger Numbers in raw ADS counts) if sendCounts is true
  • accelData (Array with X, Y, Z accelerometer values when new data available) if sendCounts is false
  • accelDataCounts (Array with X, Y, Z accelerometer values when new data available) Only present if sendCounts is true
  • auxData (Buffer filled with either 2 bytes (if time synced) or 6 bytes (not time synced))
  • stopByte (Number should be 0xCx where x is 0-15 in hex)
  • boardTime (Number the raw board time)
  • 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(serialPortName)
  2. Install the 'ready' event emitter on resolved promise
  3. In callback for 'ready' emitter, call streamStart()
  4. Install the 'sample' event emitter
const Cyton = require("@openbci/cyton");
const ourBoard = new Cyton();
ourBoard
  .connect(portName)
  .then(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 Cyton = require("@openbci/cyton");
const ourBoard = new Cyton();
ourBoard.streamStop().then(ourBoard.disconnect());

Time Syncing

You must be using OpenBCI firmware version 2 in order to do time syncing. After you .connect() and send a .softReset(), you can call .usingVersionTwoFirmware() to get a boolean response as to if you are using v1 or v2.

Now using firmware v2, the fun begins! We synchronize the Board's clock with the module's time. In firmware v2 we leverage samples with time stamps and ACKs from the Dongle to form a time synchronization strategy. Time syncing has been verified to +/- 4ms and a test report is on the way. We are still working on the synchronize of this module and an NTP server, this is an open call for any NTP experts out there! With a global NTP server you could use several different devices and all sync to the same time server. That way you can really do some serious cloud computing!

Keep your resync interval above 50ms. While it's important to resync every couple minutes due to drifting of clocks, please do not try to sync without getting the last sync event! We can only support one sync operation at a time!

Using local computer time:

const Cyton = require("@openbci/cyton");
const k = require("@openbci/utilities").constants;
const ourBoard = new Cyton({
  verbose: true
});

const resyncPeriodMin = 5; // re sync every five minutes
const secondsInMinute = 60;
let sampleRate = k.OBCISampleRate250; // Default to 250, ALWAYS verify with a call to `.sampleRate()` after 'ready' event!
let timeSyncPossible = false;

// Call to connect
ourBoard
  .connect(portName)
  .then(() => {
    // Get the sample rate after 'ready'
    sampleRate = ourBoard.sampleRate();
    // Find out if you can even time sync, you must be using v2 and this is only accurate after a `.softReset()` call which is called internally on `.connect()`. We parse the `.softReset()` response for the presence of firmware version 2 properties.
    timeSyncPossible = ourBoard.usingVersionTwoFirmware();

    ourBoard
      .streamStart()
      .then(() => {
        /** Start streaming command sent to board. */
      })
      .catch(err => {
        console.log(`stream start: ${err}`);
      });

    // PTW recommends sample driven
    ourBoard.on("sample", sample => {
      // Resynchronize every every 5 minutes
      if (
        sample._count %
          (sampleRate * resyncPeriodMin * secondsInMinute) ===
        0
      ) {
        ourBoard.syncClocksFull().then(syncObj => {
          // Sync was successful
          if (syncObj.valid) {
            // Log the object to check it out!
            console.log(`syncObj`, syncObj);

            // Sync was not successful
          } else {
            // Retry it
            console.log(`Was not able to sync, please retry?`);
          }
        });
      }

      if (sample.timeStamp) {
        // true after the first sync
        console.log(`NTP Time Stamp ${sample.timeStamp}`);
      }
    });
  })
  .catch(err => {
    console.log(`connect: ${err}`);
  });

Auto-finding boards

You must have the OpenBCI board connected to the PC before trying to automatically find it.

If a port is not automatically found, then call .listPorts() to get a list of all serial ports this would be a good place to present a drop down picker list to the user, so they may manually select the serial port name.

const Cyton = require("@openbci/cyton");
const ourBoard = new Cyton();
ourBoard.autoFindOpenBCIBoard().then(portName => {
  if (portName) {
    /**
     * Connect to the board with portName
     * i.e. ourBoard.connect(portName).....
     */
  } else {
    /**Unable to auto find OpenBCI board*/
  }
});

Note: .autoFindOpenBCIBoard() will return the name of the Simulator if you instantiate with option simulate: true.

Auto Test - (Using impedance to determine signal quality)

Measuring impedance is a vital tool in ensuring great data is collected.

IMPORTANT! Measuring impedance takes time, so only test what you must

Your OpenBCI board will have electrodes hooked up to either a P input, N input or in some cases both inputs.

To test specific inputs of channels:

  1. Connect to board.
  2. Start streaming.
  3. Install the 'impedanceArray' event
  4. Call .impedanceTestChannels() with your configuration array

A configuration array looks like, for an 8 channel board, ['-','N','n','p','P','-','b','b']

Where there are the same number of elements as channels and each element can be either:

  • p or P (only test P input)
  • n or N (only test N input)
  • b or B (test both inputs) (takes 66% longer to run then previous two p or n)
  • - (ignore channel)

Without further ado, here is an example:

const Cyton = require("@openbci/cyton");
const ourBoard = new Cyton();
ourBoard
  .connect(portName)
  .then(function(boardSerial) {
    ourBoard.streamStart();
    ourBoard.once("impedanceArray", impedanceArray => {
      /** Work with impedance Array */
    });
    ourBoard
      .impedanceTestChannels(["n", "N", "n", "p", "P", "p", "b", "B"])
      .catch(err => console.log(err));
  })
  .catch(function(err) {
    /** Handle connection errors */
  });

But wait! What is this impedanceArray? An Array of Objects, for each object:

[{
    channel: 1,
    P: {
        raw: -1,
        text: 'init'
    },
    N: {
        raw: -1,
        text: 'init'
    }
},
{
    // Continues for each channel up to the amount of channels on board (8 or 16)
},...];

Where:

  • channel is the channel number (impedanceArray[0] is channel 1, impedanceArray[6] is channel 7)
  • P is the P input data (Note: P is capitalized)
    • raw is an impedance value resulting from the Goertzel algorithm.
    • text is a text interpretation of the average
      • Good impedance is < 5k Ohms
      • Ok impedance is 5 to 10k Ohms
      • Bad impedance is > 10k Ohms
      • None impedance is > 1M Ohms
  • N is the N input data (Note: N is capitalized) (see above for what N object consists of)

To run an impedance test on all inputs, one channel at a time:

  1. Connect to board
  2. Start streaming
  3. Install the 'impedanceArray'
  4. Call .impedanceTestAllChannels()

Note: Takes up to 5 seconds to start measuring impedances. There is an unknown number of samples taken. Not always 60!

For example:

const Cyton = require('@openbci/cyton');
const ourBoard = new Cyton();
ourBoard.connect(portName).then(function(boardSerial) {
    ourBoard.streamStart();
    ourBoard.on('impedanceArray', impedanceArray => {
        /** Work with impedance */
    });
    ourBoard.impedanceTestAllChannels();
}

See Reference Guide for a complete list of impedance tests.

SDK Reference Guide:


Constructor:

Cyton (options)

Create new instance of an Cyton board.

options (optional)

Board optional configurations.

  • baudRate {Number} - Baud Rate, defaults to 115200. Manipulating this is allowed if firmware on board has been previously configured.
  • boardType {String} - Specifies type of OpenBCI board (3 possible boards)
    • default - 8 Channel OpenBCI board (Default)
    • daisy - 8 Channel board with Daisy Module - 16 Channels
  • hardSet {Boolean} - Recommended if using daisy board! For some reason, the daisy is sometimes not picked up by the module so you can set hardSet to true which will ensure the daisy is picked up. (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)
  • simulatorDaisyModuleAttached {Boolean} - Simulates a daisy module being attached to the OpenBCI board. This is useful if you want to test how your application reacts to a user requesting 16 channels but there is no daisy module actually attached, or vice versa, where there is a daisy module attached and the user only wants to use 8 channels. (Default false)
  • simulatorFirmwareVersion {String} - Allows the simulator to use firmware version 2 features. (2 Possible Options)
    • v1 - Firmware Version 1 (Default)
    • v2 - Firmware Version 2
  • simulatorFragmentation {String} - Specifies how to break packets to simulate fragmentation, which occurs commonly in real devices. It is recommended to test code with this enabled. (4 Possible Options)
    • none - do not fragment packets; output complete chunks immediately when produced (Default)
    • random - output random small chunks of data interspersed with full buffers
    • fullBuffers - allow buffers to fill up until latency timer has expired
    • oneByOne - output each byte separately
  • simulatorLatencyTime {Number} - The time in milliseconds to wait before sending partially full buffers of data, if simulatorFragmentation is specified. (Default 16)
  • simulatorBufferSize {Number} - The size of a full buffer of data, if simulatorFragmentation is specified. (Default 4096)
  • 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)
  • simulatorSerialPortFailure {Boolean} - Simulates not being able to open a serial connection. Most likely due to a OpenBCI dongle not being plugged in.
  • sntpTimeSync - {Boolean} Syncs the module up with an SNTP time server and uses that as single source of truth instead of local computer time. If you are running experiments on your local computer, keep this false. (Default false)
  • sntpTimeSyncHost - {String} The sntp server to use, can be either sntp or ntp (Defaults pool.ntp.org).
  • sntpTimeSyncPort - {Number} The port to access the sntp server (Defaults 123)
  • verbose {Boolean} - Print out useful debugging events (Default false)
  • debug {Boolean} - Print out a raw dump of bytes sent and received (Default false)

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

Methods:

.autoFindOpenBCIBoard()

Automatically find an OpenBCI board.

Note: This will always return an Array of COM ports on Windows

Returns a promise, fulfilled with a portName such as /dev/tty.* on Mac/Linux or OpenBCISimulator if this.options.simulate === true.

.channelOff(channelNumber)

Turn off a specified channel

channelNumber

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

Returns a promise, fulfilled if the command was sent to the write queue.

.channelOn(channelNumber)

Turn on a specified channel

channelNumber

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

Returns a promise, fulfilled if the command was sent to the write queue.

.channelSet(channelNumber,powerDown,gain,inputType,bias,srb2,srb1)

Send a channel setting command to the board.

channelNumber

Determines which channel to set. It's a 'Number' (1-16)

powerDown

Powers the channel up or down. It's a 'Bool' where true turns the channel off and false turns the channel on (default)

gain

Sets the gain for the channel. It's a 'Number' that is either (1,2,4,6,8,12,24(default))

inputType

Selects the ADC channel input source. It's a 'String' that MUST be one of the following: "normal", "shorted", "biasMethod" , "mvdd" , "temp" , "testsig", "biasDrp", "biasDrn".

bias

Selects if the channel shall include the channel input in bias generation. It's a 'Bool' where true includes the channel in bias (default) or false it removes it from bias.

srb2

Select to connect (true) this channel's P input to the SRB2 pin. This closes a switch between P input and SRB2 for the given channel, and allows the P input to also remain connected to the ADC. It's a 'Bool' where true connects this input to SRB2 (default) or false which disconnect this input from SRB2.

srb1

Select to connect (true) all channels' N inputs to SRB1. This effects all pins, and disconnects all N inputs from the ADC. It's a 'Bool' where true connects all N inputs to SRB1 and false disconnects all N inputs from SRB1 (default).

Returns a promise fulfilled if proper commands sent to the write queue, rejects on bad input or no board.

Example

ourBoard.channelSet(2, false, 24, "normal", true, true, false);
// sends ['x','2','0','6','0','1','1','0','X'] to the command queue

.connect(portName)

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

portName

The system path of the OpenBCI board serial port to open. For example, /dev/tty on Mac/Linux or COM1 on Windows.

Returns a promise, fulfilled by a successful serial connection to the board.

.debugSession()

Calls all .printPacketsBad(), .printPacketsRead(), .printBytesIn()

.disconnect()

Closes the serial port opened by .connect(). Waits for stop streaming command to be sent if currently streaming.

Returns a promise, fulfilled by a successful close of the serial port object, rejected otherwise.

.getInfo()

Get the core info object. It's the object that actually drives the parsing of data.

Returns Object - {{boardType: string, sampleRate: number, firmware: string, numberOfChannels: number, missedPackets: number}}

.getSettingsForChannel(channelNumber)

Gets the specified channelSettings register data from printRegisterSettings call.

channelNumber

A number specifying which channel you want to get data on. Only 1-8 at this time.

Note, at this time this does not work for the daisy board

Returns a promise, fulfilled if the command was sent to the board and the .processBytes() function is ready to reach for the specified channel.

.hardSetBoardType(boardType)

Used to sync the module and board to boardType.

Note: This has the potential to change the way data is parsed

boardType

A String indicating the number of channels.

  • default - Default board: Sample rate is 250Hz and number of channels is 8.
  • daisy - Daisy board: Sample rate is 125Hz and number of channels is 16.

Returns a promise, fulfilled if both the board and module are the requested boardType, rejects otherwise.

.impedanceTestAllChannels()

To apply test signals to the channels on the OpenBCI board used to test for impedance. This can take a little while to actually run (<8 seconds)!

Don't forget to install the 'impedanceArray' emitter to receive the impendances!

Note, you must be connected in order to set the test commands. Also this method can take up to 5 seconds to send all commands!

Returns a promise upon completion of test.

.impedanceTestChannels(arrayOfCommands)

arrayOfCommands

The array of configurations where there are the same number of elements as channels and each element can be either:

  • p or P (only test P input)
  • n or N (only test N input)
  • b or B (test both inputs) (takes 66% longer to run then previous two p or n)
  • - (ignore channel)

Don't forget to install the impedanceArray emitter to receive the impendances!

Note, you must be connected in order to set the test commands. Also this method can take up to 5 seconds to send all commands!

Returns a promise upon completion of test.

.impedanceTestChannel(channelNumber)

Run a complete impedance test on a single channel, applying the test signal individually to P & N inputs.

channelNumber

A Number, specifies which channel you want to test.

Returns a promise that resolves a single channel impedance object.

Example

const Cyton = require("@openbci/cyton");
const ourBoard = new Cyton();
ourBoard
  .connect(portName)
  .then(function(boardSerial) {
    ourBoard.on("ready", function() {
      ourBoard.streamStart();
      ourBoard
        .impedanceTestChannel(1)
        .then(impedanceObject => {
          /** Do something with impedanceObject! */
        })
        .catch(err => console.log(err));
    });
  })
  .catch(function(err) {
    /** Handle connection errors */
  });

Where an impedance for this method call would look like:

{
  "channel": 1,
  "P": {
    "raw": 2394.45,
    "text": "good"
  },
  "N": {
    "raw": 7694.45,
    "text": "ok"
  }
}

.impedanceTestChannelInputP(channelNumber)

Run impedance test on a single channel, applying the test signal only to P input.

channelNumber

A Number, specifies which channel you want to test.

Returns a promise that resolves a single channel impedance object.

Example

const Cyton = require("@openbci/cyton");
const ourBoard = new Cyton();
ourBoard
  .connect(portName)
  .then(() => {
    ourBoard.on("ready", () => {
      ourBoard.streamStart();
      ourBoard
        .impedanceTestChannelInputP(1)
        .then(impedanceObject => {
          /** Do something with impedanceObject! */
        })
        .catch(err => console.log(err));
    });
  })
  .catch(function(err) {
    /** Handle connection errors */
  });

Where an impedance for this method call would look like:

{
  "channel": 1,
  "P": {
    "raw": 2394.45,
    "text": "good"
  },
  "N": {
    "raw": -1,
    "text": "init"
  }
}

.impedanceTestChannelInputN(channelNumber)

Run impedance test on a single channel, applying the test signal only to N input.

channelNumber

A Number, specifies which channel you want to test.

Returns a promise that resolves a single channel impedance object.

Example

const Cyton = require('@openbci/cyton');
const ourBoard = new Cyton();
ourBoard.connect(portName).then(() => {
    ourBoard.on('ready',() => {
        ourBoard.streamStart();
        ourBoard.impedanceTestChannelInputN(1)
            .then(impedanceObject => {
                /** Do something with impedanceObject! */
            })
            .catch(err => console.log(err));
    });
}).catch(err => console.log(err));

Where an impedance for this method call would look like:

{
  "channel": 1,
  "P": {
    "raw": -1,
    "text": "init"
  },
  "N": {
    "raw": 7694.45,
    "text": "ok"
  }
}

.impedanceTestContinuousStart()

Sends command to turn on impedances for all channels and continuously calculate their impedances.

Returns a promise, that fulfills when all the commands are sent to the internal write buffer

.impedanceTestContinuousStop()

Sends command to turn off impedances for all channels and stop continuously calculate their impedances.

Returns a promise, that fulfills when all the commands are sent to the internal write buffer

.isConnected()

Checks if the driver is connected to a board. Returns a boolean, true if connected

.isStreaming()

Checks if the board is currently sending samples. Returns a boolean, true if streaming

.listPorts()

List available ports so the user can choose a device when not automatically found.

Returns a promise, fulfilled with a list of available serial ports.

.numberOfChannels()

Get the current number of channels available to use. (i.e. 8 or 16).

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

Returns a number, the total number of available channels.

.overrideInfoForBoardType(boardType)

Set the info property for board type.

Note: This has the potential to change the way data is parsed

boardType

A String indicating the number of channels.

  • default - Default board: Sample rate is 250Hz and number of channels is 8.
  • daisy - Daisy board: Sample rate is 125Hz and number of channels is 16.

.printBytesIn()

Prints the total number of bytes that were read in this session to the console.

.printPacketsBad()

Prints the total number of packets that were not able to be read in this session to the console.

.printPacketsRead()

Prints the total number of packets that were read in this session to the console.

.printRegisterSettings()

Prints all register settings for the ADS1299 and the LIS3DH on the OpenBCI board.

Returns a promise, fulfilled if the command was sent to the write queue.

.radioBaudRateSet(speed)

Used to set the OpenBCI Host (Dongle) baud rate. With the RFduino configuration, the Dongle is the Host and the Board is the Device. Only the Device can initiate a communication between the two entities. There exists a detrimental error where if the Host is interrupted by the radio during a Serial write, then all hell breaks loose. So this is an effort to eliminate that problem by increasing the rate at which serial data is sent from the Host to the Serial driver. The rate can either be set to default or fast. Further the function should reject if currently streaming. Lastly and more important, if the board is not running the new firmware then this functionality does not exist and thus this method will reject. If the board is using firmware 2+ then this function should resolve the new baud rate after closing the current serial port and reopening one.

Note, this functionality requires OpenBCI Firmware Version 2.0

speed

{String} - The baud rate that to switch to. Can be either default (115200) or fast (230400).

Returns {Promise} - Resolves a {Number} that is the new baud rate, rejects on error.

.radioChannelGet()

Used to query the OpenBCI system for it's radio channel number. The function will reject if not connected to the serial port of the dongle. Further the function should reject if currently streaming. Lastly and more important, if the board is not running the new firmware then this functionality does not exist and thus this method will reject. If the board is using firmware 2+ then this function should resolve an Object. See returns below.

Note, this functionality requires OpenBCI Firmware Version 2.0

Returns {Promise} - Resolve an object with keys channelNumber which is a Number and err which contains an error in the condition that there system is experiencing board communications failure.

.radioChannelSet(channelNumber)

Used to set the system radio channel number. The function will reject if not connected to the serial port of the dongle. Further the function should reject if currently streaming. Lastly and more important, if the board is not running the new firmware then this functionality does not exist and thus this method will reject. If the board is using firmware 2+ then this function should resolve.

Note, this functionality requires OpenBCI Firmware Version 2.0

channelNumber

{Number} - The channel number you want to set to, 1-25.

Returns {Promise} - Resolves with the new channel number, rejects with err.

.radioChannelSetHostOverride(channelNumber)

Used to set the ONLY the radio dongle Host channel number. This will fix your radio system if your dongle and board are not on the right channel and bring down your radio system if you take your dongle and board are not on the same channel. Use with caution! The function will reject if not connected to the serial port of the dongle. Further the function should reject if currently streaming. Lastly and more important, if the board is not running the new firmware then this functionality does not exist and thus this method will reject. If the board is using firmware 2+ then this function should resolve.

Note, this functionality requires OpenBCI Firmware Version 2.0

channelNumber

{Number} - The channel number you want to set to, 1-25.

Returns {Promise} - Resolves with the new channel number, rejects with err.

.radioPollTimeGet()

Used to query the OpenBCI system for it's device's poll time. The function will reject if not connected to the serial port of the dongle. Further the function should reject if currently streaming. Lastly and more important, if the board is not running the new firmware then this functionality does not exist and thus this method will reject. If the board is using firmware 2+ then this function should resolve the poll time when fulfilled. It's important to note that if the board is not on, this function will always be rejected with a failure message.

Note, this functionality requires OpenBCI Firmware Version 2.0

Returns {Promise} - Resolves with the new poll time, rejects with err.

.radioPollTimeSet(pollTime)

Used to set the OpenBCI poll time. With the RFduino configuration, the Dongle is the Host and the Board is the Device. Only the Device can initiate a communication between the two entities. Therefore this sets the interval at which the Device polls the Host for new information. Further the function should reject if currently streaming. Lastly and more important, if the board is not running the new firmware then this functionality does not exist and thus this method will reject. If the board is using firmware 2+ then this function should resolve.

Note, this functionality requires OpenBCI Firmware Version 2.0

pollTime

{Number} - The poll time you want to set to, 0-255.

Returns {Promise} - Resolves with the new channel number, rejects with err.

.radioSystemStatusGet()

Used to ask the Host if it's radio system is up. This is useful to quickly determine if you are in fact ready to start trying to connect and such. The function will reject if not connected to the serial port of the dongle. Further the function should reject if currently streaming. Lastly and more important, if the board is not running the new firmware then this functionality does not exist and thus this method will reject. If the board is using firmware +v2.0.0 and the radios are both on the same channel and powered, then this will resolve true.

Note, this functionality requires OpenBCI Firmware Version 2.0

Returns {Promise} - Resolves true if both radios are powered and on the same channel; false otherwise.

.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 a number, the current sample rate.

.sdStart(recordingDuration)

Start logging to the SD card. If you are not streaming when you send this command, then you should expect to get a success or failure message followed by and end of transmission $$$.

recordingDuration

The duration you want to log SD information for. Opens a new SD file to write into. Limited to:

  • 14sec - 14 seconds
  • 5min - 5 minutes
  • 15min - 15 minutes
  • 30min - 30 minutes
  • 1hour - 1 hour
  • 2hour - 2 hour
  • 4hour - 4 hour
  • 12hour - 12 hour
  • 24hour - 24 hour

Note: You must have the proper type of SD card inserted into the board for logging to work.

Returns resolves if the command was added to the write queue.

.sdStop()

Stop logging to the SD card and close any open file. If you are not streaming when you send this command, then you should expect to get a success or failure message followed by and end of transmission $$$. The success message contains a lot of useful information about what happened when writing to the SD card.

Returns resolves if the command was added to the write queue.

.simulatorEnable()

To enter simulate mode. Must call .connect() after.

Note, must be called after the constructor.

Returns a promise, fulfilled if able to enter simulate mode, reject if not.

.simulatorDisable()

To leave simulate mode.

Note, must be called after the constructor.

Returns a promise, fulfilled if able to stop simulate mode, reject if not.

.sntp

Extends the popular SNTP package on npmjs

.sntpGetOffset()

Stateful method for querying the current offset only when the last one is too old. (defaults to daily)

Returns a promise with the time offset

.sntpStart()

This starts the SNTP server and gets it to remain in sync with the SNTP server.

Returns a promise if the module was able to sync with NTP server.

.sntpStop()

Stops the SNTP from updating

.softReset()

Sends a soft reset command to the board.

Note, this method must be sent to the board before you can start streaming. This triggers the initial 'ready' event emitter.

Returns a promise, fulfilled if the command was sent to the write queue.

.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 a promise, fulfilled if the command was sent to the write queue, rejected if unable.

.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 a promise, fulfilled if the command was sent to the write queue, rejected if unable.

.syncClocks()

Send the command to tell the board to start the syncing protocol. Must be connected, streaming and using version +2 firmware.

Note, this functionality requires OpenBCI Firmware Version 2.0

Returns {Promise} resolves if the command was sent to the write queue, rejects if unable.

.syncClocksFull()

Send the command to tell the board to start the syncing protocol. Must be connected, streaming and using v2 firmware. Uses the synced event to ensure multiple syncs don't overlap.

Note, this functionality requires OpenBCI Firmware Version 2.0

Returns {Promise} resolves if synced event is emitted, rejects if not connected or using firmware v2. Resolves with a synced object:

{
    "boardTime": 0, // The time contained in the time sync set packet.
    "correctedTransmissionTime": false, // If the confirmation and the set packet arrive in the same serial flush we have big problem! This will be true in this case. See source code for full explanation.
    "timeSyncSent": 0, // The time the `<` was sent to the Dongle.
    "timeSyncSentConfirmation": 0, // The time the `<` was sent to the Board; It's really the time `,` was received from the Dongle.
    "timeSyncSetPacket": 0, // The time the set packet was received from the Board.
    "timeRoundTrip": 0, // Simply timeSyncSetPacket - timeSyncSent.
    "timeTransmission": 0, // Estimated time it took for time sync set packet to be sent from Board to Driver.
    "timeOffset": 0, // The map (or translation) from boardTime to module time.
    "timeOffsetMaster": 0, // The map (or translation) from boardTime to module time averaged over time syncs.
    "valid": false // If there was an error in the process, valid will be false and no time sync was done. It's important to resolve this so we can perform multiple promise syncs as show in the example below.
}

Example

Syncing multiple times to base the offset of the average of the four syncs.

const Cyton = require('@openbci/cyton');
const ourBoard = new Cyton({
  verbose:true
});
let portName = /* INSERT PORT NAME HERE */;
let samples = []; // Array to store time synced samples into
let timeSyncActivated = false;

ourBoard.connect(portName)
  .then(() => {
    ourBoard.on('ready',() => {
      ourBoard.streamStart()
        .then(() => {
          /** Could also call `.syncClocksFull()` here */
        })
        .catch(err => {
          console.log(`Error starting stream ${err}`);
        })
    });
    ourBoard.on('sample',sample => {
      /** If we are not synced, then do that! */
      if (timeSyncActivated === false) {
        timeSyncActivated = true;
        ourBoard.syncClocksFull()
          .then(syncObj => {
            if (syncObj.valid) {
              console.log('1st sync done');
            }
            return ourBoard.syncClocksFull();
          })
          .then(syncObj => {
            if (syncObj.valid) {
              console.log('2nd sync done');
            }
            return ourBoard.syncClocksFull();
          })
          .then(syncObj => {
            if (syncObj.valid) {
              console.log('3rd sync done');
            }
            return ourBoard.syncClocksFull();
          })
          .then(syncObj => {
            if (syncObj.valid) {
              console.log('4th sync done');
            }
            /* Do awesome time syncing stuff */
          })
          .catch(err => {
            console.log(`sync err ${err}`);
          });
      }
      if (sample.hasOwnProperty("timeStamp") && sample.hasOwnProperty("boardTime")) {
        /** If you only want to log samples with time stamps */
        samples.push(sample);
      }
    });
})
.catch(err => {
  console.log(`connect ${err}`);
});

.syncRegisterSettings()

Syncs the internal channel settings object with a cyton, this will take about over a second because there are delays between the register reads in the firmware.

Returns a promise, fulfilled with channel settings array, where each element is a standard channel setting object, once the channel settings have been synced and reject on error.

.testSignal(signal)

Apply the internal test signal to all channels.

signal

A String indicating which test signal to apply

  • dc - Connect to DC signal
  • ground - Connect to internal GND (VDD - VSS)
  • pulse1xFast - Connect to test signal 1x Amplitude, fast pulse
  • pulse1xSlow - Connect to test signal 1x Amplitude, slow pulse
  • pulse2xFast - Connect to test signal 2x Amplitude, fast pulse
  • pulse2xSlow - Connect to test signal 2x Amplitude, slow pulse
  • none - Reset to default

Returns a promise, if the commands were sent to write buffer.

.time()

Uses ._sntpNow() time when sntpTimeSync specified true in options, or else Date.now() for time.

Returns time since UNIX epoch in ms.

.usingVersionTwoFirmware()

Convenience method to determine if you can use firmware v2.x.x capabilities.

Note, should be called after a .softReset() because we can parse the output of that to determine if we are using firmware version 2.

Returns a boolean, true if using firmware version 2 or greater.

.write(dataToWrite)

Send commands to the board. Due to the OpenBCI board firmware 1.0, a 10ms spacing must be observed between every command sent to the board. This method handles the timing and spacing between characters by adding characters to a global write queue and pulling from it every 10ms. If you are using firmware version +2.0 then you no spacing will be used.

dataToWrite

Either a single character or an Array of characters

Returns a promise, fulfilled if the board has been connected and dataToWrite has been added to the write queue, rejected if there were any problems.

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"]);

Taking full advantage of the write queue. The following would be sent at t = 0, 10ms, 20ms, 30ms

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

Events:

.on('close', callback)

Emitted when the serial connection to the board is closed.

.on('droppedPacket', callback)

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

.on('eot', callback)

Emitted when there is an EOT a.k.a. '$$$' with a buffer filled with the data.

.on('error', callback)

Emitted when there is an on the serial port.

.on('hardSet', callback)

Emitted when the module detects the board is not configured as the options for the module intended and tries to save itself. i.e. when the daisy option is true and a soft reset message is parsed and the module determines that a daisy was not detected, the module will emit hardSet then send an attach daisy command to recover. Either error will be emitted if unable to attach or ready will be emitted if success.

.on('impedanceArray', callback)

Emitted when there is a new impedanceArray available. Returns an array.

.on('query', callback)

Emitted resulting in a call to .getChannelSettings() with the channelSettingsObject

.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.

.on('synced', callback)

Emitted when there is a new sample available.

Constants:

To use the constants file simply:

const k = require("@openbci/utilities").constants;

console.log(k.OBCISimulatorPortName); // prints OpenBCISimulator to the console.

.OBCISimulatorPortName

The name of the simulator port.

Interfacing With Other Tools:

LabStreamingLayer

LabStreamingLayer is a tool for streaming or recording time-series data. It can be used to interface with Matlab, Python, Unity, and many other programs.

To use LSL with the NodeJS SDK, go to our labstreaminglayer example, which contains code that is ready to start an LSL stream of OpenBCI data.

Follow the directions in the readme to get started.

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

Roadmap:

  1. Ganglion integration (3.x)
  2. Compatible with node streams (3.x)
  3. Remove factory paradigm from main file (3.x)
  4. ES6/ES7 total adoption (3.x)
  5. Browser support (with browser serialport) (x.x)