How to create LSL bridges

Follow this tutorial to learn what are MEDUSA© LSL bridges and how to create them.


For MEDUSA© Platform v2023

If you have any questions that are beyond the scope of this help file, please feel free to ask for help in the forum or contact with us.

The Lab Streaming Layer (LSL) is an open-source networked middleware ecosystem that simplifies the process of collecting, analyzing, and sharing data streams. It is a powerful and versatile tool for stakeholders who need to stream, receive, synchronize, and record data from diverse sensor hardware.

LSL offers the following benefits:

  • Designed to stream data in real-time, allowing researchers to analyze and visualize the data as it is being collected
  • Built-in synchronization functions, allowing researchers to easily combine data from multiple sources and ensure that the data is accurately aligned in time
  • Flat learning curve, thanks to its simple, interoperable, standardized API that connects data sources to data consumers without needing to have a deep understanding of networking or data formats
  • Compatibility with a wide range of hardware and software platforms.
  • Support in several programming languages. LSL has been developed in C to maximize performance, but it includes bindings for Python, C#, Java, MATLAB, and more.

To get started with LSL, check out the introduction and quick start pages of the official documentation. This page also includes links to tutorials, examples, and other helpful resources that will help you learn more about LSL and how to use it to stream, receive, synchronize, and record data from a wide range of sensor hardware.

From MEDUSA, we adopted LSL for its extended use (especially in EEG). Instead of reinventing the wheel by creating a new communication protocol, we decided to wrap LSL in a nice set of high-level functions that provide additional functionalities to meet our goal: to provide a powerful, easy-to-use, flexible environment for researchers. However, in order to connect your recording hardware to MEDUSA, you need a LSL bridge.

In the following sections of this tutorial you will learn what are LSL bridges and how to create them.


What's an LSL bridge?

The signal acquisition process in MEDUSA© Platform has three main components:

  • The data source, which could be any device or process that streams data. For example, an EEG headset, an ECG recorder, a camera or a microphone. But also a computer process generating random samples.
  • The consumers, which are all those processes within MEDUSA© Platform that receive data through LSL, such as real-time charts or apps.
  • The LSL bridge, which is a script or application that connect the data source to the consumers through the LSL protocol. Therefore, a LSL bridge is a program that receives the data stream from the device using the hardware's firmware and sends it through LSL.

First steps

The first thing to do when building a LSL bridge is to check if you actually need to do the work. Check these resources first to find out if your device already has a LSL bridge available:

Once you are confident that there is no available LSL bridge for the device that you want to connect to MEDUSA© Platform, you need to answer the follwing questions:

  • Does your device support access to the recorded data in real time? Usually, research grade devices include APIs to receive the data. However, there are many devices that do not support this possibility. If you have doubts, ask your manufacturer.
  • Do you have the programming skills and the time to create the LSL bridge? Although we will guide you with this tutorial, some programming experience is required to understand the provided examples, manage the API of your device, or solve any problem that might arise in the process. Moreover, developing a full LSL bridge application could be a time-consuming task. If you don't have the skill or the time, nothing is lost! Contact us and we'll help you out.

IMPORTANT: if your device do not support access in real time to the recorded data, you can't build a LSL bridge.

If you still want to create your LSL bridge, let's get to it!


Basic script

In this section you will see the basics for the implementation of a LSL bridge. We will use a functional example that generates random samples and sends them through LSL.

The example has been developed in Python to facilitate the explanation of the main concepts. Download this script from the MEDUSA© Tutorials repository. You can find examples in other programming languages here. Particularly, if you are interested in a C++ example which is analogous to the one we are doing in this tutorial, check here. Let's get to it!

First, import the necessary packages. We will use time and threading from the standard Python library. Additionally, we will need to import pylsl and numpy.

                
import time, threading
import pylsl
import numpy as np
                
              

Then we define the parameters of the script. You can change the values of these variables to see the effect on MEDUSA© Platform. In this case we are going to create a LSL stream called SignalGenerator with a unique source id. We set the stream type to EEG, since it's one of the default types supported by MEDUSA along with MEG and EMG. Other signals can also be recorded (e.g., fNIRS, ECG), but there are no special options for these formats yet. The labels of the channels are set to 0, 1, 2, 3, etc. As we are using the EEG stream type, if you change the labels to the international system (e.g., Fz, Cz, etc), the coordinates are saved along the signal, allowing advanced options for offline processing with MEDUSA© Kernel (e.g., plot topographic plots).

                
#%% PARAMETERS

# LSL parameters
stream_name = 'SignalGenerator'
stream_type = 'EEG'
source_id = '433b4d0a-78ae-11ed-a1eb-0242ac120002'
chunk_size = 16
format = 'float32'
n_cha = 8
l_cha = [str(i) for i in range(8)]
units = 'uV'
manufacturer = 'MEDUSA'
sample_rate = 250

# Random signal parameters
mean = 0
std = 1
                
              

Now we instantiate the LSL StreamOutlet, which is the class responsible for the data streaming. First, we will need to create a StreamInfo class and include some extra information in the metadata of this class. Especially important is the channel information. In this regard, MEDUSA expects the format as is defined below. Other formats are also supported, but some options might not be available in MEDUSA© Kernel.

                
#%% CREATE LSL OUTLET

# Create the stream info
lsl_info = pylsl.StreamInfo(name=stream_name,
                            type=stream_type,
                            channel_count=n_cha,
                            nominal_srate=sample_rate,
                            channel_format=format,
                            source_id=source_id)

# Modify description to include additional information (e.g., manufacturer)
lsl_info.desc().append_child_value("manufacturer", manufacturer)

# Append channel information. By default, MEDUSA© Platform expects this
# information in the "channels" section of the LSL stream description
channels = lsl_info.desc().append_child("channels")
for l in l_cha:
    channels.append_child("channel") \
        .append_child_value("label", l) \
        .append_child_value("units", units) \
        .append_child_value("type", stream_type)

# Create LSL outlet
lsl_outlet = pylsl.StreamOutlet(info=lsl_info,
                                chunk_size=chunk_size,
                                max_buffered=100*chunk_size)
                
              

The next step is to define the function that will generate the data and sent it through LSL. In this example, we will generate random data on demand. However, if you are trying to connect your device, this is the place where you will have to use the hardware API to get the signal in order to send it immediately with the StreamOutlet.push_sample and StreamOutlet.push_chunk functions. In this case, you will have to remove the piece of code that waits to generate the next chunk, as this will wait will be given by the hardware itself: time.sleep(chunk_size / sample_rate)

                
def send_data():
    """Function that generates random data and sends it through LSL
    """
    while io_run.is_set():
        try:
            if lsl_outlet is not None:
                # Get data
                # --------------------------------------------------------------
                # TODO: Get the data from an actual device using its API
                # For this tutorial, we will generate random data
                sample = std * np.random.randn(chunk_size, n_cha) + mean
                sample = sample.tolist()
                # --------------------------------------------------------------
                # Get the timestamp of the chunk
                timestamp = pylsl.local_clock()
                # Send the chunk through LSL
                lsl_outlet.push_chunk(sample, timestamp)
                # Wait for the next chunk. This timer is not particularly 
                # accurate
                time.sleep(chunk_size / sample_rate)
        except Exception as e:
            raise e
                
              

Finally, we run the previous function in a separated thread to avoid blocking the main thread, which is paused until the user presses the enter key. Afterwards, the thread stops and the program finishes. Once you run this piece of code, the data stream that we just created will be available MEDUSA© Platform, where it can be added to the working LSL streams.

                
#%% STREAM DATA

# Run data source in other thread so the execution can be stopped on demand
io_run = threading.Event()
io_run.set()

lsl_thread = threading.Thread(
    name='SignalGeneratorThread',
    target=send_data
)
lsl_thread.start()

# Streaming data...
print('SignalGenerator is streaming data. Open MEDUSA and check that the '
      'stream is received correctly')

#%% STOP EXECUTION AND CLEAR

# Pause the main thread until the user presses enter
input("Press enter to finish...")

# Stop the thread and join
io_run.clear()
lsl_thread.join()

# Final message
print('SignalGenerator finished successfully')

                
              

Congratulations! You've created your first LSL bridge. Of course, this is a basic example to understand the main concepts, but much more can be done. Check the next section and the LSL documentation for more information.


Advanced topics

Build an LSL bridge with GUI

Now that you have understood the basics of LSL, you can start building a functional application for your LSL bridge. The previous example requires some coding experience to open the file and change the parameters. Additionally, it requires setting up a Python/C++ environment to run the script.

It would be more convenient to have a final executable with a graphic user interface (GUI) to run the program, right? This brings options such as pause and resume the streaming, easy configuration of the number of channels and labels, change the sample rate, etc.

To do that, we recommend to use PyQt to build the GUI and PyInstaller to compile the program and create a standalone executable (only for Windows). If you feel more comfortable with C/C++, check the official LSL repository to find examples.

To explain these topics is out of the scope of this tutorial, but you can use our open-source implementation of the signal generator as a complete and functional example. Additionally, we recommend checking the official documentations of Qt and Pyinstaller.