haply 1.3.1

Haply Robotics Client Library for the Inverse Service
Documentation
//! Tracy profiling demo: haptic loop + versegrip orientation probing.
//!
//! Usage:
//!   cargo run --example test_tracy --features tracy
//!   cargo run --example test_tracy --features tracy -- --help
//!
//! Requires Haply Inverse Service (v3.5+) with an Inverse3 device connected.

use haply::{ compute_total_force, device_model::*, HaplyDevice, Sphere };
use std::io::Write;

const HTTP_BASE: &str = "http://localhost:10001";
const WS_URL: &str = "ws://localhost:10001/";
const LOOP_TARGET_PERIOD_US: u64 = 250;

#[cfg(feature = "tracy")]
fn init_tracy_bridge() {
    use std::sync::Once;
    use tracing_subscriber::prelude::*;

    static ONCE: Once = Once::new();
    ONCE.call_once(|| {
        let _ = tracing_log::LogTracer::builder().with_max_level(log::LevelFilter::Trace).init();

        let tracy_layer = tracing_tracy::TracyLayer::default();
        let subscriber = tracing_subscriber
            ::registry()
            .with(tracing_subscriber::filter::LevelFilter::TRACE)
            .with(tracy_layer);
        let _ = tracing::subscriber::set_global_default(subscriber);
    });
}

#[cfg(not(feature = "tracy"))]
fn init_tracy_bridge() {
    println!("Tracy feature is disabled; run with `--features tracy` to enable profiling.");
}

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

fn print_help() {
    println!("Tracy profiling demo (haptic loop + versegrip orientation)");
    println!("Usage:");
    println!("  cargo run --example test_tracy --features tracy");
    println!("  cargo run --example test_tracy --features tracy -- --help");
    println!("  cargo run --example test_tracy --features tracy -- full");
    println!("Flags:");
    println!("  full    Disable console output and pacing sleeps for max-throughput profiling");
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let args: Vec<String> = std::env::args().skip(1).collect();
    if has_flag(&args, "--help") || has_flag(&args, "-h") {
        print_help();
        return Ok(());
    }
    let full_throttle = has_flag(&args, "full");

    init_tracy_bridge();

    let mut device = HaplyDevice::new(HTTP_BASE, WS_URL).await?;
    let configs = device.list_devices().await.unwrap_or_default();
    if configs.is_empty() {
        println!("No devices found. Exiting.");
        return Ok(());
    }

    let inverse_id = match
        configs.iter().find_map(|cfg| {
            match cfg {
                Config::DeviceConfig(dc) => Some(dc.device_info.id.clone()),
                _ => None,
            }
        })
    {
        Some(id) => id,
        None => {
            println!("No Inverse3 device found. Exiting.");
            return Ok(());
        }
    };

    let versegrip_id = configs.iter().find_map(|cfg| {
        match cfg {
            Config::WVGConfig(wvg) => Some(wvg.id.clone()),
            Config::VGConfig(vg) => Some(vg.id.clone()),
            _ => None,
        }
    });

    let _ = device.configure_session(
        SessionConfigure {
            profile: Some(ProfileConfig {
                name: "test_tracy".to_string(),
                required_version: Some(">=3.5".to_string()),
            }),
            basis: None,
            serialization: None,
            sdf: None,
        },
        Some(true)
    ).await;

    let sphere = Sphere {
        center: [0.0, -0.1, 0.18],
        radius: 0.05,
        stiffness: 500.0,
        magnetism: 0.0,
        damping: 0.1,
    };
    let objects: Vec<Box<dyn haply::physics_model::PhysicalObject>> = vec![Box::new(sphere)];

    let _ = device.update_force(
        vec![ForceInput {
            device_id: inverse_id.clone(),
            forces: Force { x: 0.0, y: 0.0, z: 0.0 },
        }],
        Some(false),
        Some(true)
    ).await;
    if !full_throttle {
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    if !full_throttle {
        println!("Running Tracy haptic loop (Ctrl+C to stop)...");
    }
    let mut tick: u64 = 0u64;
    let target_period = std::time::Duration::from_micros(LOOP_TARGET_PERIOD_US);
    loop {
        let loop_start = std::time::Instant::now();
        #[cfg(feature = "tracy")]
        let _loop_body_span = tracy_client::span!("example::loop_body");

        tick += 1;
        if let Some(vg_id) = &versegrip_id {
            if tick % 4 == 0 {
                #[cfg(feature = "tracy")]
                let _probe_span = tracy_client::span!("example::probe_orientation");
                let _ = device.probe_orientation(vec![vg_id.clone()], Some(false)).await;
            }
        }

        let state = device.read_state().await?;
        let pos = state.inverse3
            .iter()
            .find(|d| d.device_id == inverse_id)
            .and_then(|d| d.state.cursor_position)
            .unwrap_or_default();

        let total_force = {
            #[cfg(feature = "tracy")]
            let _force_span = tracy_client::span!("example::compute_total_force");
            compute_total_force(pos.into(), &objects)
        };
        let force = Force {
            x: total_force[0],
            y: total_force[1],
            z: total_force[2],
        };
        {
            #[cfg(feature = "tracy")]
            let _update_force_span = tracy_client::span!("example::update_force");
            let _ = device.update_force(
                vec![ForceInput {
                    device_id: inverse_id.clone(),
                    forces: force,
                }],
                Some(true),
                Some(true)
            ).await;
        }

        let orientation = state.wireless_verse_grip
            .iter()
            .find_map(|d| d.state.orientation)
            .or_else(|| state.custom_verse_grip.iter().find_map(|d| d.state.orientation))
            .or_else(|| state.verse_grip.iter().find_map(|d| d.state.orientation));

        if !full_throttle {
            let orientation_text = match orientation {
                Some(o) => format!("({:.4},{:.4},{:.4},{:.4})", o.w, o.x, o.y, o.z),
                None => "None".to_string(),
            };

            if tick % 400 == 0 {
                print!(
                    "\rPos: ({:.4},{:.4},{:.4}) Force: ({:.4},{:.4},{:.4}) Ori: {}    ",
                    pos.x,
                    pos.y,
                    pos.z,
                    total_force[0],
                    total_force[1],
                    total_force[2],
                    orientation_text
                );
                {
                    #[cfg(feature = "tracy")]
                    let _flush_span = tracy_client::span!("example::stdout_flush");
                    std::io::stdout().flush().ok();
                }
            }
        }

        #[cfg(feature = "tracy")]
        drop(_loop_body_span);

        #[cfg(feature = "tracy")]
        {
            let elapsed = loop_start.elapsed();
            let sleep_remainder = if elapsed < target_period {
                target_period - elapsed
            } else {
                std::time::Duration::ZERO
            };
            let overrun = if elapsed > target_period {
                elapsed - target_period
            } else {
                std::time::Duration::ZERO
            };

            let loop_cycle_us = elapsed.as_micros() as f64;
            let sleep_remainder_us = sleep_remainder.as_micros() as f64;
            let overrun_us = overrun.as_micros() as f64;
            tracy_client::plot!("loop_cycle_us", loop_cycle_us);
            tracy_client::plot!("sleep_remainder_us", sleep_remainder_us);
            tracy_client::plot!("overrun_us", overrun_us);
            tracy_client::frame_mark();
        }

        if !full_throttle {
            let elapsed = loop_start.elapsed();
            if elapsed < target_period {
                tokio::time::sleep(target_period - elapsed).await;
            }
        }
    }
}