mendi 0.0.2

Rust client for the Mendi neurofeedback headband over BLE using btleplug
Documentation
//! Simulator binary — runs a fake Mendi headband and prints events.
//!
//! ```bash
//! cargo run --features simulate --bin mendi-sim
//! cargo run --features simulate --bin mendi-sim -- --frames 500
//! cargo run --features simulate --bin mendi-sim -- --hz 50 --disconnect 200
//! ```

use std::env;

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

fn parse_arg(args: &[String], flag: &str) -> Option<String> {
    args.windows(2)
        .find(|w| w[0] == flag)
        .map(|w| w[1].clone())
}

#[tokio::main]
async fn main() {
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();

    let args: Vec<String> = env::args().collect();

    let mut config = SimConfig::default();

    if let Some(hz) = parse_arg(&args, "--hz") {
        config.frame_rate_hz = hz.parse().expect("--hz must be a number");
    }
    if let Some(n) = parse_arg(&args, "--frames").or_else(|| parse_arg(&args, "--disconnect")) {
        config.disconnect_after_frames = Some(n.parse().expect("--frames must be a number"));
    }
    if let Some(v) = parse_arg(&args, "--voltage") {
        config.battery_voltage_mv = v.parse().expect("--voltage must be a number");
    }
    if args.contains(&"--charging".to_string()) {
        config.charging = true;
        config.usb_connected = true;
    }

    eprintln!("Mendi Simulator");
    eprintln!("  Frame rate:  {} Hz", config.frame_rate_hz);
    eprintln!(
        "  Disconnect:  {}",
        config
            .disconnect_after_frames
            .map(|n| format!("after {n} frames"))
            .unwrap_or_else(|| "manual (Ctrl-C)".into())
    );
    eprintln!("  Battery:     {} mV ({}%)", config.battery_voltage_mv, {
        use mendi::types::{BATTERY_VOLTAGE_MIN_MV, BATTERY_VOLTAGE_MAX_MV};
        let v = config.battery_voltage_mv;
        if v <= BATTERY_VOLTAGE_MIN_MV {
            0
        } else if v >= BATTERY_VOLTAGE_MAX_MV {
            100
        } else {
            ((v - BATTERY_VOLTAGE_MIN_MV) as f32
                / (BATTERY_VOLTAGE_MAX_MV - BATTERY_VOLTAGE_MIN_MV) as f32
                * 100.0)
                .round() as u32
        }
    });
    eprintln!();

    let (mut rx, handle) = SimulatedDevice::start(config);

    // Ctrl-C handler — share the handle for disconnect
    let running = handle.running_flag();
    tokio::spawn(async move {
        tokio::signal::ctrl_c().await.ok();
        eprintln!("\nCtrl-C received, stopping simulation …");
        running.store(false, std::sync::atomic::Ordering::SeqCst);
    });

    let mut frame_count: u64 = 0;
    while let Some(event) = rx.recv().await {
        match event {
            MendiEvent::Connected(info) => {
                println!("✅  Connected: {info}");
            }
            MendiEvent::Disconnected => {
                println!("❌  Disconnected after {frame_count} frames.");
                break;
            }
            MendiEvent::Frame(f) => {
                frame_count += 1;
                if frame_count <= 3 || frame_count.is_multiple_of(25) {
                    println!(
                        "[FRAME #{frame_count}] temp={:.1}°C  \
                         L(ir={:6} red={:6}) R(ir={:6} red={:6}) P(ir={:6} red={:6})",
                        f.temperature,
                        f.ir_left, f.red_left,
                        f.ir_right, f.red_right,
                        f.ir_pulse, f.red_pulse,
                    );
                }
            }
            MendiEvent::Battery(b) => {
                println!(
                    "[BATTERY] {:.2}V  {}%  charging={}",
                    b.voltage(),
                    b.percentage(),
                    b.charging,
                );
            }
            MendiEvent::Calibration(c) => {
                println!(
                    "[CALIBRATION] L={:.1} R={:.1} P={:.1}  auto={}",
                    c.offset_left, c.offset_right, c.offset_pulse, c.auto_calibration,
                );
            }
            MendiEvent::Diagnostics(d) => {
                println!(
                    "[DIAGNOSTICS] imu={} sensor={}",
                    d.imu_ok, d.sensor_ok,
                );
            }
            MendiEvent::SensorRead(s) => {
                println!("[SENSOR] addr=0x{:02X} data=0x{:06X}", s.address, s.data);
            }
        }
    }

    handle.join().await;
}