Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to Secure LSL will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.16.1-secure.1.1.0-alpha] - 2026-03-18

### Added
- **ESP32 support**: liblsl-ESP32, a clean-room C reimplementation of the LSL wire protocol for ESP32 microcontrollers with full secureLSL encryption
- ESP32 outlet and inlet with ChaCha20-Poly1305 encryption, wire-compatible with desktop
- Four ESP32 examples: basic_outlet, basic_inlet, secure_outlet, secure_inlet
- ESP32 benchmark suite: throughput firmware and desktop Python collection scripts
- ESP32 documentation integrated into mkdocs site

### Verified
- Bidirectional encrypted interop: ESP32 to desktop and desktop to ESP32
- Zero packet loss at 250/500 Hz, 0.02% at 1000 Hz
- Zero measurable encryption overhead on ESP32 push path (dual-core async)

## [1.16.1-secure.1.0.0-alpha] - 2025-12-07

### Added
Expand Down
212 changes: 212 additions & 0 deletions docs/esp32/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# ESP32 Support

Secure LSL includes a protocol-compatible implementation for ESP32 microcontrollers, enabling WiFi-connected embedded devices to participate in encrypted LSL lab networks.

---

## Overview

**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.

### Scope

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.

### Why a Reimplementation?

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.

### Features

- **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

### Hardware Requirements

| 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 |

---

## Quick Start

### Prerequisites

- [ESP-IDF v5.5+](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/)
- ESP32 development board
- WiFi network shared with desktop
- For encrypted streaming: desktop Secure LSL (see [Installation](../getting-started/installation.md))

### 1. Flash the secure outlet example

```bash
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 monitor
```

### 2. Receive on desktop with Secure LSL

Ensure you have built Secure LSL with security enabled (see [Installation](../getting-started/installation.md)), then:

```bash
./cpp_secure_inlet --stream ESP32Secure --samples 100
```

### 3. Or use the unencrypted outlet

```bash
cd liblsl-ESP32/examples/basic_outlet
idf.py menuconfig # Set WiFi
idf.py build && idf.py -p PORT flash monitor
```

```python
import pylsl
streams = pylsl.resolve_byprop('name', 'ESP32Test', timeout=10)
inlet = pylsl.StreamInlet(streams[0])
sample, ts = inlet.pull_sample()
```

---

## Security Setup

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).

### Key Provisioning

The recommended workflow is to generate keys on the desktop using `lsl-keygen`, then import them to the ESP32:

```c
#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:

```c
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`.

### Desktop Configuration

The desktop must have the matching keypair in `~/.lsl_api/lsl_api.cfg`:

```ini
[security]
enabled = true
private_key = YOUR_BASE64_PRIVATE_KEY
```

For key extraction and distribution details, see the [ESP32 Security Guide](https://github.com/sccn/secureLSL/blob/main/liblsl-ESP32/docs/security.md).

---

## API Overview

```c
// 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, &timestamp, 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](https://github.com/sccn/secureLSL/blob/main/liblsl-ESP32/components/liblsl_esp32/include/lsl_esp32.h)

---

## Performance

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.

---

## Protocol Compatibility

| 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 |

---

## Examples

| 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 |

---

## Documentation

For detailed documentation, see the [liblsl-ESP32 repository](https://github.com/sccn/secureLSL/tree/main/liblsl-ESP32):

- [Architecture](https://github.com/sccn/secureLSL/blob/main/liblsl-ESP32/docs/architecture.md) -- protocol layers, threading, memory
- [Security Guide](https://github.com/sccn/secureLSL/blob/main/liblsl-ESP32/docs/security.md) -- key provisioning, setup, troubleshooting
- [Benchmarks](https://github.com/sccn/secureLSL/blob/main/liblsl-ESP32/docs/benchmarks.md) -- methodology and full results
- [Changelog](https://github.com/sccn/secureLSL/blob/main/liblsl-ESP32/CHANGELOG.md) -- version history
41 changes: 41 additions & 0 deletions liblsl-ESP32/.clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
BasedOnStyle: LLVM

# Match .rules/c-esp32.md and ESP-IDF style
IndentWidth: 4
TabWidth: 4
UseTab: Never
ColumnLimit: 100

# Linux kernel brace style (function braces on new line, control on same line)
BreakBeforeBraces: Linux
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false

# Pointer alignment
PointerAlignment: Right

# Include ordering
SortIncludes: false
IncludeBlocks: Preserve

# Spacing
SpaceAfterCStyleCast: false
SpaceBeforeParens: ControlStatements
SpacesInParentheses: false

# Alignment
AlignAfterOpenBracket: Align
AlignConsecutiveMacros: Consecutive
AlignEscapedNewlines: Left
AlignOperands: Align
AlignTrailingComments: true

# Other
AllowAllParametersOfDeclarationOnNextLine: true
BinPackArguments: true
BinPackParameters: true
IndentCaseLabels: false
MaxEmptyLinesToKeep: 2
...
30 changes: 30 additions & 0 deletions liblsl-ESP32/.clang-tidy
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
# clang-tidy checks for ESP32 C project
# Not yet integrated into CI; requires compilation database from idf.py
Checks: >
-*,
bugprone-*,
-bugprone-easily-swappable-parameters,
-bugprone-reserved-identifier,
cert-*,
-cert-dcl37-c,
-cert-dcl51-cpp,
clang-analyzer-*,
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
misc-*,
-misc-unused-parameters,
performance-*,
readability-braces-around-statements,
readability-implicit-bool-conversion,
readability-misleading-indentation,
readability-redundant-declaration,

# ESP-IDF uses many macros and patterns that trigger false positives
HeaderFilterRegex: '(benchmarks|components|examples)/.*\.h$'

CheckOptions:
- key: readability-implicit-bool-conversion.AllowPointerConditions
value: true
- key: readability-implicit-bool-conversion.AllowIntegerConditions
value: true
...
Loading
Loading