mendi 0.0.2

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](https://www.mendi.io/) 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

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

```bash
# 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)

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

```toml
[dependencies]
mendi = "0.1.0"

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

## Features

| Feature | Default | Description |
|---------|---------|-------------|
| `simulate` | No | Simulated and mock devices for testing without hardware |
| `tui` | No | Terminal UI with real-time charts (implies `simulate`) |
| `regenerate-proto` | No | Rebuild protobuf Rust types from `proto/device_v4.proto` (requires `protoc`) |

By default, no extra features are enabled and **no system dependencies** are
required beyond a Rust toolchain.

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

### Protobuf code generation

The Rust types for the wire protocol live in the `wire` module. By default,
**pre-generated code** is used (`src/wire_generated.rs`), so you do **not**
need `protoc` or the Protocol Buffers compiler installed to build this crate.

If you modify `proto/device_v4.proto` and want to regenerate the Rust types,
enable the `regenerate-proto` feature:

```bash
cargo build --features regenerate-proto
```

This requires `protoc` to be installed on your system ([install instructions](https://grpc.io/docs/protoc-installation/)).
After regenerating, copy the output from `target/debug/build/mendi-*/out/mendi.rs`
to `src/wire_generated.rs` to update the bundled version:

```bash
cp target/debug/build/mendi-*/out/mendi.rs src/wire_generated.rs
```

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

```rust
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:
```bash
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:

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