Episode 2: Sending LSL Streams

In this episode, you learn how to send EEG data through an LSL (Lab Streaming Layer) interface using g.Pype.

Note

This page is still under development. Until we have the step-by-step instructions ready, please refer to the code example below.

File example_basic_lsl_send.py (send LSL stream)View file on GitHub

 1"""
 2Basic LSL Send Example - Lab Streaming Layer Data Transmission
 3
 4This example demonstrates how to stream data over the network using Lab
 5Streaming Layer (LSL) protocol. LSL is the standard for real-time data
 6exchange in neuroscience research, enabling communication between different
 7BCI applications, analysis tools, and recording systems.
 8
 9What this example shows:
10- Generating synthetic 8-channel EEG-like signals with noise
11- Capturing keyboard events as experimental markers
12- Merging signal and event data using Router
13- Streaming combined data over LSL network protocol
14- Headless operation (no GUI) for dedicated streaming applications
15
16Expected behavior:
17When you run this example:
18- LSL stream becomes discoverable on the local network
19- Other LSL applications can connect and receive the data stream
20- Data includes 8 signal channels + 1 event channel
21- Keyboard events are transmitted as numerical markers
22- Console shows "Pipeline is running. Press enter to stop."
23
24Network streaming details:
25- Protocol: Lab Streaming Layer (LSL)
26- Stream name: Automatically generated by LSLSender
27- Data format: 9 channels (8 signals + 1 events)
28- Sampling rate: 250 Hz
29- Discovery: Automatic via LSL network protocol
30
31Real-world applications:
32- Multi-application BCI systems (data sharing between tools)
33- Real-time analysis in separate applications (MATLAB, Python)
34- Recording with third-party software (LSL Recorder)
35- Distributed BCI systems across multiple computers
36- Integration with analysis frameworks (EEGLAB, MNE-Python)
37- Real-time feedback and visualization tools
38
39Common LSL ecosystem:
40- Data sources: g.Pype, OpenViBE, BCI2000, hardware drivers
41- Data sinks: LSL Recorder, MATLAB LSL toolbox, MNE real-time
42- Analysis tools: Real-time signal processing and classification
43- Visualization: Real-time plotting and monitoring applications
44
45Usage:
46    1. Run: python example_basic_lsl_send.py
47    2. Use LSL viewer or example_basic_lsl_receive.py to see the stream
48    3. Press arrow keys to send event markers
49    4. Press Enter in console to stop streaming
50
51Prerequisites:
52    - pylsl library installed (pip install pylsl)
53    - Network connectivity for LSL discovery
54"""
55import gpype as gp
56
57fs = 250  # Sampling frequency in Hz
58
59if __name__ == "__main__":
60    # Create processing pipeline (no GUI needed for streaming)
61    p = gp.Pipeline()
62
63    # Generate synthetic 8-channel EEG-like signals
64    source = gp.Generator(
65        sampling_rate=fs,
66        channel_count=8,  # 8 EEG channels
67        signal_frequency=10,  # 10 Hz alpha rhythm
68        signal_amplitude=10,  # Signal strength
69        signal_shape="sine",  # Clean sine waves
70        noise_amplitude=10,
71    )  # Background noise
72
73    # Capture keyboard input as event markers
74    keyboard = gp.Keyboard()  # Arrow keys -> event codes
75
76    # Combine signal data (8 channels) + keyboard events (1 channel)
77    router = gp.Router(input_selector=[gp.Router.ALL, gp.Router.ALL])
78
79    # LSL sender for network streaming
80    sender = gp.LSLSender()  # Creates discoverable LSL stream
81
82    # Connect processing chain: signals + events -> network stream
83    p.connect(source, router["in1"])  # Signal data -> Router input 1
84    p.connect(keyboard, router["in2"])  # Event data -> Router input 2
85    p.connect(router, sender)  # Combined data -> LSL stream
86
87    # Start headless streaming operation
88    p.start()  # Begin data streaming
89    input("Pipeline is running. Press enter to stop.")  # Wait for user
90    p.stop()  # Stop streaming and cleanup

File example_basic_lsl_receive.py (standalone LSL receiver)View file on GitHub

  1"""
  2Basic LSL Receive Example - Lab Streaming Layer Reception and Visualization
  3
  4This example demonstrates how to receive and visualize data from Lab Streaming
  5Layer (LSL) streams in real-time. It complements example_basic_lsl_send.py by
  6showing the receiving side of LSL communication, creating a standalone
  7visualization application for LSL data streams.
  8
  9What this example shows:
 10- Automatic discovery and connection to LSL streams on the network
 11- Real-time data reception from LSL protocol
 12- Multi-channel time-series visualization using PyQtGraph
 13- Professional EEG-style display with channel stacking
 14- Continuous scrolling visualization with proper time axis
 15
 16Expected behavior:
 17When you run this example:
 18- Automatically discovers available LSL streams (type "EEG")
 19- Connects to the first available stream
 20- Opens a real-time visualization window
 21- Displays incoming data as scrolling time-series plots
 22- Shows channel names and time axis with proper scaling
 23
 24Workflow with LSL Send example:
 251. Run example_basic_lsl_send.py (creates LSL stream)
 262. Run this script (connects to and visualizes the stream)
 273. Press arrow keys in the send example to see event markers
 284. Observe real-time data updates in the visualization
 29
 30Real-world applications:
 31- Real-time monitoring of BCI experiments
 32- Quality assessment of incoming data streams
 33- Integration with third-party BCI software
 34- Network-based data visualization systems
 35- Multi-application BCI setups
 36- LSL ecosystem testing and debugging
 37
 38Technical features:
 39- Automatic LSL stream discovery by type ("EEG")
 40- Dynamic channel count detection
 41- Efficient circular buffer for continuous data
 42- Adaptive plot decimation for smooth display
 43- Professional scientific visualization styling
 44- Scrolling time axis with proper labeling
 45
 46Visualization details:
 47- Channel stacking: Each channel offset vertically for clarity
 48- Time window: 10 seconds of scrolling history
 49- Amplitude scaling: Automatic normalization for display
 50- Refresh rate: ~25 Hz for smooth real-time updates
 51- Grid lines: Optional grid overlay for easier reading
 52
 53Usage:
 54    1. Ensure an LSL stream is available (run example_basic_lsl_send.py)
 55    2. Run: python example_basic_lsl_receive.py
 56    3. Visualization window opens automatically
 57    4. Close window to stop reception
 58
 59Prerequisites:
 60    - pylsl library (pip install pylsl)
 61    - pyqtgraph (pip install pyqtgraph)
 62    - PySide6 (pip install PySide6)
 63    - Active LSL stream on the network
 64"""
 65
 66import numpy as np
 67import pyqtgraph as pg
 68from pyqtgraph.Qt import QtWidgets, QtCore
 69from PySide6.QtGui import QPalette, QColor
 70from pylsl import StreamInlet, resolve_byprop
 71import sys
 72
 73# Display configuration constants
 74SAMPLING_RATE = 250  # Expected sampling rate in Hz
 75TIME_WINDOW = 10  # Seconds of data to display
 76AMPLITUDE_LIMIT = 50  # µV - amplitude scaling for display
 77
 78MAX_POINTS = int(TIME_WINDOW * SAMPLING_RATE)  # Circular buffer size
 79
 80
 81class LSLTimeScope(QtWidgets.QMainWindow):
 82    """Real-time LSL data visualization widget with multi-channel display."""
 83
 84    def __init__(self):
 85        super().__init__()
 86        self.setWindowTitle("LSL Time Series Scope")
 87
 88        # Apply system theme colors for professional appearance
 89        palette = self.palette()
 90        self.foreground_color = palette.color(QPalette.ColorRole.WindowText)
 91        self.background_color = palette.color(QPalette.ColorRole.Window)
 92
 93        # Create main plot widget with scientific styling
 94        self.plot_widget = pg.PlotWidget()
 95        self.setCentralWidget(self.plot_widget)
 96        self.plot_item = self.plot_widget.getPlotItem()
 97        self.plot_item.showGrid(x=True, y=True, alpha=0.3)  # Subtle grid
 98        self.plot_item.getViewBox().setMouseEnabled(x=False, y=False)
 99        self.plot_widget.setBackground(self.background_color)
100
101        # Discover and connect to LSL stream
102        print("Resolving LSL stream...")
103        streams = resolve_byprop("type", "EEG")  # Find EEG-type streams
104        self.inlet = StreamInlet(streams[0])  # Connect to first available
105        info = self.inlet.info()
106        self.CHANNEL_COUNT = info.channel_count()  # Dynamic channel detection
107        self.FRAME_SIZE = 1  # Pull one sample at a time
108
109        # Configure plot layout and styling
110        self.plot_item.setLabels(left="Channels", bottom="Time (s)")
111        self.plot_item.setYRange(0, self.CHANNEL_COUNT)  # Fit all channels
112
113        # Create channel labels (CH1, CH2, etc.) on Y-axis
114        self.plot_item.getAxis("left").setTicks(
115            [
116                [
117                    (self.CHANNEL_COUNT - i - 0.5, f"CH{i + 1}")
118                    for i in range(self.CHANNEL_COUNT)
119                ]
120            ]
121        )
122        self.plot_item.setXRange(-0.5, TIME_WINDOW + 0.5)  # Time axis range
123
124        # Create individual plot curves for each channel
125        self.curves = []
126        for i in range(self.CHANNEL_COUNT):
127            # Each channel gets its own colored curve
128            curve = self.plot_item.plot(
129                pen=pg.mkPen(
130                    QColor(self.foreground_color), width=1  # noqa: E501
131                )
132            )
133            self.curves.append(curve)
134
135        # Initialize data buffers for efficient circular storage
136        self.t_vec = np.arange(MAX_POINTS) / SAMPLING_RATE  # Time vector
137        self.data_buffer = np.zeros((MAX_POINTS, self.CHANNEL_COUNT))
138        self.sample_index = 0  # Current sample position
139
140        # Setup display refresh timer for smooth real-time updates
141        self.timer = QtCore.QTimer()
142        self.timer.timeout.connect(self.update_plot)
143        self.timer.start(40)  # 25 Hz refresh rate for smooth animation
144
145        self._last_second = None  # Track time axis updates
146
147    def update_plot(self):
148        """Update the real-time plot with new LSL data."""
149        # Pull all available samples from LSL stream (non-blocking)
150        while True:
151            sample, timestamp = self.inlet.pull_sample(timeout=0.0)
152            if sample is None:  # No more samples available
153                break
154            # Convert sample to numpy array and store in circular buffer
155            frame = np.array(sample, dtype=np.float64).reshape(
156                (1, self.CHANNEL_COUNT)
157            )
158            idx = self.sample_index % MAX_POINTS  # Circular buffer index
159            self.data_buffer[idx, :] = frame
160            self.sample_index += 1
161
162        # Update plot curves with decimated data for performance
163        N = max(1, int(MAX_POINTS / self.width()))  # Decimation factor
164        t_disp = self.t_vec[::N]  # Decimated time vector
165        for i, curve in enumerate(self.curves):
166            # Stack channels vertically with offset for clear separation
167            offset = self.CHANNEL_COUNT - i - 0.5
168            curve.setData(
169                t_disp, self.data_buffer[::N, i] / AMPLITUDE_LIMIT / 2 + offset
170            )  # noqa: E501
171
172        # Update time axis labels for scrolling display
173        cur_second = int(
174            np.floor((self.sample_index % MAX_POINTS) / SAMPLING_RATE)
175        )  # noqa: E501
176        if cur_second != self._last_second:
177            time_window = TIME_WINDOW
178            if self.sample_index > MAX_POINTS:
179                # Scrolling mode: update time labels as data scrolls
180                ticks = []
181                for i in range(int(np.floor(time_window)) + 1):
182                    tick_val = (
183                        np.mod(i - (cur_second + 1), time_window)
184                        + cur_second
185                        + 1
186                    )  # noqa: E501
187                    offset = (
188                        np.floor(self.sample_index / MAX_POINTS - 1)
189                        * time_window
190                    )  # noqa: E501
191                    tick_val += offset
192                    tick_label = f"{tick_val:.0f}"
193                    ticks.append((i, tick_label))
194            else:
195                # Initial filling mode: simple incremental time labels
196                ticks = [
197                    (i, f"{i:.0f}" if i <= cur_second else "")
198                    for i in range(int(np.floor(time_window)) + 1)
199                ]
200            self.plot_item.getAxis("bottom").setTicks([ticks])
201            self._last_second = cur_second
202
203
204if __name__ == "__main__":
205    # Create Qt application and LSL visualization window
206    app = QtWidgets.QApplication(sys.argv)
207    window = LSLTimeScope()
208    window.resize(1000, 500)  # Set reasonable window size
209    window.show()
210    sys.exit(app.exec())  # Start Qt event loop