hermes-ble 0.0.1

Rust client for Hermes V1 EEG headsets over BLE using btleplug
Documentation

hermes-ble

Crates.io docs.rs License: GPL-3.0-or-later

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

[dependencies]
hermes-ble = "0.0.1"
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

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

cargo run --bin tui

Or run with the built-in simulator (no hardware needed):

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 One-line glob import of commonly needed types
hermes_client BLE scanning, connecting, and the HermesHandle command API
types All event and data types (EegSample, MotionData, HermesEvent, …)
protocol GATT UUIDs, sampling constants, ADS1299 conversion, command builders
parse Low-level byte-to-sample decoders for EEG and IMU packets
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):

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:

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

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:

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

Copyright © 2026 Eugene Hauptmann, Frédéric Simard