haply
Rust client library for the Haply Inverse Service (v3.5+), covering ~98% of the WebSocket and HTTP APIs for controlling Haply haptic devices.
[]
= "0.8"
= { = "1", = ["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, andTS(ts-rs) for TypeScript generation
Quick start
use HaplyDevice;
use ;
async
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:
&&
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 ;
// Default: keep custom_verse_grip, hide wireless duplicates
let device = new.await?;
// Keep wireless_verse_grip instead (exposes trigger, wheel, Ruko buttons)
let device = with_options.await?;
// Keep both views (no deduplication)
let device = with_options.await?;
The same option is available on the standalone HTTP client:
use ;
let http = with_options;
The chosen mode applies consistently across both WebSocket state updates and HTTP device queries.
Testing
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.
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, deprecatedPOST /force_scale, etc.) -- use the current v3.1/3.5 equivalents - HTTP dry-run (
execute: falseon config POST bodies) -- use the WebSocketexecutefield 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 (
vectorinstead ofvalues,probe_positioninstead ofprobe_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.