Secure LSL includes a protocol-compatible implementation for ESP32 microcontrollers, enabling WiFi-connected embedded devices to participate in encrypted LSL lab networks.
liblsl-ESP32 is a clean-room C reimplementation of the LSL wire protocol for ESP32, with full secureLSL encryption support. It is not a port of desktop liblsl; it reimplements the protocol from scratch using ESP-IDF native APIs.
liblsl-ESP32 provides the communication layer for streaming data over WiFi using the LSL protocol. While the ESP32 includes built-in ADC peripherals, this implementation focuses on the networking and protocol stack rather than signal acquisition. For biosignal applications (EEG, EMG, ECG), the ESP32 typically serves as a wireless bridge: a dedicated ADC IC (e.g., ADS1299, ADS1294) handles acquisition with the precision, noise floor, and simultaneous sampling required for research-grade recordings, while the ESP32 handles WiFi, LSL protocol, and encryption. This separation follows established practice in wireless biosignal systems.
The current implementation uses 802.11 WiFi, but the protocol and encryption layers are transport-agnostic (standard BSD sockets). Developers can substitute alternative low-latency transports including Ethernet (SPI PHY), Bluetooth, or ESP-NOW, reusing the LSL protocol and secureLSL encryption modules. Note that LSL is designed for low-latency local network environments; high-latency transports are not suitable.
Desktop liblsl is ~50,000+ lines of C++ coupled to Boost, pugixml, and C++ features (exceptions, RTTI) that are impractical on a device with 520KB SRAM. The LSL wire protocol is simple (UDP discovery, TCP streamfeed, binary samples), making a clean C reimplementation (~4,000 lines) both smaller and more maintainable.
- Full LSL protocol: UDP multicast discovery + TCP data streaming (v1.10)
- Bidirectional: both outlet (push) and inlet (pull)
- secureLSL encryption: ChaCha20-Poly1305 authenticated encryption, X25519 key exchange (from Ed25519 identity keys)
- Desktop interop: verified with pylsl, LabRecorder, and desktop secureLSL
- Real-time: sustains up to 1000 Hz with near-zero packet loss
- Lightweight: ~200KB SRAM footprint, 300KB+ free for application
| Requirement | Minimum | Tested |
|---|---|---|
| MCU | ESP32 (Xtensa LX6) | ESP32-WROOM-32 |
| SRAM | 520KB | ESP32-DevKitC v4 |
| Flash | 2MB+ | 4MB |
| WiFi | 802.11 b/g/n | 2.4GHz |
- ESP-IDF v5.5+
- ESP32 development board
- WiFi network shared with desktop
- For encrypted streaming: desktop Secure LSL (see Installation)
cd liblsl-ESP32/examples/secure_outlet
idf.py menuconfig
# Set WiFi credentials and secureLSL keypair
idf.py build
idf.py -p /dev/cu.usbserial-XXXX flash monitorEnsure you have built Secure LSL with security enabled (see Installation), then:
./cpp_secure_inlet --stream ESP32Secure --samples 100cd liblsl-ESP32/examples/basic_outlet
idf.py menuconfig # Set WiFi
idf.py build && idf.py -p PORT flash monitorimport pylsl
streams = pylsl.resolve_byprop('name', 'ESP32Test', timeout=10)
inlet = pylsl.StreamInlet(streams[0])
sample, ts = inlet.pull_sample()The ESP32 uses the same shared keypair model as desktop Secure LSL. All devices in a lab must share the same Ed25519 keypair.
!!! warning "All devices must share the same keypair" The ESP32 and desktop must have identical Ed25519 keypairs. Mismatched keys result in a 403 connection rejection (unanimous security enforcement).
The recommended workflow is to generate keys on the desktop using lsl-keygen, then import them to the ESP32:
#include "lsl_esp32.h"
#include "nvs_flash.h"
nvs_flash_init();
// Import the desktop keypair (recommended)
lsl_esp32_import_keypair("BASE64_PUBLIC_KEY", "BASE64_PRIVATE_KEY");
// Enable encryption for all subsequent outlets/inlets
lsl_esp32_enable_security();Alternatively, generate a new keypair on the ESP32 and distribute it to all devices:
lsl_esp32_generate_keypair();
// Export public key for distribution to desktop and other devices
char pubkey[LSL_ESP32_KEY_BASE64_SIZE];
lsl_esp32_export_pubkey(pubkey, sizeof(pubkey));
// Import the full keypair to desktop via lsl_api.cfg!!! note "No passphrase support on ESP32"
The ESP32 stores raw (unencrypted) Ed25519 keys in NVS. It does not support passphrase-protected keys (encrypted_private_key). When configuring the desktop lsl_api.cfg for ESP32 interop, use the private_key field (unencrypted format, generated with lsl-keygen --insecure) rather than the default encrypted_private_key.
The desktop must have the matching keypair in ~/.lsl_api/lsl_api.cfg:
[security]
enabled = true
private_key = YOUR_BASE64_PRIVATE_KEYFor key extraction and distribution details, see the ESP32 Security Guide.
// Stream info
lsl_esp32_stream_info_t info = lsl_esp32_create_streaminfo(
"MyStream", "EEG", 8, 250.0, LSL_ESP32_FMT_FLOAT32, "source_id");
// Outlet (push)
lsl_esp32_outlet_t outlet = lsl_esp32_create_outlet(info, 0, 360);
lsl_esp32_push_sample_f(outlet, data, 0.0);
// Inlet (pull)
lsl_esp32_stream_info_t found;
lsl_esp32_resolve_stream("name", "DesktopStream", 10.0, &found);
lsl_esp32_inlet_t inlet = lsl_esp32_create_inlet(found);
lsl_esp32_inlet_pull_sample_f(inlet, buf, buf_len, ×tamp, 5.0);
// Security
lsl_esp32_generate_keypair();
lsl_esp32_import_keypair(base64_pub, base64_priv);
lsl_esp32_export_pubkey(out, out_len);
lsl_esp32_has_keypair();
lsl_esp32_enable_security();Full API: lsl_esp32.h on GitHub
Benchmarked on ESP32-DevKitC v4 over WiFi (802.11n, RSSI -36 dBm):
| Config | Rate | Packet Loss | Encryption Cost |
|---|---|---|---|
| 8ch float32 | 250 Hz | 0% | 2 KB heap (push async) |
| 8ch float32 | 500 Hz | 0% | 2 KB heap (push async) |
| 8ch float32 | 1000 Hz | 0.02% | 2 KB heap (push async) |
| 64ch float32 | 250 Hz | 0% | 2 KB heap (push async) |
Encryption (ChaCha20-Poly1305) runs asynchronously on core 1 in the TCP feed task, while the application pushes to a lock-free ring buffer on core 0. The encryption overhead is not observable on the application push path; the 2 KB heap overhead for security sessions is the only measurable cost.
| Feature | Desktop liblsl | liblsl-ESP32 |
|---|---|---|
| Protocol version | 1.00 + 1.10 | 1.10 only |
| IP version | IPv4 + IPv6 | IPv4 only |
| Channel formats | All | float32, double64, int32, int16, int8 |
| secureLSL encryption | Yes | Yes (wire-compatible) |
| Max connections | Unlimited | 3 concurrent |
| Max channels | Unlimited | 128 |
| Example | Description |
|---|---|
basic_outlet |
Unencrypted 8-channel sine wave outlet |
basic_inlet |
Unencrypted stream receiver |
secure_outlet |
Encrypted outlet with key provisioning |
secure_inlet |
Encrypted receiver |
For detailed documentation, see the liblsl-ESP32 repository:
- Architecture -- protocol layers, threading, memory
- Security Guide -- key provisioning, setup, troubleshooting
- Benchmarks -- methodology and full results
- Changelog -- version history