Skip to content

Commit a7bfbbc

Browse files
Add ESP32 support with secureLSL encryption
* feat: add ESP32 support with secureLSL encryption Add liblsl-ESP32: clean-room C reimplementation of the LSL wire protocol for ESP32 microcontrollers with full secureLSL encryption. Components: - Core library (~4100 lines C, ESP-IDF component) - 4 examples: basic_outlet, basic_inlet, secure_outlet, secure_inlet - Benchmark suite: throughput firmware + desktop Python scripts - Documentation: architecture, security guide, benchmarks Key features: - Protocol v1.10, wire-compatible with desktop liblsl/secureLSL - ChaCha20-Poly1305 encryption, Ed25519 key exchange via libsodium - Sustains up to 1000 Hz with near-zero packet loss - Zero measurable encryption overhead (async on dual-core) - ~200KB SRAM footprint, 300KB+ free for application Documentation integrated into mkdocs site under ESP32 section. * fix: sync with upstream review fixes (copyright, docs, nonce comment) * fix: address documentation review findings - Fix key exchange terminology: X25519 (not Ed25519) - Add passphrase caveat (ESP32 uses raw keys, not encrypted_private_key) - Add key distribution workflow (import desktop keys recommended) - Fix broken links: use GitHub URLs instead of relative paths - Add MkDocs admonitions (warning, note) matching secureLSL style - Add section separators (---) for consistency - Show export_pubkey and has_keypair in API overview - Replace "Not measurable" with "2 KB heap (push async)" in perf table - Cross-reference secureLSL Installation instead of inline build commands - Remove duplicated key extraction snippet (link to security guide) - Consistent line count (~4,000) - Add copyright headers to all remaining source files - Add nonce policy documentation (strict vs windowed) * chore: bump secureLSL version to 1.1.0-alpha ESP32 support is a significant feature addition warranting a minor version bump from 1.0.0 to 1.1.0. Updated: mkdocs.yml, liblsl/CMakeLists.txt, CHANGELOG.md
1 parent 46ae845 commit a7bfbbc

File tree

117 files changed

+10602
-3
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

117 files changed

+10602
-3
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to Secure LSL will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.16.1-secure.1.1.0-alpha] - 2026-03-18
9+
10+
### Added
11+
- **ESP32 support**: liblsl-ESP32, a clean-room C reimplementation of the LSL wire protocol for ESP32 microcontrollers with full secureLSL encryption
12+
- ESP32 outlet and inlet with ChaCha20-Poly1305 encryption, wire-compatible with desktop
13+
- Four ESP32 examples: basic_outlet, basic_inlet, secure_outlet, secure_inlet
14+
- ESP32 benchmark suite: throughput firmware and desktop Python collection scripts
15+
- ESP32 documentation integrated into mkdocs site
16+
17+
### Verified
18+
- Bidirectional encrypted interop: ESP32 to desktop and desktop to ESP32
19+
- Zero packet loss at 250/500 Hz, 0.02% at 1000 Hz
20+
- Zero measurable encryption overhead on ESP32 push path (dual-core async)
21+
822
## [1.16.1-secure.1.0.0-alpha] - 2025-12-07
923

1024
### Added

docs/esp32/overview.md

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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, &timestamp, 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

liblsl-ESP32/.clang-format

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
BasedOnStyle: LLVM
3+
4+
# Match .rules/c-esp32.md and ESP-IDF style
5+
IndentWidth: 4
6+
TabWidth: 4
7+
UseTab: Never
8+
ColumnLimit: 100
9+
10+
# Linux kernel brace style (function braces on new line, control on same line)
11+
BreakBeforeBraces: Linux
12+
AllowShortFunctionsOnASingleLine: None
13+
AllowShortIfStatementsOnASingleLine: Never
14+
AllowShortLoopsOnASingleLine: false
15+
16+
# Pointer alignment
17+
PointerAlignment: Right
18+
19+
# Include ordering
20+
SortIncludes: false
21+
IncludeBlocks: Preserve
22+
23+
# Spacing
24+
SpaceAfterCStyleCast: false
25+
SpaceBeforeParens: ControlStatements
26+
SpacesInParentheses: false
27+
28+
# Alignment
29+
AlignAfterOpenBracket: Align
30+
AlignConsecutiveMacros: Consecutive
31+
AlignEscapedNewlines: Left
32+
AlignOperands: Align
33+
AlignTrailingComments: true
34+
35+
# Other
36+
AllowAllParametersOfDeclarationOnNextLine: true
37+
BinPackArguments: true
38+
BinPackParameters: true
39+
IndentCaseLabels: false
40+
MaxEmptyLinesToKeep: 2
41+
...

liblsl-ESP32/.clang-tidy

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
# clang-tidy checks for ESP32 C project
3+
# Not yet integrated into CI; requires compilation database from idf.py
4+
Checks: >
5+
-*,
6+
bugprone-*,
7+
-bugprone-easily-swappable-parameters,
8+
-bugprone-reserved-identifier,
9+
cert-*,
10+
-cert-dcl37-c,
11+
-cert-dcl51-cpp,
12+
clang-analyzer-*,
13+
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
14+
misc-*,
15+
-misc-unused-parameters,
16+
performance-*,
17+
readability-braces-around-statements,
18+
readability-implicit-bool-conversion,
19+
readability-misleading-indentation,
20+
readability-redundant-declaration,
21+
22+
# ESP-IDF uses many macros and patterns that trigger false positives
23+
HeaderFilterRegex: '(benchmarks|components|examples)/.*\.h$'
24+
25+
CheckOptions:
26+
- key: readability-implicit-bool-conversion.AllowPointerConditions
27+
value: true
28+
- key: readability-implicit-bool-conversion.AllowIntegerConditions
29+
value: true
30+
...

0 commit comments

Comments
 (0)