02. Print VerseGrip
Streams orientation (quaternion + Z-X-Y Euler angles), hall sensor level, and button state from the first wired VerseGrip.
What you'll learn:
- Reading
quaternionorientation from the state frame - Converting a quaternion to Z-X-Y Euler angles in degrees (+X right, +Y forward, +Z up)
- Using
probe_orientationas a standalone-observer keepalive - The first-message-only handshake pattern (same as tutorial 01)
Workflow
- Open a WebSocket to
ws://localhost:10001and wait for the first state frame. - Pick the first wired VerseGrip's
device_idfrom theverse_griparray. - Build a request with the session profile and a per-device
probe_orientationkeepalive (an empty-object command that keeps grip orientation flowing in state frames). - Send the request, then strip the
sessionfield — it's a one-shot handshake. - On every later frame, convert the quaternion to Euler angles and print throttled telemetry. Resend the keepalive each tick.
Parameters
| Name | Default | Purpose |
|---|---|---|
URI | ws://localhost:10001 | Simulation channel WebSocket URL |
PRINT_EVERY_MS | 100 | Console-output throttle |
| Session profile name | co.haply.inverse.tutorials:print-verse-grip | Identifies this simulation in Haply Hub |
The conversion is intrinsic Z-X-Y (yaw → pitch → roll) in the application frame +X right, +Y forward, +Z up. Do not use glm::eulerAngles — it follows a different convention and will read wrong here. All three language variants implement the same math; see the sources for the formula.
probe_orientation is actually neededprobe_orientation is only useful when your session doesn't send any command to an Inverse3. As soon as you command an Inverse3 (force, position, torque...), the service automatically streams the paired VerseGrip's orientation in every state frame — no probe needed. Use probe_orientation only for standalone grip-monitoring tools like this tutorial.
State fields read
From data.verse_grip[0].state:
orientation—quaternion(w, x, y, z)hall— integer hall-sensor readingbutton— booleantransform.rotation— workspace rotation (quaternion); position and scale are not applicable for an orientation-only device and are never emitted
transform.rotation is omitted when it equals the identity quaternion {w:1,x:0,y:0,z:0}. Supply a default when reading. Enable serialization/explicit_fields to always receive it.
Send / receive
The WebSocket loop: receive a state frame, build and send back the handshake + probe_orientation keepalive. The first outgoing message carries the session profile; every subsequent frame carries only the keepalive.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Single async loop — recv() → build command → send() → repeat.
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["verse_grip"][0]["device_id"]
request_msg = {
"session": {"configure": {"profile": {
"name": "co.haply.inverse.tutorials:print-verse-grip"}}},
"verse_grip": [{
"device_id": device_id,
"commands": {"probe_orientation": {}} # empty — keepalive
}]
}
await websocket.send(json.dumps(request_msg))
request_msg.pop("session", None) # one-shot handshake
libhv drives the WebSocket on its I/O thread — per-frame work lives in ws.onmessage. The main thread blocks on ENTER.
ws.onmessage = [&](const std::string &msg) {
const json data = json::parse(msg);
if (first_message) {
first_message = false;
device_id = data["verse_grip"][0].at("device_id").get<std::string>();
request_msg = {
{"session", {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:print-versegrip"}}}}}}},
{"verse_grip", json::array({
{{"device_id", device_id},
{"commands", {{"probe_orientation", json::object()}}}},
})},
};
}
ws.send(request_msg.dump());
request_msg.erase("session"); // one-shot handshake
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
Same libhv callback model. Typed structs replace nlohmann::json — probe_orientation_cmd is an empty struct (Glaze writes it as {}). std::optional<session_cmd> handles the one-shot handshake: .reset() after the first send omits it from subsequent JSON output.
// Struct models
struct quat { float w{1.0f}, x{}, y{}, z{}; };
struct grip_state { quat orientation{}; bool button{}; uint8_t hall{}; };
struct grip_device { std::string device_id; grip_state state; };
struct devices_message { std::vector<grip_device> verse_grip; };
struct probe_orientation_cmd {}; // empty object on the wire
struct commands_message {
std::optional<session_cmd> session; // one-shot — omitted when unset
std::vector<device_commands> verse_grip;
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
if (first_message) {
first_message = false;
out_cmds.session = session_cmd{ /* profile = print-versegrip */ };
device_commands dc{ .device_id = data.verse_grip[0].device_id };
dc.commands.probe_orientation = probe_orientation_cmd{};
out_cmds.verse_grip.push_back(std::move(dc));
}
std::string out_json;
(void)glz::write_json(out_cmds, out_json);
ws.send(out_json);
out_cmds.session.reset(); // one-shot handshake
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
Command-line flags (Python)
The Python variant accepts the same two flags as tutorial 01:
--full— pretty-prints the raw JSON payload instead of the one-line summary.--query-config— re-injectssession.force_render_full_stateon every outbound tick so every frame carries theconfigblock (device type, firmware, mount transform, …) instead of just the state delta. Useful when debugging settings changes live.
Both flags combine. The C++ variants do not expose these flags.
Tutorial 02 is also installed locally with the SDK — look in tutorials/02-haply-inverse-print-verse-grip/ under the service install directory.
Source: Python · C++ · C++ Glaze
Related: Types (quaternion) · Control Commands (probe_orientation) · WebSocket Protocol · Tutorial 03 (Wireless VG)