haply 1.3.1

Haply Robotics Client Library for the Inverse Service
Documentation
//! Basic device interaction: list devices, stream state, send zero-force.
//!
//! Usage:
//!   cargo run --example device_basics
//!   cargo run --example device_basics -- --stream
//!   cargo run --example device_basics -- --zero-force
//!   cargo run --example device_basics -- --http-only
//!
//! Toggle `USE_HTTP` const to switch between HTTP and WS paths.

use haply::{
    device_model::{ Config, DeviceType, Force, ForceInput },
    http::InverseHttpClient,
    HaplyDevice,
};
use std::io::Write;
use std::{ env, time::Duration };
use tokio::time::{ interval, sleep };

/// Set to `true` to use HTTP-only device listing; `false` for WS-based state.
const USE_HTTP: bool = false;
const HTTP_BASE: &str = "http://localhost:10001";
const WS_URL: &str = "ws://localhost:10001/";

fn has_flag(args: &[String], flag: &str) -> bool {
    args.iter().any(|a| a == flag)
}

fn value_after_flag(args: &[String], flag: &str) -> Option<String> {
    args.iter()
        .position(|a| a == flag)
        .and_then(|idx| args.get(idx + 1).cloned())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let args: Vec<String> = env::args().skip(1).collect();
    let http_only = has_flag(&args, "--http-only") || USE_HTTP;
    let stream_mode = has_flag(&args, "--stream");
    let zero_force = has_flag(&args, "--zero-force");
    let probe_id = value_after_flag(&args, "--probe");

    if stream_mode && zero_force {
        eprintln!("--stream and --zero-force cannot be combined; pick one.");
        std::process::exit(1);
    }

    if http_only {
        run_http_only(HTTP_BASE).await?;
        return Ok(());
    }

    let mut device = HaplyDevice::new(HTTP_BASE, WS_URL).await?;

    if stream_mode {
        run_stream_mode(&mut device, probe_id).await;
        return Ok(());
    }

    if zero_force {
        run_zero_force_mode(&mut device, probe_id).await;
        return Ok(());
    }

    // Wait for full state (give service time to report all devices)
    sleep(Duration::from_millis(500)).await;
    device.send_force_full_render().await?;
    sleep(Duration::from_millis(300)).await;

    let state = device.read_state().await?;
    let total =
        state.inverse3.len() +
        state.verse_grip.len() +
        state.wireless_verse_grip.len() +
        state.custom_verse_grip.len();

    println!("Session ID: {}", state.session_id);
    println!("Found {} connected device(s)", total);

    let mut idx = 1;
    for d in &state.inverse3 {
        println!("  #{}: Inverse3 - ID: {}", idx, d.device_id);
        idx += 1;
    }
    for d in &state.verse_grip {
        println!("  #{}: VerseGrip - ID: {}", idx, d.device_id);
        idx += 1;
    }
    for d in &state.wireless_verse_grip {
        println!("  #{}: WirelessVerseGrip - ID: {}", idx, d.device_id);
        idx += 1;
    }
    for d in &state.custom_verse_grip {
        println!("  #{}: CustomVerseGrip - ID: {}", idx, d.device_id);
        idx += 1;
    }

    if total == 0 {
        println!("No devices found");
    }
    Ok(())
}

async fn run_http_only(http_base: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let client = InverseHttpClient::new(http_base);

    let version = client.get_version().await?;
    println!("Haply Inverse Service Version: {:?}", version);

    let configs = client.get_devices().await?;
    println!("Found {} connected device(s)", configs.len());

    for (idx, config) in configs.iter().enumerate() {
        match config {
            Config::DeviceConfig(dc) => {
                println!("  #{}: Inverse3 - ID: {}", idx + 1, dc.device_info.id);
            }
            Config::VGConfig(vg) => {
                println!("  #{}: VerseGrip - ID: {}", idx + 1, vg.id);
            }
            Config::WVGConfig(wvg) => {
                let label = match wvg.type_ {
                    DeviceType::CustomVerseGrip => "CustomVerseGrip",
                    _ => "WirelessVerseGrip",
                };
                println!("  #{}: {} - ID: {}", idx + 1, label, wvg.id);
            }
        }
    }

    Ok(())
}

async fn run_stream_mode(device: &mut HaplyDevice, probe_id: Option<String>) {
    let mut ticker = interval(Duration::from_millis(100));
    loop {
        ticker.tick().await;

        if let Some(id) = &probe_id {
            if let Err(err) = device.probe_cursor_position(vec![id.clone()], Some(true)).await {
                eprintln!("\rProbe command failed for {}: {}", id, err);
            } else {
                sleep(Duration::from_millis(200)).await;
            }
        }

        let state = match device.force_read_full_state(None).await {
            Ok(state) => state,
            Err(err) => {
                eprintln!("\rFailed to read full state: {}", err);
                continue;
            }
        };

        let inv_count = state.inverse3.len();
        let vg_count = state.verse_grip.len();
        let wvg_count = state.wireless_verse_grip.len();
        let cvg_count = state.custom_verse_grip.len();
        print!(
            "\rDevices: {} Inverse3, {} VG, {} WVG, {} CVG   ",
            inv_count,
            vg_count,
            wvg_count,
            cvg_count
        );
        std::io::stdout().flush().unwrap();
    }
}

async fn run_zero_force_mode(device: &mut HaplyDevice, probe_id: Option<String>) {
    let mut ticker = interval(Duration::from_millis(100));
    loop {
        ticker.tick().await;

        if let Some(id) = &probe_id {
            if let Err(err) = device.probe_cursor_position(vec![id.clone()], Some(true)).await {
                eprintln!("\rProbe command failed for {}: {}", id, err);
            } else {
                sleep(Duration::from_millis(200)).await;
            }
        }

        let state = match device.read_state().await {
            Ok(state) => state,
            Err(err) => {
                eprintln!("\rFailed to read state: {}", err);
                continue;
            }
        };

        if let Some(inv) = state.inverse3.first() {
            let pos = inv.state.cursor_position.unwrap_or_default();
            print!("\r[{}] Pos: ({:.4}, {:.4}, {:.4})   ", inv.device_id, pos.x, pos.y, pos.z);
            std::io::stdout().flush().unwrap();

            let force_in = ForceInput {
                device_id: inv.device_id.clone(),
                forces: Force { x: 0.0, y: 0.0, z: 0.0 },
            };
            if let Err(err) = device.update_force(vec![force_in], Some(true), Some(true)).await {
                eprintln!("\rFailed to update force: {}", err);
            }
        } else {
            print!("\rNo Inverse3 devices available   ");
            std::io::stdout().flush().unwrap();
        }
    }
}