haply 1.3.1

Haply Robotics Client Library for the Inverse Service
Documentation
//! Bubble navigation toggle demo -- press VerseGrip Button A to disable/re-enable navigation.
//!
//! Usage:
//!   cargo run --example navigation_toggle_demo
//!
//! Starts with bubble navigation enabled using the same settings as `navigation_demo`.
//! Press Button A on a wireless/custom VerseGrip:
//! - first press: disable navigation
//! - next press: re-enable bubble navigation with the same settings
//!
//! Toggle `USE_HTTP` to switch between HTTP and WS configuration.

use haply::{
    device_model::{
        BubbleCenterSettings,
        BubbleNavigationSettings,
        CollisionDetectionSettings,
        Inverse3Configure,
        Linear3D,
        NavigationConfigure,
        SdfPrimitive,
    },
    http::InverseHttpClient,
    HaplyDevice,
};
use std::io::Write;
use std::time::Duration;
use tokio::time::{ interval, sleep };

const USE_HTTP: bool = false;
const HTTP_BASE: &str = "http://localhost:10001";
const WS_URL: &str = "ws://localhost:10001/";
/// Session selector for HTTP session-scoped endpoints.
const SESSION: &str = ":-1";

fn bubble_nav_config() -> NavigationConfigure {
    NavigationConfigure {
        mode: "bubble".to_string(),
        bubble: Some(BubbleNavigationSettings {
            center: Some(BubbleCenterSettings {
                // Zero offset from current cursor at navigation start.
                position: Some(Linear3D { x: 0.0, y: -0.05, z: 0.18 }),
                // `false` means `position` is interpreted as an absolute point.
                relative: Some(false),
                follow: Some(false),
                speed: None,
            }),
            shape: Some(SdfPrimitive::Sphere { r: 0.05 }),
            velocity_zone_width: Some(0.03),
            max_velocity: Some(0.5),
            spring_surface: Some(7.0),
            damping_surface: Some(0.7),
            collision_detection: Some(CollisionDetectionSettings {
                // Disabled by default for position-verification tests.
                enabled: Some(false),
                force_threshold: Some(1.0),
                inflate_ratio: Some(2.0),
                exit_ratio: Some(0.7),
            }),
            ..Default::default()
        }),
    }
}

fn disabled_nav_config() -> NavigationConfigure {
    NavigationConfigure {
        mode: "disabled".to_string(),
        bubble: None,
    }
}

async fn apply_navigation_mode(
    device: &mut HaplyDevice,
    http: &InverseHttpClient,
    inverse_id: &str,
    enabled: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let nav_config = if enabled { bubble_nav_config() } else { disabled_nav_config() };

    if USE_HTTP {
        http.set_navigation("inverse3", inverse_id, Some(SESSION), &nav_config).await?;
    } else {
        device.configure_inverse3(
            inverse_id,
            Inverse3Configure {
                navigation: Some(nav_config),
                ..Default::default()
            },
            Some(true),
        ).await?;
    }

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let mut device = HaplyDevice::new(HTTP_BASE, WS_URL).await?;
    let http = InverseHttpClient::new(HTTP_BASE);

    sleep(Duration::from_millis(300)).await;
    let state = device.read_state().await?;

    let inv = state.inverse3.first().ok_or("No Inverse3 device connected")?;
    let id = inv.device_id.clone();
    println!("Using Inverse3 device: {}", id);

    println!("Enabling bubble navigation...");
    apply_navigation_mode(&mut device, &http, &id, true).await?;

    println!("Press VerseGrip Button A to toggle navigation (Ctrl+C to stop)");
    let mut ticker = interval(Duration::from_millis(50));
    let mut tick_count: u32 = 0;
    let mut nav_enabled = true;
    let mut last_button_a = false;

    loop {
        ticker.tick().await;
        tick_count += 1;

        let state = device.read_state().await?;
        let button_a = state.custom_verse_grip
            .iter()
            .find_map(|d| d.state.buttons.map(|b| b.a))
            .or_else(|| state.wireless_verse_grip.iter().find_map(|d| d.state.buttons.map(|b| b.a)))
            .unwrap_or(false);

        // Rising-edge detect so one press toggles once.
        if button_a && !last_button_a {
            nav_enabled = !nav_enabled;
            apply_navigation_mode(&mut device, &http, &id, nav_enabled).await?;
            println!(
                "\nButton A pressed -> navigation {}",
                if nav_enabled { "ENABLED (bubble)" } else { "DISABLED" }
            );
        }
        last_button_a = button_a;

        // Send command each tick to keep streaming alive.
        // Request full state every ~500ms for navigation module fields.
        if tick_count % 10 == 0 {
            device.send_force_full_render().await?;
        } else {
            device.send_command().await?;
        }

        if let Some(inv) = state.inverse3.first() {
            let cursor = inv.state.cursor_position.unwrap_or_default();
            let ws_pos = inv.state.transform
                .as_ref()
                .map(|t| t.position)
                .unwrap_or_default();
            let nav_center = inv.state.navigation
                .as_ref()
                .and_then(|n| n.bubble.as_ref())
                .and_then(|b| b.center);
            let nav_mode = inv.status.navigation
                .as_ref()
                .and_then(|s| s.mode.as_deref())
                .unwrap_or("off");

            print!(
                "\rButtonA: {}  NavEnabled: {}  Cursor: ({:.4},{:.4},{:.4})  WS: ({:.4},{:.4},{:.4})  Nav: {:?}  Mode: {}   ",
                button_a,
                nav_enabled,
                cursor.x,
                cursor.y,
                cursor.z,
                ws_pos.x,
                ws_pos.y,
                ws_pos.z,
                nav_center,
                nav_mode
            );
            std::io::stdout().flush().unwrap();
        }
    }
}