Simulation Channel
Overview
The Simulation Channel is a high-frequency, bidirectional WebSocket channel used to exchange device state and send session or device commands.
Default URL: ws://localhost:10001
The port can be changed in the configuration.
Core contract (important):
- When a client connects, the service sends an initial inventory message containing the complete list of devices.
- After that, the server sends exactly one State Update message for each message received from the client.
- Each State Update contains the state (and status) of all devices.
This page documents the core Simulation Channel protocol and commands.
Additional modules/features can register and extend the system with their own commands and/or state fields. Those are documented separately in the modules section.
Protocol Rules
One response per client message
The service emits one State Update message containing the state of all devices for each message received from the client.
This means:
- If you need a fresh snapshot of device state, you must send something (a session command, a probe command, or a device command).
- The channel behaves like a "tick" loop driven by client messages.
Polling without applying forces (probe commands)
If you want to observe state changes without applying forces or changing simulation parameters, use probing commands:
probe_positionforinverse3probe_orientationfor Verse grips
Probing commands contain no command data and simply force device information queries so positional/orientation data is up to date on the next State Update.
Simulation sessions that are already sending control commands (set_cursor_force, set_cursor_position, etc.) do not need to send probe commands -- the device state is always included in the response frame. Probes are intended for monitoring-only sessions (e.g. Haply Hub) that need to poll device state without driving the device.
Configuration vs state
- The Initial Inventory message includes device
config,state, andstatus. - Regular State Update messages include device
stateandstatus.
If you need a snapshot including configuration again, use session.force_render_full_state.
configure vs commands
Device messages support two separate maps: configure and commands. They serve different purposes:
| Map | Purpose | Frequency |
|---|---|---|
configure | One-shot persistent config: preset, basis, mount, filters, module settings | Send once or on change |
commands | Per-tick, non-persistent: force, position, torques | Must be sent every tick |
Entries in commands (set_cursor_force, set_cursor_position, set_angular_torques, set_angular_position) are applied once per tick and not stored -- if you stop sending them the effect immediately ceases and the device returns to idle. Use configure for anything that should persist across ticks without being re-sent.
set_transform is a special caseset_transform lives under commands but is persistent — the service keeps
the last transform you sent until you send a new one. You don't have to ship
it every tick. That said, because its purpose is camera / scene navigation,
streaming it at tick rate (e.g. one transform per frame when the user pans the
scene) is the expected usage pattern for continuous-motion navigation.
Coordinate Systems
Haply uses a right-handed coordinate system with Z-Up by default.
Two configure entries affect how coordinates are interpreted and returned:
session.configure.basis: changes the mapping between Haply coordinates and your application's coordinate system (see Basis below).inverse3[*].configure.preset: selects a named factory configuration that places the workspace origin (e.g.arm_front_centeredputs(0, 0, 0)at the workspace centre). See the per-device Configure section.
Message Formats
This section describes the high-level envelope and message types. Full examples are provided later in this document.
Device groups
Messages are grouped by device type at the top level (e.g. inverse3, verse_grip, wireless_verse_grip). Each device type key maps to an array of per-device objects.
Initial Inventory (server → client)
Sent once immediately after a WebSocket connection is established.
Each entry includes:
device_idconfigstatestatus
See: Example: Initial Inventory Payload
State Update (server → client)
Sent once for each client message.
Each entry includes:
device_idstatestatus
See: Example: State Update Payload
Session command envelope (client → server)
Session commands are actions that apply to the current connection/session and are not device-specific.
{
"session": {
"<command_name>": {
"...": "..."
}
}
}
Device command envelope (client → server)
Device commands are sent under the device type key as an array, allowing commands to be sent to one or more devices in a single message. Each device entry supports both a configure map and a commands map.
{
"<device_type>": [
{
"device_id": "<id>",
"configure": {
"<config_key>": { "...": "..." }
},
"commands": {
"<command_name>": { "...": "..." }
}
}
]
}
You can include multiple entries to command multiple devices of the same type in a single message.
Note that commands is a dictionary and can contain multiple commands for a given device, but only one per command type.
The execute field
Every command or configure entry accepts an optional "execute" boolean (default true). Set "execute": false to have the entry parsed but not applied. This is useful for reflection-based serializers (e.g. Unity JsonUtility) that always emit every field.
"set_cursor_force": { "execute": false, "vector": { "x": 0.0, "y": 0.0, "z": 0.0 } }
Command Reference
Session
Force Render Full State
Request a snapshot of all device states and configurations.
{
"session": {
"force_render_full_state": {}
}
}
Session Configure
Session-level persistent configuration is sent via session.configure. This mirrors the device-level configure map pattern.
Profile
Sets the session profile name — used by Haply Hub to recognise the simulation and persist per-app device tweaks.
{
"session": {
"configure": {
"profile": {
"name": "my_profile"
}
}
}
}
Basis (session-level)
Sets the basis mapping for the entire session. The basis mapping describes how Haply coordinates are transformed into your application's coordinate system; after it is set, all device states are returned in that basis and all values you send are interpreted in it.
The permutation string is expressed relative to Haply's right-handed / Z-up
coordinate system — a permutation of X, Y, Z, optionally prefixed by
+ or -. Examples:
XYZ,ZYX,+Y-Z+X,X-ZYYZXmeans yourYis Haply's right, yourZis Haply's forward, yourXis Haply's up.
Left-handed Z-up example (Unreal, X-YZ):
{
"session": {
"configure": {
"basis": { "permutation": "X-YZ" }
}
}
}
session.set_basisThe axis-sign convention changed between the legacy session.set_basis and
session.configure.basis. A permutation that worked under the old command
may produce an inverted transformation under the new one — negate the axis
signs where needed (e.g. X-ZY becomes XZ-Y).
All devices commands
Probing (all devices)
Use probing commands to request up-to-date positional/orientation information without applying forces or other simulation changes.
- inverse3:
probe_position - Verse grips (
verse_grip,wireless_verse_grip):probe_orientation
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"probe_position": {}
}
}
],
"verse_grip": [
{
"device_id": "049D",
"commands": {
"probe_orientation": {}
}
}
],
"wireless_verse_grip": [
{
"device_id": "049D",
"commands": {
"probe_orientation": {}
}
}
]
}
Set Transform (all devices)
Set the workspace transform (device-space to application-space) for a device.
Unlike the other commands entries, set_transform is persistent — the
service keeps the last value you sent until you send a new one. You don't need
to ship it every tick, but because its purpose is camera / scene navigation,
streaming it at tick rate is the expected usage pattern for continuous motion
(e.g. one transform per frame while the user pans the scene).
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_transform": {
"transform": {
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0 },
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 }
}
}
}
}
]
}
For Verse grip and Wireless Verse grip devices, only the rotation component
of the transform has an effect — the grip reports orientation only, so there
is nothing for position or scale to scale or translate. The position and
scale fields are accepted (and echoed back in the snapshot) purely for schema
consistency with Inverse3; leaving them at the identity values
(position = {0,0,0}, scale = {1,1,1}) is the standard practice.
Inverse3 device configure
Configuration entries are sent in the configure map. They are persistent -- send once or on change.
Preset
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"preset": { "preset": "arm_front_centered" }
}
}
]
}
Available values: device_defaults, arm_front, arm_front_centered, led_front, led_front_centered, custom.
Basis (device-level)
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"basis": { "permutation": "ZXY" }
}
}
]
}
Mount
Sets the device mount transform. Note the transform wrapper inside mount.
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"mount": {
"transform": {
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0 },
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 }
}
}
}
}
]
}
The command side (client to server) wraps the transform: "mount": { "transform": { ... } }.
The snapshot side (server to client) is flat: "mount": { "position": {...}, "rotation": {...}, "scale": {...} }.
This is intentional -- commands use a unified wrapper, while snapshots serialize the transform directly. Be careful not to copy the snapshot shape into a command payload.
Damping
Controls uniform and/or directional damping. At least one field must be present.
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"damping": { "scalar": 0.5 }
}
}
]
}
You can also set directional damping, or both at once:
"damping": { "scalar": 0.5, "vector": { "x": 0.0, "y": 1.0, "z": 0.0 } }
Force Gate
Sets force gate attenuation gain (0.0 to 1.0).
{
"inverse3": [
{
"device_id": "049D",
"configure": {
"force_gate": { "gain": 0.3 }
}
}
]
}
Inverse3 commands
To send commands to an inverse3 device, include an entry with a matching device_id under the inverse3 key. Commands are per-tick and must be resent each tick to remain active.
Command one device:
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}
Command multiple devices in one message:
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
},
{
"device_id": "049E",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}
Set Cursor Position
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_position": {
"position": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}
Set Cursor Force
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}
Set Angular Position
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_angular_position": {
"angles": {
"a0": 1.0,
"a1": 2.0,
"a2": 3.0
}
}
}
}
]
}
Set Angular Torques
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_angular_torques": {
"torques": {
"a0": 1.0,
"a1": 2.0,
"a2": 3.0
}
}
}
}
]
}
Verse Grip configure
Verse Grip and Wireless Verse Grip devices support the same configure keys as Inverse3 for preset, basis, and mount.
Extended Verse Grip
The set_extension_data command is part of the extended protocol for Verse grips, used with grip versions that implement the board extension communication protocol.
Set Grip Extension Data
Supported data lengths:
- Up to 20 bytes upstream (client → device).
- Up to 12 bytes downstream (device → client), returned in the State Update message as
state.extension_data.
Data specification:
- Array length: 20 bytes
- Value range: each value is 0–255
{
"wireless_verse_grip": [
{
"device_id": "049D",
"commands": {
"set_extension_data": {
"extension_data": [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
}
}
}
]
}
Examples
Example: Initial Inventory Payload
The service sends a message containing the complete device list when a WebSocket is connected. The initial message has the following JSON format:
{
"inverse3": [
{
"device_id": "04BA",
"config": {
"type": "inverse3",
"port": "COM13",
"device_info": {
"major_version": 7,
"minor_version": 1,
"id": "04BA",
"device_type": 4,
"uuid": "2D35F80DD9005F599B68F49944CB04BA"
},
"extended_device_id": "2D35F80DD9005F599B68F49944CB04BA",
"extended_firmware_version": "8C20FDC8010AA1E15AA133CDA2534874",
"gravity_compensation": {
"enabled": true,
"scaling_factor": 1
},
"handedness": "right",
"streaming_mode": "USB",
"torque_scaling": {
"enabled": true
},
"home_return": {
"enabled": false
},
"filters": {
"force_gate": { "gain": 0.3 },
"damping": { "scalar": 0 }
},
"preset": "defaults",
"basis": { "permutation": "XYZ" },
"mount": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
}
},
"state": {
"cursor_position": {
"x": 0.07842738,
"y": -0.14836666,
"z": 0.14297646
},
"cursor_velocity": {
"x": -0.011969013,
"y": 0.0012009288,
"z": -0.043197
},
"angular_position": {
"x": -69.31704,
"y": 137.62952,
"z": 19.832787
},
"angular_velocity": {
"x": 0,
"y": 0,
"z": 0
},
"body_orientation": {
"x": -0.01940918,
"y": 0.7026367,
"z": 0.00048828125,
"w": 0.7113037
},
"current_cursor_force": { "x": 0, "y": 0, "z": 0 },
"current_cursor_position": { "x": 0, "y": 0, "z": 0 },
"current_angular_torques": { "x": 0, "y": 0, "z": 0 },
"current_angular_position": { "x": 0, "y": 0, "z": 0 },
"control_domain": "cartesian",
"control_mode": "force",
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"calibrated": false,
"in_use": false,
"power_supply": true,
"ready": true,
"started": true
}
}
],
"verse_grip": [
{
"device_id": "61548",
"config": {
"type": "verse_grip",
"port": "COM3",
"preset": "device_defaults",
"basis": { "permutation": "XYZ" },
"mount": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
}
},
"state": {
"button": false,
"hall": 0,
"orientation": {
"x": -0.5019531,
"y": 0.8632202,
"z": -0.048095703,
"w": -0.022338867
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"error": 0,
"ready": true
}
}
],
"wireless_verse_grip": [
{
"device_id": "0",
"config": {
"type": "wireless_verse_grip",
"port": "COM6",
"major_version": 1,
"minor_version": 4,
"hardware_version": 1,
"streaming_mode": "Radio",
"preset": "device_defaults",
"basis": { "permutation": "XYZ" },
"mount": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
}
},
"state": {
"battery_level": 0.816,
"battery_voltage": 3.77,
"buttons": {
"a": false,
"b": false,
"c": false
},
"hall": 16,
"orientation": {
"x": -0.019866943,
"y": -0.017486572,
"z": 0.05508423,
"w": -0.9963989
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"connected": true,
"awake": true,
"ready": true
}
}
]
}
Example: State Update Payload
The service will send one state update message containing the state of all devices for each message received.
If you wish to know the state of the machine, you must send it a message beforehand (for example a probe command or a force value, even if the values are zeros). This is particularly important when using devices as input sources (e.g., tracking position) without applying forces.
The state update message has the following JSON format:
{
"inverse3": [
{
"device_id": "04BA",
"state": {
"cursor_position": {
"x": 0.07842738,
"y": -0.14836666,
"z": 0.14297646
},
"cursor_velocity": {
"x": -0.011969013,
"y": 0.0012009288,
"z": -0.043197
},
"angular_position": {
"x": -69.31704,
"y": 137.62952,
"z": 19.832787
},
"angular_velocity": {
"x": 0,
"y": 0,
"z": 0
},
"body_orientation": {
"x": -0.01940918,
"y": 0.7026367,
"z": 0.00048828125,
"w": 0.7113037
},
"current_cursor_force": { "x": 0, "y": 0, "z": 0 },
"current_cursor_position": { "x": 0, "y": 0, "z": 0 },
"current_angular_torques": { "x": 0, "y": 0, "z": 0 },
"current_angular_position": { "x": 0, "y": 0, "z": 0 },
"control_domain": "cartesian",
"control_mode": "force",
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"calibrated": false,
"in_use": false,
"power_supply": true,
"ready": true,
"started": true
}
}
],
"verse_grip": [
{
"device_id": "61548",
"state": {
"button": false,
"hall": 0,
"orientation": {
"x": -0.5019531,
"y": 0.8632202,
"z": -0.048095703,
"w": -0.022338867
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"error": 0,
"ready": true
}
}
],
"wireless_verse_grip": [
{
"device_id": "0",
"state": {
"battery_level": 0.816,
"battery_voltage": 3.77,
"buttons": {
"a": false,
"b": false,
"c": false
},
"hall": 16,
"orientation": {
"x": -0.019866943,
"y": -0.017486572,
"z": 0.05508423,
"w": -0.9963989
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"connected": true,
"awake": true,
"ready": true
}
}
],
"custom_verse_grip": [
{
"device_id": "0",
"state": {
"battery_level": 0.816,
"battery_voltage": 3.77,
"buttons": {
"a": false,
"b": false,
"c": false
},
"hall": 16,
"orientation": {
"x": -0.019866943,
"y": -0.017486572,
"z": 0.05508423,
"w": -0.9963989
},
"extension_data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"ready": true
}
}
]
}