# hermes-ble
[](https://crates.io/crates/hermes-ble)
[](https://docs.rs/hermes-ble)
[](LICENSE)
Async Rust library and terminal UI for streaming EEG data from **Hermes V1** headsets over Bluetooth Low Energy.
## Supported hardware
| Hermes V1 | ADS1299 | 8 | 9-DOF (accel + gyro + mag) | 250 Hz EEG, 24-bit resolution |
## Features
- **Cross-platform BLE** — built on [btleplug](https://crates.io/crates/btleplug) (Linux, macOS, Windows)
- **Async Rust** — tokio-based, zero-copy packet parsing
- **Full-screen TUI** — real-time 8-channel EEG waveform viewer with ratatui
- **9-DOF motion** — accelerometer, gyroscope, and magnetometer streaming
- **Packet-loss detection** — automatic gap detection in the 0–127 packet index ring
- **Built-in simulator** — test the TUI without hardware (`--simulate`)
- **Testable app state** — the `tui_app` module can be driven with mock signals in unit tests
## Quick start
### As a library
Add to your `Cargo.toml`:
```toml
[dependencies]
hermes-ble = "0.0.1"
```
```rust
use hermes_ble::prelude::*;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = HermesClient::new(HermesClientConfig::default());
let (mut rx, handle) = client.connect().await?;
handle.start().await?;
while let Some(event) = rx.recv().await {
match event {
HermesEvent::Eeg(s) => println!("EEG ch0={:.2} µV", s.channels[0]),
HermesEvent::Disconnected => break,
_ => {}
}
}
Ok(())
}
```
### Headless CLI
```bash
cargo run --bin hermes-ble
```
Scans for a Hermes device, connects, and prints all EEG/IMU events to stdout. Supports interactive commands via stdin:
| `q` | Quit |
| `t` | Enable test mode |
| `d` | Disconnect |
### Terminal UI
```bash
cargo run --bin tui
```
Or run with the built-in simulator (no hardware needed):
```bash
cargo run --bin tui -- --simulate
```
#### Keyboard shortcuts (streaming view)
| `Tab` | Open device picker |
| `1` | Switch to EEG view |
| `2` | Switch to Motion view |
| `s` | Trigger a fresh BLE scan |
| `+` / `=` | Zoom out (increase µV scale) |
| `-` | Zoom in (decrease µV scale) |
| `a` | Auto-scale Y axis to current peak |
| `v` | Toggle smooth overlay |
| `p` | Pause display |
| `r` | Resume display |
| `c` | Clear waveform buffers |
| `d` | Disconnect current device |
| `q` / `Esc` | Quit |
#### Keyboard shortcuts (device picker)
| `↑` / `↓` | Navigate list |
| `Enter` | Connect to highlighted device |
| `s` | Rescan |
| `Esc` | Close picker |
## Module overview
| [`prelude`](https://docs.rs/hermes-ble/latest/hermes_ble/prelude/) | One-line glob import of commonly needed types |
| [`hermes_client`](https://docs.rs/hermes-ble/latest/hermes_ble/hermes_client/) | BLE scanning, connecting, and the `HermesHandle` command API |
| [`types`](https://docs.rs/hermes-ble/latest/hermes_ble/types/) | All event and data types (`EegSample`, `MotionData`, `HermesEvent`, …) |
| [`protocol`](https://docs.rs/hermes-ble/latest/hermes_ble/protocol/) | GATT UUIDs, sampling constants, ADS1299 conversion, command builders |
| [`parse`](https://docs.rs/hermes-ble/latest/hermes_ble/parse/) | Low-level byte-to-sample decoders for EEG and IMU packets |
| [`tui_app`](https://docs.rs/hermes-ble/latest/hermes_ble/tui_app/) | Testable TUI state machine, signal simulator, and smoothing filter |
## Event types
The `mpsc::Receiver<HermesEvent>` returned by `connect()` emits:
| `Eeg(EegSample)` | 8-channel EEG sample in µV (multiple per BLE notification) |
| `Motion(MotionData)` | 9-DOF reading (accel in g, gyro in °/s, mag in gauss) |
| `Event(DeviceEvent)` | Device event (e.g. button press) |
| `Config(ConfigResponse)` | Config characteristic response |
| `Connected(String)` | BLE link established (device name) |
| `Disconnected` | BLE link lost |
| `PacketsDropped(usize)` | Gap detected in EEG packet index sequence |
## Testing
Run the full test suite (103 tests including doc-tests):
```bash
cargo test
```
### Testing the TUI with mock signals
The `tui_app` module exposes the core `App` state machine without any terminal or BLE dependencies, so you can drive it with arbitrary mock signals in tests:
```rust
use hermes_ble::tui_app::{App, AppMode, ViewMode, sim_sample};
use hermes_ble::types::*;
let mut app = App::new();
// Feed the built-in simulator
for i in 0..500 {
let t = i as f64 / 250.0;
for ch in 0..8 {
app.push(ch, sim_sample(t, ch));
}
}
assert_eq!(app.total_samples(), 500);
// Or feed custom mock signals
app.push(0, 50.0 * (std::f64::consts::TAU * 10.0 * 0.5).sin());
// Or apply full HermesEvent structs
app.apply_event(&HermesEvent::Eeg(EegSample {
packet_index: 0,
sample_index: 0,
timestamp: 1000.0,
channels: [10.0; 8],
}));
// Test scale, pause, view switching, etc.
app.auto_scale();
app.paused = true;
app.view = ViewMode::Motion;
```
## Benchmarks
```bash
cargo bench # run all benchmarks
cargo bench --bench parse_bench # parse module only
cargo bench --bench protocol_bench # protocol + TUI app only
cargo bench -- decode_i24_be # single benchmark by name
```
HTML reports are generated at `target/criterion/report/index.html`.
### Benchmark suite
| `decode_i24_be` | 24-bit signed integer decoding |
| `parse_eeg_packet/{1,4,8,16}_samples` | Full EEG notification parsing at various sizes |
| `detect_missing_{none,gap_10,wrap}` | Packet-loss detection |
| `parse_motion` | 9-DOF IMU notification parsing |
| `ads1299_to_microvolts` | ADC-to-µV conversion |
| `sim_sample` / `sim_8ch_one_tick` | Signal simulator throughput |
| `smooth_signal_500pts_w9` | Moving-average filter (500 points, window 9) |
| `app_push_one_sample` / `app_push_8ch_one_tick` | TUI buffer ingestion |
| `auto_scale_full_bufs` | Auto-scale with full 8×500 sample buffers |
## Project structure
```
hermes-ble-rs/
├── Cargo.toml
├── LICENSE # GPL-3.0-or-later
├── README.md
├── src/
│ ├── lib.rs # Crate root and prelude
│ ├── main.rs # Headless CLI binary
│ ├── hermes_client.rs # BLE client, scanning, HermesHandle
│ ├── types.rs # EegSample, MotionData, HermesEvent, …
│ ├── protocol.rs # GATT UUIDs, constants, ADS1299 conversion
│ ├── parse.rs # Packet decoders (EEG, motion)
│ ├── tui_app.rs # Testable TUI state, simulator, smoothing
│ └── bin/
│ └── tui.rs # Full-screen ratatui TUI binary
├── tests/
│ ├── parse_tests.rs # 25 integration tests
│ ├── protocol_tests.rs # 12 integration tests
│ ├── types_tests.rs # 9 integration tests
│ └── tui_app_tests.rs # 17 integration tests (mock signals)
└── benches/
├── parse_bench.rs # Parsing benchmarks
└── protocol_bench.rs # Protocol + TUI app benchmarks
```
## Platform notes
### Linux
Requires the `libdbus` development headers:
```bash
# Debian / Ubuntu
sudo apt install libdbus-1-dev
# Fedora
sudo dnf install dbus-devel
```
### macOS
Works out of the box via CoreBluetooth. The library waits for the Bluetooth adapter to reach `PoweredOn` state before scanning.
### Windows
Works via WinRT Bluetooth APIs. No additional setup required.
## License
This project is licensed under the [GNU General Public License v3.0 or later](LICENSE).
Copyright © 2026 Eugene Hauptmann, Frédéric Simard