Skip to main content
Version: 3.5.x

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:

BlockContainsChanges
configFirmware info, preset, basis, mount, filtersRarely — only on explicit config change
statePosition, velocity, force, orientation, transformsEvery tick (high frequency)
statusReady, calibrated, power supply, in_useOccasionally (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:

MapPurposePersistence
configureOne-shot settings: preset, basis, mount, filters, module configRemembered until changed
commandsPer-tick control: force, position, torquesApplied 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 case

set_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

Don't probe if you're already sending commands

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.

Unknown keys are silently ignored

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.