sccn / lsl_archived

Multi-modal time-synched data transmission over local network
242 stars 134 forks source link

liblsl wrapper for JavaScript #168

Open gabrielibagon opened 7 years ago

gabrielibagon commented 7 years ago

Have there been any attempts at generating an LSL library for JavaScript? I'm interested in using LSL with OpenBCI's NodeJS driver.

I've looked at the "AUTOGENERATE HOWTO", but before going through the trouble, I'd like to know if some implementation already exists, or if there are any details about LSL and JavaScript I should know about before attempting to generate one.

mgrivich commented 7 years ago

I've done a fair amount of work with Javascript, LSL, and node.js, so I know the issues involved. I have not made it public, because I only implemented the functions I needed, not some sort of general tool. Could you describe more precisely the work flow you are going for? Data generated here, converted to LSL there, shown on web page here, etc. Then I could say if your proposed workflow is possible and reasonable.

On Jan 20, 2017 9:41 PM, "Gabriel Ibagon" notifications@github.com wrote:

Have there been any attempts at generating an LSL library for Javascript? I'm interested in using LSL with OpenBCI's NodeJS driver.

I've looked at the "AUTOGENERATE HOWTO", but before going through the trouble, I'd like to know if some implementation already exists, or if there are any issues with LSL and JavaScript I should know about before attempting to generate one.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/sccn/labstreaminglayer/issues/168, or mute the thread https://github.com/notifications/unsubscribe-auth/AFC33cek9DULUH9xaK0etHs_U8We8gWEks5rUZqdgaJpZM4Lp-Ot .

gabrielibagon commented 7 years ago

Thanks for the response. I'm using the OpenBCI NodeJS module to stream OpenBCI data from the serial port. Here is the link to package: https://github.com/OpenBCI/OpenBCI_NodeJS . From there, I would like to send EEG data + markers to one of various programs, either for recording or real time processing (LabRecorder, NeuroPype, BCILAB, etc). I usually do all of this in Python (serial port input and LSL streaming), but I'm looking to migrate a project to NodeJS (since the OpenBCI node package is well-tested and reliable).

The current suggested technique involves calling the python shell from node to use pylsl, using the "python-shell" package (see: https://github.com/OpenBCI/OpenBCI_NodeJS#interfacing-with-other-tools), but I find this to be an awkward workaround. It'd be nice if there was a native way to incorporate LSL output capabilities as a general tool in a program that's using the openbci-sdk package.

mgrivich commented 7 years ago

OpenBCI provides a direct interface to python https://github.com/OpenBCI/OpenBCI_Python which is referenced from openbci.com. Is there any problem with using that? Using node will add complexity without much value unless you are looking to include web type applications in your project.

On 1/23/2017 2:37 PM, Gabriel Ibagon wrote:

Thanks for the response. I'm using the OpenBCI NodeJS module to stream OpenBCI data from the serial port. Here is the link to package: https://github.com/OpenBCI/OpenBCI_NodeJS . From there, I would like to send EEG data + markers to one of various programs, either for recording or real time processing (LabRecorder, NeuroPype, BCILAB, etc). I usually do all of this in Python (serial port input and LSL streaming), but I'm looking to migrate a project to NodeJS (since the OpenBCI node package is well-tested and reliable).

The current suggested technique involves calling the python shell from node to use pylsl, using the "python-shell" package (see: https://github.com/OpenBCI/OpenBCI_NodeJS#interfacing-with-other-tools), but I find this to be an awkward workaround. It'd be nice if there was a native way to incorporate LSL output capabilities in a program that's using the openbci-sdk package.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/sccn/labstreaminglayer/issues/168#issuecomment-274640388, or mute the thread https://github.com/notifications/unsubscribe-auth/AFC33Z6rUedATRTWi9jfvORmwqchAGfzks5rVSuigaJpZM4Lp-Ot.

andrewjaykeller commented 7 years ago

I would love to see LSL wrapped in javascript!

gabrielibagon commented 7 years ago

@mgrivich Right - I've used the Python library before, but the OpenBCI_NodeJS interface has some additional functionality not included in the python interface. My motivation was to provide a general tool that could be used with the OpenBCI_NodeJS library for my project, and for general use for OpenBCI users using NodeJS.

Is it possible to create an JS wrapper for this? Or would it not make much sense to create a general JS implementation, because of issues with using LSL+JS in other environments such as web applications, etc?

mgrivich commented 7 years ago

Okay. I'll describe approximately what you have to do, and leave it up to you as to whether or not it is worth it. I will include a significant amount of sample code that in theory could be turned into a full-fledged node.js wrapper for LSL, but I do not have the time or inclination to do that at this time. The stream types it supports are fine, but it only supports the types I've needed so far.

Node.js has the ability to accept C++ addons (https://nodejs.org/api/addons.html) but the syntax and methodology can be obscure. My addon is below.

lsl_node_plugin.cpp:

#include <node.h>
#include "../cpp/lsl_cpp.h"
#include <cmath>
#include <memory>
#include <vector>
#include <iostream>
#include <map>
#include <unistd.h>

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
using v8::Number;
using v8::Exception;

using namespace lsl;
using namespace std;

std::map<std::string, std::shared_ptr<stream_inlet> > inlets;
std::map<std::string, std::shared_ptr<stream_inlet> > string_inlets;
std::map<std::string, std::shared_ptr<stream_outlet> > outlets;
std::map<std::string, float*> datas;
std::map<std::string, double*> time_stamps;

void open_in_stream(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = args.GetIsolate();
    if(args.Length() != 3) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong number of arguments.")));
        return;
    }

    if(!args[0]->IsString()) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "First argument should be stream name.")));   
        return; 
    }

    string stream_name(*v8::String::Utf8Value(args[0]->ToString()));

    Local<v8::Float32Array> array = args[1].As<v8::Float32Array>();
    Local<v8::ArrayBuffer> buffer = array->Buffer();
    v8::ArrayBuffer::Contents contents = buffer->Externalize();
    datas[stream_name] = static_cast<float*>(contents.Data()); 

    array = args[2].As<v8::Float32Array>();
    buffer = array->Buffer();
    contents = buffer->Externalize();   
    time_stamps[stream_name] = static_cast<double*>(contents.Data());

    vector<stream_info> streams = resolve_stream("name", stream_name);
    if(streams.size() == 0) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Unable to open stream")));
        return; 
    }
    inlets[stream_name] = make_shared<stream_inlet>(streams[0], 10);

    cout << "Opened stream: " << stream_name << endl;

}

void open_in_string_stream(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = args.GetIsolate();

    if(!args[0]->IsString()) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "First argument should be stream name.")));   
        return; 
    }

    string stream_name(*v8::String::Utf8Value(args[0]->ToString()));

    vector<stream_info> streams = resolve_stream("name", stream_name);
    if(streams.size() == 0) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Unable to open stream")));
        return; 
    }

    string_inlets[stream_name] = make_shared<stream_inlet>(streams[0]);

    cout << "Opened stream: " << stream_name << endl;

}

void open_out_stream(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = args.GetIsolate();
    if(args.Length() != 2) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong number of arguments.")));
        return;
    }

    if(!args[0]->IsString()) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Arguments should be strings.")));    
        return; 
    }

    string stream_name(*v8::String::Utf8Value(args[0]->ToString()));

    Local<v8::Float32Array> array = args[1].As<v8::Float32Array>();
    Local<v8::ArrayBuffer> buffer = array->Buffer();
    v8::ArrayBuffer::Contents contents = buffer->Externalize();
    datas[stream_name] = static_cast<float*>(contents.Data()); 

    //note that array->Length() returns value in Float32s
    stream_info info(stream_name, "Control", array->Length(), IRREGULAR_RATE, lsl::cf_float32, "76df09a0-c382-4c8b-bfdd-99b6768c0fd2");
    outlets[stream_name] = make_shared<stream_outlet>(info);    

    cout << "Opened stream: " << stream_name << endl;

}

void sampling_rate(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = args.GetIsolate();
    if(args.Length() != 1) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong number of arguments.")));
        return;
    }

    if(!args[0]->IsString()) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Arguments should be strings.")));    
        return; 
    }

    string stream_name(*v8::String::Utf8Value(args[0]->ToString()));
    Local<Number> num = Number::New(isolate, inlets[stream_name]->info().nominal_srate());
    args.GetReturnValue().Set(num);
}

void channel_count(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = args.GetIsolate();
    if(args.Length() != 1) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong number of arguments.")));
        return;
    }

    if(!args[0]->IsString()) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Arguments should be strings.")));    
        return; 
    }

    string stream_name(*v8::String::Utf8Value(args[0]->ToString()));

    Local<Number> num = Number::New(isolate, inlets[stream_name]->info().channel_count());

    args.GetReturnValue().Set(num);

}

void close_stream(const FunctionCallbackInfo<Value>& args) {

    Isolate* isolate = args.GetIsolate();
    if(args.Length() != 1) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong number of arguments.")));
        return;
    }

    if(!args[0]->IsString()) {
        isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Arguments should be strings.")));    
        return; 
    }

    string stream_name(*v8::String::Utf8Value(args[0]->ToString()));
    cout << "Closing stream: " << stream_name << endl;

    datas.erase(stream_name);
    time_stamps.erase(stream_name);
    inlets.erase(stream_name);
    outlets.erase(stream_name);

}

void pull_string_sample(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = args.GetIsolate();
    string stream_name(*v8::String::Utf8Value(args[0]->ToString()));
    std::shared_ptr<stream_inlet> inlet = string_inlets[stream_name];
    string sample;
    double time_stamp = inlet->pull_sample(&sample, 1, 0.0);
    if(time_stamp != 0) {
        args.GetReturnValue().Set(String::NewFromUtf8(isolate, sample.c_str()));
    } else {
        args.GetReturnValue().Set(String::NewFromUtf8(isolate, ""));
    }
}

void pull_samples(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = args.GetIsolate();
//  if(args.Length() != 2) {
//      isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong number of arguments.")));
//      return;
//  }

//  if(!args[0]->IsString() || !args[1]->IsString()) {
//      isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Arguments should be strings.")));    
//      return; 
//  }

    string stream_name(*v8::String::Utf8Value(args[0]->ToString()));

    int nSamps = args[1]->ToInt32()->Value();

    double time_stamp = 0.0;
    float* data = datas[stream_name];
    double* time = time_stamps[stream_name];
    std::shared_ptr<stream_inlet> inlet = inlets[stream_name];
    int nChans = inlets[stream_name]->info().channel_count();
    int i = 0;
    static double old_time_stamp = 0;
    for(; i<nSamps; ) {
        time_stamp = inlet->pull_sample(data+i*nChans, nChans, 0.0);
        time[i] = time_stamp;
        if(time_stamp == 0) {
            break;  
        } else if(time_stamp < old_time_stamp) {
            printf("Samples went backwards: %g\n", time_stamp - old_time_stamp);
        }
        i++;
        old_time_stamp = time_stamp;
    }   

    Local<Number> num = Number::New(isolate, i);

    args.GetReturnValue().Set(num);
}

void pull_sample(const FunctionCallbackInfo<Value>& args) {
    Isolate* isolate = args.GetIsolate();
    string stream_name(*v8::String::Utf8Value(args[0]->ToString()));

    double time_stamp = 0.0;
    float* data = datas[stream_name];
    std::shared_ptr<stream_inlet> inlet = inlets[stream_name];

    time_stamp = inlet->pull_sample(data, inlets[stream_name]->info().channel_count(), 0.0);

    Local<Number> num = Number::New(isolate, time_stamp);

    args.GetReturnValue().Set(num);
}

void push_sample(const FunctionCallbackInfo<Value>& args) {
//  Isolate* isolate = args.GetIsolate();
//  if(args.Length() != 2) {
//      isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Wrong number of arguments.")));
//      return;
//  }

//  if(!args[0]->IsString() || !args[1]->IsString()) {
//      isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "Arguments should be strings.")));    
//      return; 
//  }

    string stream_name(*v8::String::Utf8Value(args[0]->ToString()));

    float* data = datas[stream_name];
    std::shared_ptr<stream_outlet> outlet = outlets[stream_name];
    outlet->push_sample(data);

}

void init(Local<Object> exports) {
  NODE_SET_METHOD(exports, "openInStream", open_in_stream);
  NODE_SET_METHOD(exports, "openInStringStream", open_in_string_stream);
  NODE_SET_METHOD(exports, "openOutStream", open_out_stream);
  NODE_SET_METHOD(exports, "closeStream", close_stream);
  NODE_SET_METHOD(exports, "pullSamples", pull_samples);
  NODE_SET_METHOD(exports, "pullSample", pull_sample);
  NODE_SET_METHOD(exports, "pullStringSample", pull_string_sample);
  NODE_SET_METHOD(exports, "pushSample", push_sample);
  NODE_SET_METHOD(exports, "samplingRate", sampling_rate);
  NODE_SET_METHOD(exports, "channelCount", channel_count);
}

NODE_MODULE(addon, init)

Compile with node-gyp build and the following binding.gyp:

{
  "targets": [
    {
      "target_name": "lsl",
      "include_dirs": ["../cpp"],
      "sources": ["lsl_node_plugin.cpp"],
      "libraries": ["liblsl.so"],
      "cflags_cc!": ["-fno-exceptions"]
    }
  ]
}

Of course, you'll have to have your node environment set up, and compile liblsl.so for your system. I used the code.blocks project from https://github.com/sccn/labstreaminglayer/tree/master/LSL/liblsl/project/code.blocks to compile it here. Below is index.js. What this does is pull and push LSL streams from the C++ layer, as well as push and pull the streams to a web page using socket.io (http://socket.io). For your application, you will need to select (or implement) the streams of interest to you and remove the socket.io stuff. Note that node.js is almost completely asynchronous. If you are not used to it, you will probably need to spend some time with some simpler examples and documentation first.

const addon = require('./build/Release/lsl');
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('./socket.io')(server);
var port = process.env.PORT || 3000;

server.listen(port, function () {
  console.log('Server listening at port %d', port);
});

app.use(express.static(__dirname + '/public'));

addon.openInStringStream('Messages');

//input multi-sample float32 data
var maxSamples = 1000;
var bufArr = new ArrayBuffer(4*4*maxSamples); //float32 * 4 channels * maxSamples
var samplesArray = new Float32Array(bufArr);
var timestampsBuf = new ArrayBuffer(8*1*maxSamples); //float64 * maxSamples 
var timestampsArray = new Float64Array(timestampsBuf); 
addon.openInStream('RawData', samplesArray, timestampsArray);

//input single-sample float32 data
var latenciesBuf = new ArrayBuffer(4*4); //float32 * 4 channels 
var latenciesArray = new Float32Array(latenciesBuf);
var latenciesTimestampsBuf = new ArrayBuffer(8); //float64
var latenciesTimestampsArray = new Float64Array(latenciesTimestampsBuf);
addon.openInStream('Latencies', latenciesArray, latenciesTimestampsArray);

var controlString = "";

//output single-sample float32 data
var controlBuf = new ArrayBuffer(4*8); //float32 * 8 values
var controlArray = new Float32Array(controlBuf);
addon.openOutStream('Control', controlArray);

var timeout;

var samplingRate = addon.samplingRate('RawData');

//pull all streams from from C++, push to webpage
function pull() {

  var timestamp = addon.pullSample('Latencies');
  if(timestamp != 0) {
     io.sockets.emit('latency', {latency: latenciesArray, timestamp: timestamp});
  }

  var samplesRead = addon.pullSamples('RawData', maxSamples);

   if(samplesRead > 0 ) {
    io.sockets.emit('sample', {samples: samplesArray, timestamps: timestampsArray, length: samplesRead});
  }  

  var message = addon.pullStringSample('Messages');
  if(message.length > 0) {
    console.log(message);
    io.sockets.emit('message', message);
  }

  timeout = setTimeout(pull, 10); //call pull in 10 ms

}

pull();

//take controls change from webpage, push to C++
io.on('connection', function (socket) {

  socket.on('controls', function(msg) {
    for(var i=0; i<8; i++) {
        controlArray[i] = msg.controls[i];
    }

    addon.pushSample('Control', 8);

  });

});

process.on('SIGINT', function() {
  clearTimeout(timeout);
  addon.closeStream('Control');
  addon.closeStream('RawData');
  process.exit();   
});

The server can be launched with node . as normal.

gabrielibagon commented 7 years ago

Thanks a lot for your input, I appreciate it. I will look into this approach and see how it can be incorporated into what I'm trying to do.

alexcastillo commented 7 years ago

Hey @gabrielibagon

Any updates on this? I'm also interested.

gabrielibagon commented 7 years ago

@alexcastillo I didn't end up using this. I sent OpenBCI data from node to python/pylsl, like described here: https://github.com/OpenBCI/OpenBCI_NodeJS/tree/master/examples/labstreaminglayer. That method fit what I needed at the time, though I would still be interested in figuring out how to use LSL natively in javascript...

alexcastillo commented 7 years ago

Thanks for the update. We are talking about a JavaScript wrapper in this thread: https://github.com/NeuroJS/eeg-stream-data-model/issues/1#issuecomment-309243508

urish commented 7 years ago

Partial LSL implementation for node.js (using ffi): lsl.js

Usage example: Streaming data from Muse 2016 to LSL

jdpigeon commented 6 years ago

I'm interested in moving forward on a JavaScript-based LSL wrapper. Does anyone have any updates or thoughts on this process?

The most straightforward way seems to be extending @urish's repo by bridging more liblsl functions using node-ffi.

However, with all the growing interest in cloud-based BCI tech I'm thinking it might be more portable and future-proof to use web assembly or N-API. Is that bonkers or should we do it? :)

dmedine commented 6 years ago

I'm not a web-tech person, but it looks like this lsl wrapper (https://github.com/urish/node-lsl) would be very simple to extend.

As far as webassembly or N-API is concerned, I have little experience. I kinda feel like someone may have done this already but that it isn't opensource, but I really have no idea. I do know that the N-API is super hard to deal with.

On 12.09.2018 23:25, Dano Morrison wrote:

I'm interested in moving forward on a JavaScript-based LSL wrapper. Does anyone have any updates or thoughts on this process?

The most straightforward way seems to be extending @urish https://github.com/urish's repo by bridging more liblsl functions using node-ffi.

However, with all the growing interest in cloud-based BCI tech I'm thinking it might be more portable and future-proof to use web assembly or N-API. Is that bonkers or should we do it? :)

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/sccn/labstreaminglayer/issues/168#issuecomment-420803208, or mute the thread https://github.com/notifications/unsubscribe-auth/ADch7mAxwYGqYK-moQDhPaMisNGYLJAjks5uaXusgaJpZM4Lp-Ot.

jdpigeon commented 6 years ago

I've made a lot of progress on node-lsl today. Successfully wrapped resolve_byprop, create_inlet, and pull_chunk and connected it to an lsl stream. https://github.com/makebrainwaves/node-lsl

I see there's a lot of nice abstraction that the python and C# wrappers added to make LSL easier to work with. It might take a little bit longer to get that implemented for node-lsl. I'd say for anyone who's willing to dive into the liblsl source documentation to see how things work, we're almost there for being able to both read and write LSL in JS.