mendi 0.0.1

Rust client for the Mendi neurofeedback headband over BLE using btleplug
Documentation

mendi

Async Rust library and CLI for streaming fNIRS neurofeedback data from Mendi headbands over Bluetooth Low Energy.

Hardware

The Mendi headband is a consumer fNIRS (functional near-infrared spectroscopy) device that measures blood oxygenation in the prefrontal cortex using infrared and red LEDs. It communicates over BLE using protobuf-encoded messages on six GATT characteristics:

UUID Name Data
0xABB1 Frame IMU (accel + gyro) + temperature + 3 optical channels
0xABB2 Sensor Optical sensor register read/write
0xABB3 IMU IMU register read/write
0xABB4 ADC Battery voltage, charging status, USB status
0xABB5 Diagnostics Self-test results (IMU ok, sensor ok)
0xABB6 Calibration LED current offsets, auto-calibration, low-power mode

Each optical channel (left, right, pulse) provides three readings:

  • IR (infrared) — penetrates tissue to measure oxygenated hemoglobin
  • Red — measures deoxygenated hemoglobin
  • Ambient — background light for subtraction

Quick start

use mendi::prelude::*;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = MendiClient::new(MendiClientConfig::default());
    let (mut rx, handle) = client.connect().await?;

    while let Some(event) = rx.recv().await {
        match event {
            MendiEvent::Frame(f) => {
                println!("temp={:.1}°C  IR: L={} R={} P={}",
                    f.temperature, f.ir_left, f.ir_right, f.ir_pulse);
            }
            MendiEvent::Battery(b) => {
                println!("Battery: {:.2}V charging={}", b.voltage(), b.charging);
            }
            MendiEvent::Disconnected => break,
            _ => {}
        }
    }
    Ok(())
}

CLI usage

# Run the CLI (scans, connects, and prints all data)
cargo run

# With debug logging
RUST_LOG=mendi=debug cargo run

Interactive commands once connected:

  • q – quit
  • c – write default calibration (auto-cal on)
  • e – enable optical sensor
  • d – disable optical sensor

TUI (real-time charts)

# Real device
cargo run --features tui --bin mendi-tui

# Simulated (no hardware needed)
cargo run --features tui --bin mendi-tui -- --simulate

Keys: 1 IR view, 2 full view, +/- scale, a auto-scale, v smooth toggle, p pause, r resume, c clear, q quit

Using as a library

[dependencies]
mendi = "0.1.0"

# With simulation/mock support for testing:
mendi = { version = "0.1.0", features = ["simulate"] }

Protocol details

The wire protocol is defined in proto/device_v4.proto (protobuf v3). All characteristics use protobuf encoding. The Frame characteristic (0xABB1) is the primary data stream, delivering real-time sensor readings at the headband's native sample rate.

Frame fields

Field Type Description
acc_x/y/z i32 Accelerometer (±2G default, raw int16)
ang_x/y/z i32 Gyroscope (±125°/s default, raw int16)
temp f32 Temperature in °C
ir_l/r/p i32 Infrared readings (left, right, pulse)
red_l/r/p i32 Red LED readings (left, right, pulse)
amb_l/r/p i32 Ambient light readings (left, right, pulse)

Convenience methods on FrameReading convert raw IMU values to physical units:

  • accel_x_g(), accel_y_g(), accel_z_g() — acceleration in g
  • gyro_x_dps(), gyro_y_dps(), gyro_z_dps() — angular velocity in °/s

UUID namespace

Mendi uses a vendor-specific UUID base: fc3eXXXX-c6c4-49e6-922a-6e551c455af5. Use mendi_uuid(short) to construct UUIDs from 16-bit short codes.

Simulation & testing (feature simulate)

The simulate feature adds two test utilities that don't require hardware:

SimulatedDevice

Generates realistic fNIRS data with sinusoidal optical signals, IMU noise, and periodic battery/calibration events:

use mendi::simulate::{SimulatedDevice, SimConfig};
use mendi::types::MendiEvent;

let config = SimConfig {
    frame_rate_hz: 25.0,
    disconnect_after_frames: Some(100),
    ..Default::default()
};
let (mut rx, handle) = SimulatedDevice::start(config);
while let Some(event) = rx.recv().await {
    // Same MendiEvent stream as real hardware
}

Run the simulator binary:

cargo run --features simulate --bin mendi-sim
cargo run --features simulate --bin mendi-sim -- --frames 500 --hz 50

MockDevice

Deterministic scripted mock for unit tests — no timing, no randomness:

use mendi::simulate::MockDevice;
use mendi::types::MendiEvent;

let (mut rx, mock) = MockDevice::with_frames(10, 32);
// Receives: Connected, 10 Frames, Disconnected

MendiHandle API

Once connected, the MendiHandle provides:

Method Description
write_calibration(...) Set LED current offsets and auto-calibration mode
enable_sensor() Start optical sensor data (some FW versions require this)
disable_sensor() Stop optical sensor data
write_sensor_register(addr, data) Low-level optical sensor register write
read_sensor_register(addr) Low-level optical sensor register read
write_imu_register(addr, data) Low-level IMU register write
read_imu_register(addr, n) Low-level IMU register read
is_connected() Check BLE connection status
disconnect() Gracefully disconnect

License

MIT