Skip to main content
Version: 3.5.x

06. Combined (Inverse3 + Wireless VerseGrip)

Two-device tutorial: point the grip and hold a button to drive the Inverse3 cursor in that direction. The cursor is clamped inside a spherical workspace.

What you'll learn:

  • Reading two device types in the same state frame (inverse3 and wireless_verse_grip)
  • Extracting the grip's pointing direction from its quaternion (local +Y axis)
  • Using set_cursor_position to drive the cursor toward a computed target
  • Clamping the target to a safe workspace sphere — Minverse uses a smaller radius than Inverse3
  • Setting a workspace preset (arm_front_centered) so the origin sits at the middle of reach

Workflow

  1. Discover both devices:
    • C++ variants query GET /devices over HTTP at startup, then print a calibration prompt and wait for ENTER.
    • Python reads both device IDs from the first WebSocket state frame.
  2. Register the session profile and set configure.preset: arm_front_centered on the first message (one-shot handshake).
  3. Each tick: read the grip's orientation and buttons.{a, b} state.
  4. If a motion button is held, compute the grip's world-space direction (R(q) · ĵ — the rotated unit +Y axis) and accumulate it into the target position scaled by SPEED.
  5. Clamp the target inside the workspace sphere and send it via set_cursor_position.
  6. (Python only) Adapt the sphere radius from the device's config.typeminverse = 0.04 m, everything else = 0.10 m.

Parameters

NameDefaultPurpose
SPEED0.01 m/tickMotion step while a button is held
RADIUS_INVERSE30.10 mWorkspace clamp radius for Inverse3 / Inverse3x
RADIUS_MINVERSE0.04 mWorkspace clamp radius for Minverse (Python only — C++ hardcodes 0.10)
PRINT_EVERY_MS200Telemetry throttle
Session profile nameco.haply.inverse.tutorials:combinedIdentifies this simulation in Haply Hub
Calibrate before running
  • Let the Inverse3 self-calibrate (or place the grip on the inkwell and wait for the LED to turn solid).
  • Detach the grip from the inkwell.
  • Hold A or B and rotate the grip — the cursor moves along the direction the grip is pointing.

State fields read

From the per-tick state frame:

  • data.inverse3[0].state.cursor_positionvec3
  • data.wireless_verse_grip[0].state.orientationquaternion
  • data.wireless_verse_grip[0].state.buttons.{a, b, c} — booleans
  • (Python, first frame only) data.inverse3[0].config.type — selects Inverse3 vs Minverse radius
  • (Python, first frame only) data.inverse3[0].status.calibrated — prompts the user if false

Send / receive

The quaternion-to-direction math (rotate +Y by R(q)) and sphere clamp are classic linear algebra — see the source files. The Inverse-API side is the handshake + per-tick set_cursor_position.

Single async loop. Python reads both device IDs from the first state frame; the handshake attaches profile + configure.preset: arm_front_centered to the first set_cursor_position.

async with websockets.connect(URI) as websocket:
while True:
msg = await websocket.recv()
data = json.loads(msg)

if first_message:
first_message = False
inverse3_id = data["inverse3"][0]["device_id"]
grip_id = data["wireless_verse_grip"][0]["device_id"]
radius = get_workspace_radius(data["inverse3"][0].get("config", {}))

# Handshake: profile + preset + first position command
request_msg = {
"session": {"configure": {"profile": {"name": SLUG}}},
"inverse3": [{
"device_id": inverse3_id,
"configure": {"preset": {"preset": "arm_front_centered"}},
"commands": {"set_cursor_position": {"position": position}},
}],
}
else:
# Per tick: update position from grip pointing direction (classic math, not shown), send
request_msg = {
"inverse3": [{
"device_id": inverse3_id,
"commands": {"set_cursor_position": {"position": position}},
}],
}

await websocket.send(json.dumps(request_msg))

Source: Python · C++ · C++ Glaze

Related: Control Commands (set_cursor_position) · Types (quaternion, vec3) · Mount & Workspace (presets) · Tutorial 03 (Wireless VG) · Tutorial 05 (Position Control)