hermes-ble 0.0.1

Rust client for Hermes V1 EEG headsets over BLE using btleplug
Documentation
# hermes-ble

[![Crates.io](https://img.shields.io/crates/v/hermes-ble.svg)](https://crates.io/crates/hermes-ble)
[![docs.rs](https://docs.rs/hermes-ble/badge.svg)](https://docs.rs/hermes-ble)
[![License: GPL-3.0-or-later](https://img.shields.io/crates/l/hermes-ble.svg)](LICENSE)

Async Rust library and terminal UI for streaming EEG data from **Hermes V1** headsets over Bluetooth Low Energy.

## Supported hardware

| Model | ADC | EEG channels | IMU | Notes |
|---|---|---|---|---|
| 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:

| Command | Action |
|---|---|
| `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)

| Key | Action |
|---|---|
| `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)

| Key | Action |
|---|---|
| `` / `` | Navigate list |
| `Enter` | Connect to highlighted device |
| `s` | Rescan |
| `Esc` | Close picker |

## Module overview

| Module | Purpose |
|---|---|
| [`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:

| Variant | Description |
|---|---|
| `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

| Benchmark | What it measures |
|---|---|
| `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