haply 1.2.0

Haply Robotics Client Library for the Inverse Service
Documentation

haply

Crates.io License

Rust client library for the Haply Inverse Service (v3.5+), covering ~98% of the WebSocket and HTTP APIs for controlling Haply haptic devices.

[dependencies]

haply = "0.8"

tokio = { version = "1", features = ["full"] }

Features

  • Real-time haptic control via WebSocket -- per-tick force, position, and torque commands with streaming device state
  • Device configuration -- preset, basis, mount, damping, force gate, handedness, gravity compensation
  • Session management -- profile naming, version constraints, basis configuration
  • Bubble navigation -- rate-control locomotion with current 3.5 schema (shape, center behavior, springs, damping, collision detection)
  • Full HTTP REST client -- device queries, sessions, settings, device config, filters, grip pairing, navigation
  • Event channel -- real-time device connect/disconnect and system events over a dedicated WebSocket
  • Strongly typed -- all wire types derive Serialize, Deserialize, and TS (ts-rs) for TypeScript generation

Quick start

use haply::HaplyDevice;
use haply::device_model::{ForceInput, Force};

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

    // Read device state from the WebSocket stream
    let state = device.read_state().await?;
    println!("Session {}, {} device(s)", state.session_id, state.inverse3.len());

    // Send zero force to the first Inverse3
    if let Some(inv) = state.inverse3.first() {
        device.update_force(vec![ForceInput {
            device_id: inv.device_id.clone(),
            forces: Force { x: 0.0, y: 0.0, z: 0.0 },
        }], Some(true), Some(true)).await?;
    }

    device.shutdown().await?;
    Ok(())
}

Examples

Run any example with cargo run --example <name>. Each example has a const USE_HTTP: bool toggle to switch between HTTP and WebSocket paths.

Example Description Flags
device_basics Device discovery, state streaming, zero-force loop --http-only, --stream, --zero-force, --probe <ID>
haptic_sample Force feedback from virtual sphere + cube objects
profile_demo Set and verify session profile naming
ping_demo WS keepalive with live device discovery
navigation_demo Bubble navigation with cursor + workspace position streaming
damping_demo Damping filter with position/velocity/orientation streaming
events_demo Event channel polling (device connect/disconnect) POLL_RATE env var

All examples default to localhost:10001. For local development without hardware, start the included mock service first:

cd haply-service-mock && cargo run

API coverage

WebSocket

HaplyDevice provides convenience methods for all 3.5 WebSocket operations:

Category Methods
Force / Position update_force, update_position, update_angular_torques, update_angular_position
Probes probe_cursor_position, probe_orientation
Extension data update_extension_data
Device config configure_inverse3, configure_versegrip
Session config configure_session
Session send_force_full_render, send_ping
State read_state, force_read_full_state

For advanced use, construct Command enum variants directly and call update_command_msg + send_command for full control over frame composition.

HTTP

InverseHttpClient covers the full 3.5 REST API:

Category Endpoints
System get_version, get_expert_status, get_devices, get_devices_for_session
Sessions get_sessions, get_session, get/set/delete_session_profile
Settings get_settings, set_settings, get/set/delete_setting
Device config handedness, torque_scaling, gravity_compensation, home_return
Session-scoped basis, mount (+PATCH), preset, state_transform (+PATCH)
Filters filters_damping, filters_force_gate
Navigation get/set/delete_navigation
Grip pairing get/set/delete_paired_with
Utility reset_port, save_configuration

Session-scoped HTTP routes use ?session=<expr> selectors (for example :-1, #5, :default:0) as the canonical crate format. Some service compatibility paths may also accept session_id, but this crate treats session selectors as the primary interface.

Verse grip duplicate handling

Service 3.5 reports certain devices (e.g. the Ruko controller) in both wireless_verse_grip and custom_verse_grip with the same device ID. The wireless view includes Ruko-specific fields (trigger, wheel, directional buttons), while the custom view includes extension_data and generic a/b/c buttons.

By default, duplicates are resolved by keeping the custom view. You can change this at construction time:

use haply::{HaplyDevice, VerseGripDuplicateMode};

// Default: keep custom_verse_grip, hide wireless duplicates
let device = HaplyDevice::new("http://localhost:10001", "ws://localhost:10001/").await?;

// Keep wireless_verse_grip instead (exposes trigger, wheel, Ruko buttons)
let device = HaplyDevice::with_options(
    "http://localhost:10001",
    "ws://localhost:10001/",
    VerseGripDuplicateMode::PreferWireless,
).await?;

// Keep both views (no deduplication)
let device = HaplyDevice::with_options(
    "http://localhost:10001",
    "ws://localhost:10001/",
    VerseGripDuplicateMode::KeepBoth,
).await?;

The same option is available on the standalone HTTP client:

use haply::{http::InverseHttpClient, VerseGripDuplicateMode};

let http = InverseHttpClient::with_options(
    "http://localhost:10001",
    VerseGripDuplicateMode::PreferWireless,
);

The chosen mode applies consistently across both WebSocket state updates and HTTP device queries.

Testing

cargo test              # all tests (unit + integration)

cargo test --lib        # unit tests only

cargo test --test integration_test  # integration tests against mock

The test suite includes serialization round-trips, wire format golden tests, command building, state merging, and end-to-end integration tests against the included mock service.

Profiling (Tracy)

Tracy instrumentation is available behind the tracy feature. Existing log calls are forwarded into Tracy through a tracing bridge in the test_tracy example which recreates the same haptics as in haptic_sample.

cargo run --example test_tracy --features tracy

Project structure

src/
  device.rs               HaplyDevice high-level API
  state.rs                State management + command building
  device_model/           Wire types (devices, commands, enums, primitives, navigation)
  interfaces/http/        HTTP client (system, sessions, settings, device config)
  interfaces/websocket.rs WebSocket client
  interfaces/events.rs    Event channel
  physics.rs              Demo physics helpers (Sphere, Cube)
haply-service-mock/       Mock service for local development
examples/                 8 example apps
tests/                    Integration tests

Coverage gaps

The following 3.5 service features are intentionally not implemented:

  • v3.0 legacy endpoints (GET /3.0/sessions, deprecated POST /force_scale, etc.) -- use the current v3.1/3.5 equivalents
  • HTTP dry-run (execute: false on config POST bodies) -- use the WebSocket execute field instead
  • Typed selector builders -- device and session selectors are passed as &str, not typed enums

Compatibility

Breaking change: Starting with release 1.0.0, the crate targets Haply Inverse Service v3.5+ exclusively. Outgoing commands use 3.5 wire field names (vector instead of values, probe_position instead of probe_cursor_position) and are not compatible with pre-3.5 services. If you need to target an older service version, pin to a prior release of this crate from before 1.0.0.

  • Incoming payloads from older services still deserialize correctly (all 3.5 fields default to None).

Changelog

See CHANGELOG.md for release history.

License

Dual-licensed under MIT or Apache-2.0.