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— boolean
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
Source: Python · C++ · C++ Glaze
Related: Types (quaternion) · Control Commands (probe_orientation) · WebSocket Protocol · Tutorial 03 (Wireless VG)