03. Print Wireless VerseGrip
Same pattern as tutorial 02 but for a Wireless VerseGrip — adds buttons (A/B/C) and battery level to the streamed state.
What you'll learn:
- Reading wireless-specific state fields:
buttons.{a,b,c},battery_level,hall - Using
probe_orientationas a standalone-observer keepalive - Same first-message-only handshake pattern as tutorial 02
Workflow
- Open a WebSocket to
ws://localhost:10001and wait for the first state frame. - Pick the first Wireless VerseGrip's
device_idfrom thewireless_verse_griparray. - Build a request with the session profile and a per-device
probe_orientationkeepalive. - 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 including button states and battery level. 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-wireless-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.wireless_verse_grip[0].state:
orientation—quaternion(w, x, y, z)hall— integer hall-sensor readingbuttons.a,buttons.b,buttons.c— booleansbattery_level— float (0.0 – 1.0)
Send / receive
Same shape as tutorial 02, just with the wireless_verse_grip device array. The WebSocket loop receives a state frame and sends 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.
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["wireless_verse_grip"][0]["device_id"]
request_msg = {
"session": {"configure": {"profile": {
"name": "co.haply.inverse.tutorials:print-wireless-verse-grip"}}},
"wireless_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
ws.onmessage = [&](const std::string &msg) {
const json data = json::parse(msg);
if (first_message) {
first_message = false;
device_id = data["wireless_verse_grip"][0].at("device_id").get<std::string>();
request_msg = {
{"session", {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:print-wireless-verse-grip"}}}}}}},
{"wireless_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
Typed structs. Note button_state is named explicitly (not buttons) to avoid shadowing the buttons field on wvg_state — Glaze maps fields by name, so the struct type name is free.
// Struct models
struct quat { float w{1.0f}, x{}, y{}, z{}; };
struct button_state { bool a{}, b{}, c{}; };
struct wvg_state {
quat orientation{};
uint8_t hall{};
button_state buttons{};
float battery_level{};
};
struct wvg_device { std::string device_id; wvg_state state; };
struct devices_message { std::vector<wvg_device> wireless_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> wireless_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;
request_msg.session = session_cmd{ /* profile = print-wireless-verse-grip */ };
device_commands dc{ .device_id = data.wireless_verse_grip[0].device_id };
dc.commands.probe_orientation = probe_orientation_cmd{};
request_msg.wireless_verse_grip.push_back(std::move(dc));
}
std::string out_json;
(void)glz::write_json(request_msg, out_json);
ws.send(out_json);
request_msg.session.reset(); // one-shot handshake
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
Source: Python · C++ · C++ Glaze
Related: Tutorial 02 (Wired VG) · Types (quaternion) · Control Commands (probe_orientation) · WebSocket Protocol