# haply
[](https://crates.io/crates/haply)
[](https://crates.io/crates/haply)
Rust client library for the [Haply Inverse Service](https://www.haply.co/) (v3.5+), covering ~98% of the WebSocket and HTTP APIs for controlling Haply haptic devices.
```toml
[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 30+ tunable parameters (shape, springs, damping, workspace bounding, avatar boundaries)
- **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](https://crates.io/crates/ts-rs)) for TypeScript generation
## Quick start
```rust
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.
| `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:
```sh
cd haply-service-mock && cargo run
```
## API coverage
### WebSocket
`HaplyDevice` provides convenience methods for all 3.5 WebSocket operations:
| 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:
| 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` |
## 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:
```rust
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:
```rust
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
```sh
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`.
```sh
cargo run --example test_tracy --features tracy
```
## Project structure
```text
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](CHANGELOG.md) for release history.
## License
Dual-licensed under MIT or Apache-2.0.