mendi 0.0.2

Rust client for the Mendi neurofeedback headband over BLE using btleplug
Documentation
use anyhow::Result;
use log::{error, info, warn};
use std::io::{self, BufRead};

use mendi::mendi_client::{MendiClient, MendiClientConfig};
use mendi::types::MendiEvent;

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

    let config = MendiClientConfig {
        scan_timeout_secs: 15,
        name_prefix: "Mendi".into(),
        filter_by_service_uuid: true,
    };

    // ── Stdin command channel (shared across reconnects) ──────────────────────
    let (line_tx, line_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
    std::thread::spawn(move || {
        let stdin = io::stdin();
        for line in stdin.lock().lines() {
            match line {
                Ok(l) => {
                    if line_tx.send(l.trim().to_owned()).is_err() {
                        break;
                    }
                }
                Err(_) => break,
            }
        }
    });

    // Wrap in Arc<Mutex> so it survives reconnect loops
    let line_rx = std::sync::Arc::new(tokio::sync::Mutex::new(line_rx));

    // ── Connect loop (auto-reconnect) ─────────────────────────────────────────
    loop {
        let client = MendiClient::new(config.clone());

        info!("Connecting to Mendi headband …");
        let (mut rx, handle) = match client.connect().await {
            Ok(r) => r,
            Err(e) => {
                warn!("Connection failed: {e}");
                info!("Retrying in 3 seconds …");
                tokio::time::sleep(std::time::Duration::from_secs(3)).await;
                continue;
            }
        };

        let handle = std::sync::Arc::new(handle);

        info!("Streaming started. Press Ctrl-C or type 'q' + Enter to quit.\n");
        info!("Commands:");
        info!("  q  – quit");
        info!("  c  – write default calibration (auto-cal on)");
        info!("  e  – enable sensor");
        info!("  d  – disable sensor");

        // Spawn command handler for this connection
        let handle_cmd = std::sync::Arc::clone(&handle);
        let line_rx_clone = std::sync::Arc::clone(&line_rx);
        let cmd_task = tokio::spawn(async move {
            let mut rx = line_rx_clone.lock().await;
            while let Some(line) = rx.recv().await {
                if line.is_empty() {
                    continue;
                }
                match line.as_str() {
                    "q" => {
                        info!("Quit requested.");
                        handle_cmd.disconnect().await.ok();
                        std::process::exit(0);
                    }
                    "c" => {
                        info!("Writing default calibration (auto-cal on) …");
                        if let Err(e) = handle_cmd
                            .write_calibration(0.0, 0.0, 0.0, true, false)
                            .await
                        {
                            error!("Calibration write error: {e}");
                        }
                    }
                    "e" => {
                        info!("Enabling sensor …");
                        if let Err(e) = handle_cmd.enable_sensor().await {
                            error!("Enable sensor error: {e}");
                        }
                    }
                    "d" => {
                        info!("Disabling sensor …");
                        if let Err(e) = handle_cmd.disable_sensor().await {
                            error!("Disable sensor error: {e}");
                        }
                    }
                    other => {
                        info!("Unknown command: '{other}'");
                    }
                }
            }
        });

        // ── Main event loop ───────────────────────────────────────────────────
        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.");
                    break;
                }
                MendiEvent::Frame(f) => {
                    frame_count += 1;
                    // Throttle output: print every 10th frame to avoid flooding
                    if frame_count <= 5 || frame_count.is_multiple_of(10) {
                        println!(
                            "[FRAME #{frame_count}] acc=({:+6},{:+6},{:+6}) ang=({:+6},{:+6},{:+6}) \
                             temp={:.1}°C  \
                             L(ir={:6} red={:6} amb={:6}) \
                             R(ir={:6} red={:6} amb={:6}) \
                             P(ir={:6} red={:6} amb={:6})",
                            f.acc_x, f.acc_y, f.acc_z,
                            f.ang_x, f.ang_y, f.ang_z,
                            f.temperature,
                            f.ir_left, f.red_left, f.amb_left,
                            f.ir_right, f.red_right, f.amb_right,
                            f.ir_pulse, f.red_pulse, f.amb_pulse,
                        );
                    }
                }
                MendiEvent::Battery(b) => {
                    println!(
                        "[BATTERY] {:.2}V  {}%  charging={}  usb={}",
                        b.voltage(),
                        b.percentage(),
                        b.charging,
                        b.usb_connected,
                    );
                }
                MendiEvent::Calibration(c) => {
                    println!(
                        "[CALIBRATION] offsets: L={:.1} R={:.1} P={:.1}  \
                         auto_cal={}  low_power={}",
                        c.offset_left, c.offset_right, c.offset_pulse,
                        c.auto_calibration, c.low_power_mode,
                    );
                }
                MendiEvent::Diagnostics(d) => {
                    println!(
                        "[DIAGNOSTICS] imu_ok={}  sensor_ok={}  adc={:?}",
                        d.imu_ok,
                        d.sensor_ok,
                        d.adc.as_ref().map(|a| format!("{:.2}V ({}%)", a.voltage(), a.percentage())),
                    );
                }
                MendiEvent::SensorRead(s) => {
                    println!(
                        "[SENSOR] addr=0x{:02X}  data=0x{:06X}",
                        s.address, s.data,
                    );
                }
            }
        }

        // Abort the command task for this connection
        cmd_task.abort();

        if frame_count > 0 {
            info!("Received {frame_count} frames total this session.");
        }
        info!("Reconnecting in 3 seconds …");
        tokio::time::sleep(std::time::Duration::from_secs(3)).await;
    }
}