Skip to main content
Version: 3.5.x

04. Hello Floor

Your first haptic effect: a virtual horizontal floor that pushes back when the cursor presses into it. The force is a simple penalty spring — stiffness × penetration_depth — applied along Z via set_cursor_force.

What you'll learn:

  • Using set_cursor_force to apply a penalty-spring force
  • Reading cursor_position and computing force in real time
  • (Python) Setting a workspace preset (arm_front_centered) so the origin sits at workspace centre
  • (Python) Interactive keyboard tuning of floor height and stiffness via the keyboard package

Workflow

  1. Open a WebSocket to ws://localhost:10001 and wait for the first state frame.
  2. On the first frame: register the session profile. The Python variant additionally sends configure.preset: arm_front_centered so the origin sits at the middle of the workspace; the C++ variants use whatever configuration is already active on the device.
  3. On every frame: read cursor_position.z, compute force_z = max(0, (floor_pos - z) * stiffness), and send it back as a set_cursor_force command.
  4. Subsequent ticks send only the force command — the session profile is a one-shot handshake.
  5. (Python) Each tick also polls keyboard arrows and updates floor_pos / stiffness live.

Parameters

NameDefaultPurpose
floor_pos0.10 mZ-coordinate of the virtual floor plane
stiffness1000 N/mSpring constant (1 mm penetration → 1 N)
PRINT_EVERY_MS100–200Telemetry throttle
Session profile nameco.haply.inverse.tutorials:hello-floorIdentifies this simulation in Haply Hub
Interactive (Python only)

The Python variant uses the keyboard package (elevated privileges required on Linux):

  • / — raise / lower the floor plane
  • / — decrease / increase stiffness
  • R — reset to defaults
Extending the force

Forces are additive on the service tick — summed across sources before being sent to the device. A tutorial like this can coexist with other force producers without either blocking the other.

State fields read

From data.inverse3[i].state:

  • cursor_position.zvec3, used to compute penetration depth
  • current_cursor_force — reported for telemetry

Send / receive

Each tick: read the cursor's Z, compute force_z = max(0, (floor_pos - z) * stiffness), and send a set_cursor_force. The first outgoing message also carries the session profile (all variants) and, for Python, configure.preset: arm_front_centered.

Single async loop. Handshake on first frame carries profile + configure.preset so floor_pos = 0.1 aligns with workspace-centre coordinates.

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

if first_message:
first_message = False
device_id = data["inverse3"][0]["device_id"]
# Handshake: profile + preset (one-shot)
request_msg = {
"session": {"configure": {"profile": {"name": "co.haply.inverse.tutorials:hello-floor"}}},
"inverse3": [{
"device_id": device_id,
"configure": {"preset": {"preset": "arm_front_centered"}}
}]
}
else:
# Per tick: compute force along Z, send set_cursor_force
z = data["inverse3"][0]["state"]["cursor_position"]["z"]
force_z = 0.0 if z > floor_pos else (floor_pos - z) * stiffness
request_msg = {
"inverse3": [{
"device_id": device_id,
"commands": {"set_cursor_force":
{"vector": {"x": 0.0, "y": 0.0, "z": force_z}}}
}]
}

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

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

Related: Control Commands (set_cursor_force) · Mount & Workspace (presets) · Types (vec3) · Sessions · Tutorial 07 (Basis & Mount)