BCI and Go

TL;DR

One can now use EEG-signals in Go, using a Go client for the buffer_bci framework.

Full version

Go has been around for quite some time now, but hasn’t been used a lot in academic circles. But as more and more packages are being developed for Go, it has become a real candidate.

Part of the education I receive, is related to Brain-Computer Interfacing. This means that we’re analyzing brain signals (i.e. through EEG), and trying to ignore all non-brain signals (i.e. muscle movement). Worldwide, the buffer_bci framework is often being used to connect different hardware devices to the computer. It supports TMSi Mobita, Emotiv Epoc, Biosemi Active 2, Interaxon Muse and possibly some more.

The architecture of the buffer_bci is quite simple: one central server (“Buffer”), and multiple clients that send or receive data. One simply starts the server, starts the appropriate interface, and creates a TCP connection from his/her own application to the server (in order to use the data). This communication is based Fieldtrip Buffer.

Client-frameworks already exist for C, C#, Java, Python and Matlab/Octave. However, a framework for Go was nowhere to be found.

The Framework

In order to have a fully functioning client, it needs to be able to send and receive data. Quite some aspects are shared between the two, as they share the same protocol.

Receiving Data

There are three general functions the API offers:

  • Receiving Header data: you’ll receive the number of channels the buffer offer (i.e. number of sensors on the EEG cap), the number of samples (moments that have been captured), the number of events, the frequency at which new samples are offered, and some information about the samples (i.e. data type).
  • Receiving Samples: you can request (either all, or a subset of) the samples available. Here you’ll also receive the number of channels, samples and data type.
  • Receiving Events: you can request (either all, or a subset of) the events available. These too are being sent to the buffer, and one can retrieve information as to when this event happened.

Sending Data

The same thing applies here. Whatever the client has to receive, the client also has to be able to send — according to the protocol specification.

However, providing data to the buffer is not required in any case. There is no point at which you have to be able to send data — unless you want to create a driver / data generation algorithm in Go.

Using the Client

In my attempt to create a client (source code) (MIT License), most of it actually seems to work. Before actually executing any Go code, we need to start the server and allow for some data to be put inside the buffer.

# You can skip this if you haven't done so already
$ git clone https://github.com/jadref/buffer_bci
# This will start the buffer server.
$ ./buffer_bci/dataAcq/startBuffer.sh
# This will start a signal proxy, which will send random data.
# Instead, you can also use startMobita.sh,
# (given you have a Mobita).
$ ./buffer_bci/dataAcq/startSignalProxy.sh

Both of them are blocking, so make sure to either run them in the background, or open two terminal windows.

Using the client in your own application

As with all go packages, go get it first:

$ go get github.com/EtienneBruines/gobci

Let’s set up a connection to the (running) buffer server.

conn, err := bufferbci.Connect("localhost:1972")

This is enough to connect to the buffer server, using the default hostname and port.

header, err := conn.GetHeader()

This will get us the information we need to correctly process the data.

samples, err := conn.GetData(0, header.NSamples / 10)

Notice that we’re only using 1/10th of the samples available, to demonstrate the usage of this function. We now have a [][]float64 of size header.NSamples by header.NChannels.

Viewing the Samples

Now that we’ve received the samples, we want to make sure that we’re not just receiving all-zeroes.

fmt.Println(samples)

Simple enough, right? Given that possibly thousands of samples are available, this will not give you any useful insight in the data. You now only know your data does not consist of just zeroes.

Using gonum/plot

Visualizing these streams of data is usually done through plots — at least in the academic world. You can easily identify possible outliers, detect measurement flaws, etc.

In order to use the plotter, we have to figure out how we want to format the data. We’re using lines in this example. In order to do so, we must create a type which implements XYer. This sounds complicated, but it simply has to provide Len() and XY(int), so the plotter knows how much points it has to draw, and what their X and Y values are.

type channelXYer struct {
Values []float64
}
func (c channelXYer) Len() int {
return len(c.Values)
}

func (c channelXYer) XY(index int) (x, y float64) {
return float64(index), c.Values[index]
}

You may have noticed, that we’re using the requested index as the X-value. This is because we have no timestamps available for the data, but we do know that index i+1 was measured after index i. I have called it channelXYer, because it holds values per channel.

As you may notice, when you’re saving the plot as svg, your file may get quite large (mine was 80MB). This is because it holds a lot of different data points in the plot. You may want to decide to plot only part of it (i.e. the first fifty samples), or combine samples in the channelXYer. The latter, can be done in this way:

const avgCount = 20
func (c channelXYer) Len() int {
return len(c.Values) / avgCount
}

func (c channelXYer) XY(i int) (x, y float64) {
return float64(i/avgCount), average(c.Values[avgCount*i:avgCount*(i+1)])
}
func average(xs []float64) float64 {
total := 0.0
for _, v := range xs {
total += v
}
return total / float64(len(xs))
}

Now it only tells the plotter it has 1/20th of the data points it actually has, and averages over 20 to make sure you don’t randomly grab a bad data point. Note that when restarting the buffer server, you’ll have a small number of samples again, and then you may not want to

What does it look like?

If we compare two randomly created channels, created by the SignalProxy, it looks like the diagram below.

If we restart our buffer server, we end up with entirely different samples, thus resulting in a different diagram.

Adding a third channel, is easy, but might result in a weird plot.

We might assume that the third channel (blue), is broken — as it only looks like it’s zero all the time. However, this third channel had values between -1 and 1, which results in a strange-looking plot. We can plot this channel to be sure (below in red).

The complete source code of this, is also available in the examples directory at the repository.

Discussion & Future work

Imagine the possibilities when having this data available in Go (live!). Some graphics engines are becoming more mature (engi, Azul), and with the capability to plug-in external hardware to detect brain signals, the first BCI-controlled game written in Go is more than a possibility.

One thing that I noticed is missing in Go, is a package which allows for plotting real-time. The “easiest” way to do this right now, is to set-up a webserver in Go, and stream it to a webpage with javascript.

Perhaps there are some design-choices in the Go client, that you would have done differently? Maybe returning the samples as a (builtin) channel, or the samples as interface{}’s instead of converting everything to float64?

Share your thoughts :-).