06. Kombiniert (Inverse3 + Wireless VerseGrip)
Anleitung mit zwei Geräten: Richten Sie den Griff aus und halten Sie eine Taste gedrückt, um den Cursor von Inverse3 in diese Richtung zu bewegen. Der Cursor ist auf einen kugelförmigen Arbeitsbereich beschränkt.
Was Sie lernen:
- Auslesen von zwei Gerätetypen aus demselben Zustandsrahmen (
inverse3undwireless_verse_grip) - Ermittlung der Zeigerichtung des Griffs anhand seiner Quaternion (lokale
+Y-Achse) - Verwendung von
set_cursor_position, um den Cursor auf ein berechnetes Ziel zu bewegen - Begrenzung des Zielpunkts auf eine sichere Arbeitsraumkugel – Minverse verwendet einen kleineren Radius als Inverse3
- Festlegen eines Arbeitsraum-Voreinstellungen (
arm_front_centered), damit der Ursprung in der Mitte des Bereichs liegt
Ablauf
- Beide Geräte erkennen:
- C++- Varianten werden beim Start über HTTP abgefragt
GET /devicesab, geben dann eine Kalibrierungsaufforderung aus und warten auf die ENTER-Taste. - Python liest beide Geräte-IDs aus dem ersten Status-Frame von WebSocket aus.
- C++- Varianten werden beim Start über HTTP abgefragt
- Registrieren Sie das Sitzungsprofil und fügen Sie in der ersten Nachricht
configure.preset: arm_front_centered(einmaliger Handschlag). - Bei jedem Zyklus: Lesen Sie die
orientationdes Griffs und den Zustand vonbuttons.{a, b}. - Wenn eine Bewegungstaste gedrückt gehalten wird, berechnen Sie die Richtung des Griffs im Weltkoordinatensystem (
R(q) · ĵ– die gedrehte Einheitsachse (+Y) und addieren Sie diese, skaliert mitSPEED, zur Zielposition. - Begrenzen Sie den Zielpunkt innerhalb der Arbeitsraumkugel und senden Sie ihn über
set_cursor_position. - (nur Python) Passen Sie den Kugelradius anhand von
config.typedes Geräts an –minverse= 0,04 m, alle anderen = 0,10 m.
Parameter
| Name | Standardwert | Zweck |
|---|---|---|
SPEED | 0.01 m/Tick | Schrittweite, solange eine Taste gedrückt gehalten wird |
RADIUS_INVERSE3 | 0.10 m | Begrenzungsradius des Arbeitsraums für „ Inverse3 “ / „Inverse3x“ |
RADIUS_MINVERSE | 0.04 m | Begrenzungsradius des Arbeitsraums für „ Minverse “ (nur Python – C++ ist fest auf 0.10 (codiert) |
PRINT_EVERY_MS | 200 | Einschränkung der Telemetrie |
| Name des Sitzungsprofils | co.haply.inverse.tutorials:combined | Kennzeichnet diese Simulation im „ Haply “-Hub |
- Lassen Sie „ Inverse3 “ sich selbst kalibrieren (oder legen Sie den Griff auf das Inkwell und warten Sie, bis die LED dauerhaft leuchtet).
- Nehmen Sie den Griff vom Tintenfass ab.
- Halten Sie A oder B gedrückt und drehen Sie den Griff – der Cursor bewegt sich in die Richtung, in die der Griff zeigt.
Ausgelesene Zustandsfelder
Aus dem Zustandsrahmen jedes Zyklus:
data.inverse3[0].state.cursor_position—vec3data.wireless_verse_grip[0].state.orientation—quaterniondata.wireless_verse_grip[0].state.buttons.{a, b, c}— Booleans- (Python, nur im ersten Rahmen)
data.inverse3[0].config.type— wählt den Radius für „ Inverse3 “ oder Minverse - (Python, nur im ersten Rahmen)
data.inverse3[0].status.calibrated— fordert den Benutzer auf, wenn der Wert „false“ ist
Senden / Empfangen
Die Berechnung der Richtung aus dem Quaternion (Drehung von +Y mit R(q)) und die Begrenzung auf die Kugel sind klassische lineare Algebra – siehe die Quelldateien. Die Inverse-API-Seite besteht aus dem Handshake und dem pro Zyklus gesendeten set_cursor_position.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Eine einzige asynchrone Schleife. Python liest beide Geräte-IDs aus dem ersten Statusrahmen aus; der Handshake hängt vom Profil und configure.preset: arm_front_centered an den ersten set_cursor_position-Aufruf an.
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))
C++ ermittelt beim Start beide Geräte-IDs über GET /devices (HTTP) und öffnet anschließend WebSocket. onmessage läuft im I/O-Thread von libhv; der Hauptthread wird blockiert, bis die ENTER-Taste gedrückt wird.
// Startup (synchronous):
const std::string inv3_device_id = get_first_device_id("inverse3");
const std::string grip_device_id = get_first_device_id("wireless_verse_grip");
// Per tick:
ws.onmessage = [&](const std::string &msg) {
json data = json::parse(msg);
// ... classic math (not shown): update local Inverse3State + WirelessVerseGripState,
// compute new position from grip orientation, clamp to sphere ...
json command;
command["inverse3"] = json::array();
command["inverse3"].push_back({
{"device_id", inv3_device_id},
{"commands", {{"set_cursor_position",
{{"position", {{"x", pos.x}, {"y", pos.y}, {"z", pos.z}}}}}}}
});
if (first_message) {
first_message = false;
command["session"] = {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:combined"}}}}}};
command["inverse3"][0]["configure"] = {
{"preset", {{"preset", "arm_front_centered"}}}};
}
ws.send(command.dump());
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
Das gleiche Callback-Modell wie bei libhv. Typisierte Strukturen bilden sowohl die Zustandsrahmen als auch den ausgehenden Befehl ab – zwei std::vector<> in devices_message ermöglichen es, mit einem einzigen glz::read beide Gerätetypen auszulesen.
// 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{}; };
struct wvg_device { std::string device_id; wvg_state state; };
struct inverse_state { vec3 cursor_position{}, cursor_velocity{}; };
struct inverse_device { std::string device_id; inverse_state state; };
struct devices_message {
std::vector<inverse_device> inverse3;
std::vector<wvg_device> wireless_verse_grip;
};
struct set_cursor_position_cmd { vec3 position; };
struct commands_message {
std::optional<session_cmd> session;
std::vector<device_commands> inverse3;
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
const auto &wvg = data.wireless_verse_grip[0].state;
cursor_pos = data.inverse3[0].state.cursor_position;
// ... classic math (not shown): if (wvg.buttons.a || wvg.buttons.b)
// move in pointing dir; clamp to sphere ...
commands_message out_cmds{};
device_commands dc{ .device_id = inv3_device_id };
dc.commands.set_cursor_position = set_cursor_position_cmd{cursor_pos};
if (first_message) {
first_message = false;
out_cmds.session = session_cmd{ /* profile = combined */ };
dc.configure = device_configure{ .preset = preset_cfg{"arm_front_centered"} };
}
out_cmds.inverse3.push_back(std::move(dc));
std::string out_json;
(void)glz::write_json(out_cmds, out_json);
ws.send(out_json);
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
Tutorial 06 wird zusätzlich lokal mit dem SDK installiert – suchen Sie in tutorials/06-haply-inverse-combined/ im Installationsverzeichnis des Dienstes.
Quelle: Python · C++ · C++ Glaze
Siehe auch: Steuerbefehle (set_cursor_position) · Typen (Quaternion, vec3) · Montage & Arbeitsraum (Voreinstellungen) · Tutorial 03 (Wireless VG) · Tutorial 05 (Positionsregelung)