WebSocket Protocol
The simulation channel is a WebSocket connection on port 10001. It carries the real-time command/state loop between your application and the service.
Core rule: one response per message
The service sends exactly one state update for each message it receives from your client:
There is no polling, no subscription, no out-of-band push. The cadence is entirely driven by your send rate.
Step 1 — Initial inventory (first message from service)
On connect, the service sends a full snapshot containing config, state,
and status for every detected device:
{
"session_id": 7,
"inverse3": [
{
"device_id": "049D",
"config": {
"type": "inverse3",
"port": "COM12",
"device_info": { "major_version": 7, "minor_version": 4, "id": "049D", "device_type": 4, "uuid": "…" },
"extended_device_id": "…",
"extended_firmware_version": "…",
"gravity_compensation": { "enabled": true, "scaling_factor": 1.0 },
"handedness": "right",
"streaming_mode": "USB",
"torque_scaling": { "enabled": true },
"home_return": { "enabled": false },
"preset": "defaults",
"basis": { "permutation": "XYZ" },
"mount": { "position": { "x": 0, "y": 0, "z": 0 }, "rotation": { "w": 1, "x": 0, "y": 0, "z": 0 }, "scale": { "x": 1, "y": 1, "z": 1 } },
"filters": {
"force_gate": { "gain": 0.3 },
"damping": { "scalar": 0.0 }
}
},
"state": {
"cursor_position": { "x": 0.0, "y": -0.12, "z": 0.15 },
"cursor_velocity": { "x": 0.0, "y": 0.0, "z": 0.0 },
"angular_position": { "a0": 0.0, "a1": 0.0, "a2": 0.0 },
"angular_velocity": { "a0": 0.0, "a1": 0.0, "a2": 0.0 },
"body_orientation": { "w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0 },
"current_cursor_force": { "x": 0.0, "y": 0.0, "z": 0.0 },
"current_cursor_position": { "x": 0.0, "y": 0.0, "z": 0.0 },
"current_angular_torques": { "a0": 0.0, "a1": 0.0, "a2": 0.0 },
"current_angular_position": { "a0": 0.0, "a1": 0.0, "a2": 0.0 },
"control_domain": "cartesian",
"control_mode": "idle",
"transform": { "position": { "x": 0, "y": 0, "z": 0 }, "rotation": { "w": 1, "x": 0, "y": 0, "z": 0 }, "scale": { "x": 1, "y": 1, "z": 1 } },
"transform_velocity": { "position": { "x": 0, "y": 0, "z": 0 }, "rotation": { "w": 1, "x": 0, "y": 0, "z": 0 }, "scale": { "x": 0, "y": 0, "z": 0 } }
},
"status": {
"calibrated": true,
"in_use": false,
"power_supply": true,
"ready": true,
"started": true
}
}
],
"verse_grip": [],
"wireless_verse_grip": []
}
The three blocks have different update frequencies:
| Block | Contains | Changes |
|---|---|---|
config | Firmware info, preset, basis, mount, filters | Rarely — only on explicit config change |
state | Position, velocity, force, orientation, transforms | Every tick (high frequency) |
status | Ready, calibrated, power supply, in_use | Occasionally (low frequency) |
Step 2 — Send your first command
Parse the initial inventory to find your device ID, then send back a message containing your session profile, any initial configuration, and/or a control command:
{
"session": {
"configure": {
"profile": { "name": "co.haply.inverse.tutorials:hello-floor" }
}
},
"inverse3": [
{
"device_id": "049D",
"configure": {
"preset": { "preset": "arm_front_centered" }
},
"commands": {
"set_cursor_force": { "vector": { "x": 0.0, "y": 0.0, "z": 0.0 } }
}
}
]
}
Step 3 — Subsequent state updates
After the first exchange, the service only sends state + status (no
config) unless a configuration change triggers a full snapshot push:
{
"session_id": 7,
"inverse3": [
{
"device_id": "049D",
"state": { "cursor_position": { … }, "cursor_velocity": { … }, … },
"status": { "ready": true, "calibrated": true, … }
}
]
}
Step 4 — Repeat
Send a command → receive a state update. The loop continues at your send rate.
configure vs commands
Each device entry in your outgoing message can contain two maps:
| Map | Purpose | Persistence |
|---|---|---|
configure | One-shot settings: preset, basis, mount, filters, module config | Remembered until changed |
commands | Per-tick control: force, position, torques | Applied once, then forgotten |
See Session Configuration for the
full list of configure keys, and Control Commands for
the commands entries.
set_transform is a special caseset_transform lives under commands but is persistent — the service
keeps the last value until you send a new one. This is because its purpose is
scene navigation, where you stream transforms during camera motion but want the
last position to stick between presses.
The execute flag
Any configure or commands entry can include "execute": false to make the
service parse and validate the payload without applying it.
{
"inverse3": [{
"device_id": "049D",
"configure": {
"preset": { "preset": "arm_front", "execute": true },
"damping": { "scalar": 0.0, "execute": false }
}
}]
}
This is useful for reflection-based serializers (e.g., Unity's JsonUtility)
that always emit all fields: set unused entries to execute: false so they
don't overwrite real config. The default is true — omitting the flag means
"apply normally".
Warnings
probe_position and probe_orientation are monitoring-only keepalives for
sessions that don't send control commands (e.g., Haply Hub). If your session is
already sending set_cursor_force, set_cursor_position, etc., don't also
send probes — the device state is already in every response. Mixing both
wastes bandwidth and may trigger session-probe-dropped events.
If a command seems to have no effect, the field name is probably wrong. The service currently ignores unrecognised JSON keys without error. Check the service logs and verify field names against the API Reference. This behaviour is planned to change in a future version.