lsl example for the ganglion
Esse commit está contido em:
+15
-49
@@ -65,12 +65,12 @@ Please ensure [Python 2.7 is installed](https://www.python.org/downloads/) for a
|
||||
### macOS
|
||||
|
||||
* install [Xcode](https://itunes.apple.com/ca/app/xcode/id497799835?mt=12)
|
||||
|
||||
|
||||
### Linux
|
||||
|
||||
* Kernel version 3.6 or above
|
||||
* ```libbluetooth-dev```
|
||||
|
||||
|
||||
### Windows 8+
|
||||
|
||||
* [node-gyp requirements for Windows](https://github.com/TooTallNate/node-gyp#installation)
|
||||
@@ -82,7 +82,7 @@ Please ensure [Python 2.7 is installed](https://www.python.org/downloads/) for a
|
||||
|
||||
See [@don](https://github.com/don)'s set up guide on [Bluetooth LE with Node.js and Noble on Windows](https://www.youtube.com/watch?v=mL9B8wuEdms).
|
||||
|
||||
|
||||
|
||||
## <a name="install"></a> Installation:
|
||||
Install from npm:
|
||||
```
|
||||
@@ -91,7 +91,7 @@ npm install openbci-ganglion
|
||||
|
||||
## <a name="about"></a> About:
|
||||
|
||||
The Ganglion driver used by OpenBCI's Processing GUI and Electron Hub.
|
||||
The Ganglion driver used by OpenBCI's Processing GUI and Electron Hub.
|
||||
|
||||
Check out the [**_automatic_** tests](https://codecov.io/gh/OpenBCI/OpenBCI_NodeJS_Ganglion) written for it!
|
||||
|
||||
@@ -333,13 +333,13 @@ Checks if the driver is connected to a board.
|
||||
|
||||
Checks if bluetooth is powered on. Cannot start scanning till this is true.
|
||||
|
||||
**_Returns_** {Boolean} - true if bluetooth is powered on.
|
||||
**_Returns_** {Boolean} - true if bluetooth is powered on.
|
||||
|
||||
#### <a name="method-is-searching"></a> .isSearching()
|
||||
|
||||
Checks if noble is currently scanning. See [`.searchStart()`](#method-search-start) and [`.searchStop`()`](#method-search-stop`)
|
||||
|
||||
**_Returns_** {Boolean} - true if searching.
|
||||
**_Returns_** {Boolean} - true if searching.
|
||||
|
||||
#### <a name="method-is-streaming"></a> .isStreaming()
|
||||
|
||||
@@ -373,7 +373,7 @@ Call to make `noble` start scanning for Ganglions.
|
||||
|
||||
**_maxSearchTime_** {Number}
|
||||
|
||||
The amount of time to spend searching. (Default is 20 seconds)
|
||||
The amount of time to spend searching. (Default is 20 seconds)
|
||||
|
||||
**_Returns_** {Promise} - fulfilled if scan was started.
|
||||
|
||||
@@ -459,13 +459,13 @@ Returns an object with properties:
|
||||
|
||||
**_accelData_** {Array}
|
||||
|
||||
Array of floats for each dimension in g's.
|
||||
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.
|
||||
Array of integers for each dimension in counts.
|
||||
|
||||
**NOTE:** Only present if `sendCounts` is `false`.
|
||||
|
||||
@@ -493,7 +493,7 @@ Emitted when there is an on the serial port.
|
||||
|
||||
#### <a name="event-impedance"></a> .on('impedance', callback)
|
||||
|
||||
Emitted when there is a new impedance available.
|
||||
Emitted when there is a new impedance available.
|
||||
|
||||
Returns an object with properties:
|
||||
|
||||
@@ -529,13 +529,13 @@ Returns an object with properties:
|
||||
|
||||
**_channelData_** {Array}
|
||||
|
||||
Array of floats for each channel in volts..
|
||||
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.
|
||||
Array of integers for each channel in counts.
|
||||
|
||||
**NOTE:** Only present if `sendCounts` is `false`.
|
||||
|
||||
@@ -577,46 +577,12 @@ Emitted when a noble scan is stopped.
|
||||
|
||||
### <a name="interfacing-with-other-tools-labstreaminglayer"></a> LabStreamingLayer
|
||||
|
||||
[LabStreamingLayer](https://github.com/sccn/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).
|
||||
[LabStreamingLayer](https://github.com/sccn/labstreaminglayer) is a tool for streaming or recording time-series data. It can be used to interface with [Matlab](https://github.com/sccn/labstreaminglayer/tree/master/LSL/liblsl-Matlab), [Python](https://github.com/sccn/labstreaminglayer/tree/master/LSL/liblsl-Python), [Unity](https://github.com/xfleckx/LSL4Unity), and many other programs.
|
||||
|
||||
For example, a VR display device running a Unity simulation may, using the [LSL4Unity](https://github.com/xfleckx/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](https://github.com/sccn/BCILAB) for analysis to trigger responses in the Unity display.
|
||||
To use LSL with the NodeJS SDK, go to our [labstreaminglayer example](https://github.com/OpenBCI/OpenBCI_NodeJS_Ganglion/tree/master/examples/labstreaminglayer), which contains code that is ready to start an LSL stream of OpenBCI data.
|
||||
|
||||
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](https://www.npmjs.com/package/python-shell)) to handoff the data to LSL for you, like so:
|
||||
Follow the directions in the [readme](https://github.com/OpenBCI/OpenBCI_NodeJS_Ganglion/blob/master/examples/labstreaminglayer/readme.md) to get started.
|
||||
|
||||
In your NodeJS code, before initializing/connecting to the OpenBCIBoard:
|
||||
```js
|
||||
// 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:
|
||||
```js
|
||||
st = sample.channelData.join(' ')
|
||||
//getTime returns milliseconds since midnight 1970/01/01
|
||||
var s = ''+ sample.timeStamp + ': '+ st
|
||||
lsloutlet.send(s)
|
||||
```
|
||||
|
||||
in LSLHandoff.py:
|
||||
```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.
|
||||
|
||||
## <a name="developing"></a> Developing:
|
||||
### <a name="developing-running"></a> Running:
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import json
|
||||
import sys
|
||||
import numpy as np
|
||||
import time
|
||||
import zmq
|
||||
from pylsl import StreamInfo, StreamOutlet
|
||||
|
||||
class Interface:
|
||||
def __init__(self, verbose=False):
|
||||
context = zmq.Context()
|
||||
self._socket = context.socket(zmq.PAIR)
|
||||
self._socket.connect("tcp://localhost:3004")
|
||||
|
||||
self.verbose = verbose
|
||||
|
||||
if self.verbose:
|
||||
print("Client Ready!")
|
||||
|
||||
# Send a quick message to tell node process we are up and running
|
||||
self.send(json.dumps({
|
||||
'action': 'started',
|
||||
'command': 'status',
|
||||
'message': time.time()*1000.0
|
||||
}))
|
||||
|
||||
def send(self, msg):
|
||||
"""
|
||||
Sends a message to TCP server
|
||||
:param msg: str
|
||||
A string to send to node TCP server, could be a JSON dumps...
|
||||
:return: None
|
||||
"""
|
||||
if self.verbose:
|
||||
print('<- out ' + msg)
|
||||
self._socket.send_string(msg)
|
||||
return
|
||||
|
||||
def recv(self):
|
||||
"""
|
||||
Checks the ZeroMQ for data
|
||||
:return: str
|
||||
String of data
|
||||
"""
|
||||
return self._socket.recv()
|
||||
|
||||
|
||||
def initializeOutlet(interface):
|
||||
"""
|
||||
Initializes and returns an LSL outlet
|
||||
:param interface: Interface
|
||||
the Python interface to communicate with node
|
||||
:return: StreamOutlet
|
||||
returns a labstreaminglayer StreamOutlet
|
||||
"""
|
||||
info = StreamInfo('OpenBCI_EEG', 'EEG', 4, 256, 'float32', 'openbci12345')
|
||||
outlet = StreamOutlet(info)
|
||||
return outlet
|
||||
|
||||
def main(argv):
|
||||
verbose = False
|
||||
# Create a new python interface.
|
||||
interface = Interface(verbose)
|
||||
# Create a new labstreaminglayer outlet
|
||||
outlet = initializeOutlet(interface);
|
||||
|
||||
# Stream incoming data to LSL
|
||||
while True:
|
||||
msg = interface.recv()
|
||||
try:
|
||||
dicty = json.loads(msg)
|
||||
message = dicty.get('message')
|
||||
data = message.get('channelDataCounts')
|
||||
timeStamp = message.get('timeStamp')
|
||||
outlet.push_sample(data,timeStamp)
|
||||
except BaseException as e:
|
||||
print(e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
@@ -0,0 +1,109 @@
|
||||
const Ganglion = require('openbci-ganglion').Ganglion;
|
||||
var portPub = 'tcp://127.0.0.1:3004';
|
||||
var zmq = require('zmq-prebuilt');
|
||||
var socket = zmq.socket('pair');
|
||||
var verbose = false;
|
||||
|
||||
let ganglion = new Ganglion({
|
||||
nobleAutoStart: true,
|
||||
sendCounts: true,
|
||||
verbose: true
|
||||
}, (error) => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
} else {
|
||||
if (verbose) {
|
||||
console.log('Ganglion initialize completed');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ganglion.once('ganglionFound', (peripheral) => {
|
||||
ganglion.searchStop();
|
||||
ganglion.on('sample', (sample) => {
|
||||
sendToPython({
|
||||
action: 'process',
|
||||
command: 'sample',
|
||||
message: sample
|
||||
});
|
||||
});
|
||||
ganglion.once('ready', () => {
|
||||
ganglion.streamStart();
|
||||
});
|
||||
ganglion.connect(peripheral);
|
||||
});
|
||||
|
||||
// ganglion.searchStart();
|
||||
function exitHandler (options, err) {
|
||||
if (options.cleanup) {
|
||||
if (verbose) console.log('clean');
|
||||
ganglion.manualDisconnect = true;
|
||||
ganglion.disconnect();
|
||||
ganglion.removeAllListeners('droppedPacket');
|
||||
ganglion.removeAllListeners('accelerometer');
|
||||
ganglion.removeAllListeners('sample');
|
||||
ganglion.removeAllListeners('message');
|
||||
ganglion.removeAllListeners('impedance');
|
||||
ganglion.removeAllListeners('close');
|
||||
ganglion.removeAllListeners('error');
|
||||
ganglion.removeAllListeners('ganglionFound');
|
||||
ganglion.removeAllListeners('ready');
|
||||
ganglion.destroyNoble();
|
||||
}
|
||||
if (err) console.log(err.stack);
|
||||
if (options.exit) {
|
||||
if (verbose) console.log('exit');
|
||||
if (ganglion.isSearching()) {
|
||||
ganglion.searchStop().catch(console.log);
|
||||
}
|
||||
ganglion.manualDisconnect = true;
|
||||
ganglion.disconnect(true).catch(console.log);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ZMQ
|
||||
socket.bind(portPub, function (err) {
|
||||
if (err) throw err;
|
||||
console.log(`bound to ${portPub}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Used to send a message to the Python process.
|
||||
* @param {Object} interProcessObject The standard inter-process object.
|
||||
* @return {None}
|
||||
*/
|
||||
var sendToPython = (interProcessObject, verbose) => {
|
||||
if (verbose) {
|
||||
console.log(`<- out ${JSON.stringify(interProcessObject)}`);
|
||||
}
|
||||
if (socket) {
|
||||
socket.send(JSON.stringify(interProcessObject));
|
||||
}
|
||||
};
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const rl = require('readline').createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
rl.on('SIGINT', function () {
|
||||
process.emit('SIGINT');
|
||||
});
|
||||
}
|
||||
|
||||
// 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,32 @@
|
||||
{
|
||||
"name": "labstreaminglayer",
|
||||
"version": "1.0.0",
|
||||
"description": "labstreaminglayer example",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "concurrently --kill-others \"python handoff.py\" \"node index.js\"",
|
||||
"start-node": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"python",
|
||||
"openbci",
|
||||
"node",
|
||||
"labstreaminglayer",
|
||||
"LSL"
|
||||
],
|
||||
"author": "AJ Keller",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"openbci-ganglion": "^0.4.3",
|
||||
"zmq-prebuilt": "^2.1.0",
|
||||
"noble": "1.7.0"
|
||||
},
|
||||
"devEngines": {
|
||||
"node": "<=6.x",
|
||||
"npm": ">=3.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^3.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
# OpenBCI Ganglion Node SDK to Lab Streaming Layer
|
||||
|
||||
## About
|
||||
|
||||
This code provides an example of how to stream OpenBCI Ganglion data through the [lab streaming layer](https://github.com/sccn/labstreaminglayer) using the NodeJS SDK.
|
||||
|
||||
Follow the steps in this README to start streaming. The code is ready to run as-is, but can be modified and extended to customize how you are sending your data. This is designed to be used with the **OpenBCI Ganglion** (for **Ganglion support**, see the [Ganglion Node SDK](https://github.com/OpenBCI/OpenBCI_NodeJS/tree/master/examples/labstreaminglayer)).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* [Python](https://www.python.org/downloads/)
|
||||
* [ZeroMQ](http://zeromq.org/bindings:python)
|
||||
|
||||
```py
|
||||
pip install pyzmq
|
||||
```
|
||||
* [Node.js LTS](https://nodejs.org/en/)
|
||||
* [Lab Streaming Layer](https://github.com/sccn/labstreaminglayer)
|
||||
|
||||
```py
|
||||
pip install pylsl
|
||||
```
|
||||
|
||||
|
||||
## Installation
|
||||
First, install Python dependencies:
|
||||
```bash
|
||||
python setup.py install
|
||||
```
|
||||
Next, install NodeJS dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
Note: depending on your computer settings, you may need to run as administrator or with `sudo`.
|
||||
|
||||
## Running
|
||||
```
|
||||
npm start
|
||||
```
|
||||
For running just the node, for example if you were running the python in a separate ide and debugging, it's useful.
|
||||
```python
|
||||
npm run start-node
|
||||
```
|
||||
Note: depending on your computer settings, you may need to run as administrator or with `sudo`.
|
||||
|
||||
## Writing Lab Streaming Layer Code
|
||||
If you would like to use lab streaming layer in a custom OpenBCI NodeJS application, you must include an instance of the OpenBCI NodeJS library and an instance of a Python interface. A basic example is shown below:
|
||||
|
||||
index.js
|
||||
```js
|
||||
const Ganglion = require('openbci-ganglion').Ganglion;
|
||||
var portPub = 'tcp://127.0.0.1:3004';
|
||||
var zmq = require('zmq-prebuilt');
|
||||
var socket = zmq.socket('pair');
|
||||
|
||||
let ganglion = new Ganglion();
|
||||
|
||||
socket.bind(portPub)
|
||||
|
||||
ganglion.once('ganglionFound', (peripheral) => {
|
||||
ganglion.searchStop();
|
||||
ganglion.on('sample', (sample) => {
|
||||
socket.send(JSON.stringify({message: sample}))
|
||||
});
|
||||
ganglion.once('ready', () => {
|
||||
ganglion.streamStart();
|
||||
});
|
||||
ganglion.connect(peripheral);
|
||||
});
|
||||
|
||||
// ZMQ
|
||||
|
||||
|
||||
/* Insert additional exit handlers and cleanup below*/
|
||||
```
|
||||
|
||||
handoff.py
|
||||
```python
|
||||
import json
|
||||
import zmq
|
||||
from pylsl import StreamInfo, StreamOutlet
|
||||
|
||||
# Create ZMQ socket
|
||||
context = zmq.Context()
|
||||
_socket = context.socket(zmq.PAIR)
|
||||
_socket.connect("tcp://localhost:3004")
|
||||
|
||||
# Create a new labstreaminglayer outlet
|
||||
numChans = 4;
|
||||
sampleRate = 256;
|
||||
info = StreamInfo('OpenBCI_EEG', 'EEG', numChans, sampleRate, 'float32', 'openbci_12345')
|
||||
outlet = StreamOutlet(info)
|
||||
# Stream incoming data to LSL
|
||||
while True:
|
||||
msg = _socket.recv()
|
||||
try:
|
||||
dicty = json.loads(msg)
|
||||
message = dicty.get('message')
|
||||
data = message.get('channelDataCounts')
|
||||
timeStamp = message.get('timeStamp')
|
||||
outlet.push_sample(data,timeStamp)
|
||||
except BaseException as e:
|
||||
print(e)
|
||||
```
|
||||
|
||||
## Contributing
|
||||
Please PR if you have code to contribute!
|
||||
@@ -0,0 +1,12 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(name='openbci-node-labstreaminglayer',
|
||||
version='0.0.1',
|
||||
description='Labstreaminglayer with NodeJS',
|
||||
url='',
|
||||
author='AJ Keller',
|
||||
author_email='pushtheworldllc@gmail.com',
|
||||
license='MIT',
|
||||
packages=find_packages(),
|
||||
install_requires=['numpy', 'pyzmq','pylsl'],
|
||||
zip_safe=False)
|
||||
Referência em uma Nova Issue
Bloquear um usuário