Skip to main content
Version: 3.5.x

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.
Modules

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_position for inverse3
  • probe_orientation for 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.

You may not need probe commands

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, and status.
  • Regular State Update messages include device state and status.

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:

MapPurposeFrequency
configureOne-shot persistent config: preset, basis, mount, filters, module settingsSend once or on change
commandsPer-tick, non-persistent: force, position, torquesMust 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 case

set_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_centered puts (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_id
  • config
  • state
  • status

See: Example: Initial Inventory Payload

State Update (server → client)

Sent once for each client message.

Each entry includes:

  • device_id
  • state
  • status

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-ZY
  • YZX means your Y is Haply's right, your Z is Haply's forward, your X is Haply's up.

Left-handed Z-up example (Unreal, X-YZ):

{
"session": {
"configure": {
"basis": { "permutation": "X-YZ" }
}
}
}
Migrating from session.set_basis

The 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 }
}
}
}
}
]
}
Verse grip / wireless verse grip

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 }
}
}
}
}
]
}
Mount schema asymmetry

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
}
}
]
}