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