|
| 1 | +# ESP32 Support |
| 2 | + |
| 3 | +Secure LSL includes a protocol-compatible implementation for ESP32 microcontrollers, enabling WiFi-connected embedded devices to participate in encrypted LSL lab networks. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +**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. |
| 10 | + |
| 11 | +### Scope |
| 12 | + |
| 13 | +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. |
| 14 | + |
| 15 | +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. |
| 16 | + |
| 17 | +### Why a Reimplementation? |
| 18 | + |
| 19 | +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. |
| 20 | + |
| 21 | +### Features |
| 22 | + |
| 23 | +- **Full LSL protocol**: UDP multicast discovery + TCP data streaming (v1.10) |
| 24 | +- **Bidirectional**: both outlet (push) and inlet (pull) |
| 25 | +- **secureLSL encryption**: ChaCha20-Poly1305 authenticated encryption, X25519 key exchange (from Ed25519 identity keys) |
| 26 | +- **Desktop interop**: verified with pylsl, LabRecorder, and desktop secureLSL |
| 27 | +- **Real-time**: sustains up to 1000 Hz with near-zero packet loss |
| 28 | +- **Lightweight**: ~200KB SRAM footprint, 300KB+ free for application |
| 29 | + |
| 30 | +### Hardware Requirements |
| 31 | + |
| 32 | +| Requirement | Minimum | Tested | |
| 33 | +|------------|---------|--------| |
| 34 | +| MCU | ESP32 (Xtensa LX6) | ESP32-WROOM-32 | |
| 35 | +| SRAM | 520KB | ESP32-DevKitC v4 | |
| 36 | +| Flash | 2MB+ | 4MB | |
| 37 | +| WiFi | 802.11 b/g/n | 2.4GHz | |
| 38 | + |
| 39 | +--- |
| 40 | + |
| 41 | +## Quick Start |
| 42 | + |
| 43 | +### Prerequisites |
| 44 | + |
| 45 | +- [ESP-IDF v5.5+](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/) |
| 46 | +- ESP32 development board |
| 47 | +- WiFi network shared with desktop |
| 48 | +- For encrypted streaming: desktop Secure LSL (see [Installation](../getting-started/installation.md)) |
| 49 | + |
| 50 | +### 1. Flash the secure outlet example |
| 51 | + |
| 52 | +```bash |
| 53 | +cd liblsl-ESP32/examples/secure_outlet |
| 54 | +idf.py menuconfig |
| 55 | +# Set WiFi credentials and secureLSL keypair |
| 56 | +idf.py build |
| 57 | +idf.py -p /dev/cu.usbserial-XXXX flash monitor |
| 58 | +``` |
| 59 | + |
| 60 | +### 2. Receive on desktop with Secure LSL |
| 61 | + |
| 62 | +Ensure you have built Secure LSL with security enabled (see [Installation](../getting-started/installation.md)), then: |
| 63 | + |
| 64 | +```bash |
| 65 | +./cpp_secure_inlet --stream ESP32Secure --samples 100 |
| 66 | +``` |
| 67 | + |
| 68 | +### 3. Or use the unencrypted outlet |
| 69 | + |
| 70 | +```bash |
| 71 | +cd liblsl-ESP32/examples/basic_outlet |
| 72 | +idf.py menuconfig # Set WiFi |
| 73 | +idf.py build && idf.py -p PORT flash monitor |
| 74 | +``` |
| 75 | + |
| 76 | +```python |
| 77 | +import pylsl |
| 78 | +streams = pylsl.resolve_byprop('name', 'ESP32Test', timeout=10) |
| 79 | +inlet = pylsl.StreamInlet(streams[0]) |
| 80 | +sample, ts = inlet.pull_sample() |
| 81 | +``` |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | +## Security Setup |
| 86 | + |
| 87 | +The ESP32 uses the same shared keypair model as desktop Secure LSL. All devices in a lab must share the same Ed25519 keypair. |
| 88 | + |
| 89 | +!!! warning "All devices must share the same keypair" |
| 90 | + The ESP32 and desktop must have identical Ed25519 keypairs. Mismatched keys result in a 403 connection rejection (unanimous security enforcement). |
| 91 | + |
| 92 | +### Key Provisioning |
| 93 | + |
| 94 | +The recommended workflow is to generate keys on the desktop using `lsl-keygen`, then import them to the ESP32: |
| 95 | + |
| 96 | +```c |
| 97 | +#include "lsl_esp32.h" |
| 98 | +#include "nvs_flash.h" |
| 99 | + |
| 100 | +nvs_flash_init(); |
| 101 | + |
| 102 | +// Import the desktop keypair (recommended) |
| 103 | +lsl_esp32_import_keypair("BASE64_PUBLIC_KEY", "BASE64_PRIVATE_KEY"); |
| 104 | + |
| 105 | +// Enable encryption for all subsequent outlets/inlets |
| 106 | +lsl_esp32_enable_security(); |
| 107 | +``` |
| 108 | +
|
| 109 | +Alternatively, generate a new keypair on the ESP32 and distribute it to all devices: |
| 110 | +
|
| 111 | +```c |
| 112 | +lsl_esp32_generate_keypair(); |
| 113 | +
|
| 114 | +// Export public key for distribution to desktop and other devices |
| 115 | +char pubkey[LSL_ESP32_KEY_BASE64_SIZE]; |
| 116 | +lsl_esp32_export_pubkey(pubkey, sizeof(pubkey)); |
| 117 | +// Import the full keypair to desktop via lsl_api.cfg |
| 118 | +``` |
| 119 | + |
| 120 | +!!! note "No passphrase support on ESP32" |
| 121 | + 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`. |
| 122 | + |
| 123 | +### Desktop Configuration |
| 124 | + |
| 125 | +The desktop must have the matching keypair in `~/.lsl_api/lsl_api.cfg`: |
| 126 | + |
| 127 | +```ini |
| 128 | +[security] |
| 129 | +enabled = true |
| 130 | +private_key = YOUR_BASE64_PRIVATE_KEY |
| 131 | +``` |
| 132 | + |
| 133 | +For key extraction and distribution details, see the [ESP32 Security Guide](https://github.com/sccn/secureLSL/blob/main/liblsl-ESP32/docs/security.md). |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +## API Overview |
| 138 | + |
| 139 | +```c |
| 140 | +// Stream info |
| 141 | +lsl_esp32_stream_info_t info = lsl_esp32_create_streaminfo( |
| 142 | + "MyStream", "EEG", 8, 250.0, LSL_ESP32_FMT_FLOAT32, "source_id"); |
| 143 | + |
| 144 | +// Outlet (push) |
| 145 | +lsl_esp32_outlet_t outlet = lsl_esp32_create_outlet(info, 0, 360); |
| 146 | +lsl_esp32_push_sample_f(outlet, data, 0.0); |
| 147 | + |
| 148 | +// Inlet (pull) |
| 149 | +lsl_esp32_stream_info_t found; |
| 150 | +lsl_esp32_resolve_stream("name", "DesktopStream", 10.0, &found); |
| 151 | +lsl_esp32_inlet_t inlet = lsl_esp32_create_inlet(found); |
| 152 | +lsl_esp32_inlet_pull_sample_f(inlet, buf, buf_len, ×tamp, 5.0); |
| 153 | + |
| 154 | +// Security |
| 155 | +lsl_esp32_generate_keypair(); |
| 156 | +lsl_esp32_import_keypair(base64_pub, base64_priv); |
| 157 | +lsl_esp32_export_pubkey(out, out_len); |
| 158 | +lsl_esp32_has_keypair(); |
| 159 | +lsl_esp32_enable_security(); |
| 160 | +``` |
| 161 | +
|
| 162 | +Full API: [lsl_esp32.h on GitHub](https://github.com/sccn/secureLSL/blob/main/liblsl-ESP32/components/liblsl_esp32/include/lsl_esp32.h) |
| 163 | +
|
| 164 | +--- |
| 165 | +
|
| 166 | +## Performance |
| 167 | +
|
| 168 | +Benchmarked on ESP32-DevKitC v4 over WiFi (802.11n, RSSI -36 dBm): |
| 169 | +
|
| 170 | +| Config | Rate | Packet Loss | Encryption Cost | |
| 171 | +|--------|------|-------------|-----------------| |
| 172 | +| 8ch float32 | 250 Hz | 0% | 2 KB heap (push async) | |
| 173 | +| 8ch float32 | 500 Hz | 0% | 2 KB heap (push async) | |
| 174 | +| 8ch float32 | 1000 Hz | 0.02% | 2 KB heap (push async) | |
| 175 | +| 64ch float32 | 250 Hz | 0% | 2 KB heap (push async) | |
| 176 | +
|
| 177 | +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. |
| 178 | +
|
| 179 | +--- |
| 180 | +
|
| 181 | +## Protocol Compatibility |
| 182 | +
|
| 183 | +| Feature | Desktop liblsl | liblsl-ESP32 | |
| 184 | +|---------|---------------|-------------| |
| 185 | +| Protocol version | 1.00 + 1.10 | 1.10 only | |
| 186 | +| IP version | IPv4 + IPv6 | IPv4 only | |
| 187 | +| Channel formats | All | float32, double64, int32, int16, int8 | |
| 188 | +| secureLSL encryption | Yes | Yes (wire-compatible) | |
| 189 | +| Max connections | Unlimited | 3 concurrent | |
| 190 | +| Max channels | Unlimited | 128 | |
| 191 | +
|
| 192 | +--- |
| 193 | +
|
| 194 | +## Examples |
| 195 | +
|
| 196 | +| Example | Description | |
| 197 | +|---------|-------------| |
| 198 | +| `basic_outlet` | Unencrypted 8-channel sine wave outlet | |
| 199 | +| `basic_inlet` | Unencrypted stream receiver | |
| 200 | +| `secure_outlet` | Encrypted outlet with key provisioning | |
| 201 | +| `secure_inlet` | Encrypted receiver | |
| 202 | +
|
| 203 | +--- |
| 204 | +
|
| 205 | +## Documentation |
| 206 | +
|
| 207 | +For detailed documentation, see the [liblsl-ESP32 repository](https://github.com/sccn/secureLSL/tree/main/liblsl-ESP32): |
| 208 | +
|
| 209 | +- [Architecture](https://github.com/sccn/secureLSL/blob/main/liblsl-ESP32/docs/architecture.md) -- protocol layers, threading, memory |
| 210 | +- [Security Guide](https://github.com/sccn/secureLSL/blob/main/liblsl-ESP32/docs/security.md) -- key provisioning, setup, troubleshooting |
| 211 | +- [Benchmarks](https://github.com/sccn/secureLSL/blob/main/liblsl-ESP32/docs/benchmarks.md) -- methodology and full results |
| 212 | +- [Changelog](https://github.com/sccn/secureLSL/blob/main/liblsl-ESP32/CHANGELOG.md) -- version history |
0 commit comments