07. Basis & Mount-Playground
Steuert interaktiv die Mount-Transformation des Geräts und zeigt, wie configure.basis, configure.preset und configure.mount auf Protokollebene zusammenwirken. Es wird ein fester horizontaler Boden beibehalten, damit der interaktive Fokus auf den configure-Befehlen bleibt.
Was Sie lernen werden:
- Festlegen einer Basis-Permutation (
"XZY"→ Anwendungsrahmen mit Y-Achse nach oben) - Auswahl eines Presets (
arm_front) und Nachvollziehen, was es konfiguriert - Überschreiben des Mounts zur Laufzeit mit einem Rotations-Quaternion (per Tastatur gesteuert)
- Die Regel der gegenseitigen Ausschließlichkeit:
mountundpresetkönnen nicht gemeinsam im selben Block der Gerätekonfiguration vorkommen - Einmalige
configure-Semantik: Jeder Tastendruck sendet genau eine configure-Nachricht - (C++ Glaze) Modellierung sich gegenseitig ausschließender Felder mit
std::optional
Ablauf
- Bei der ersten Nachricht: Senden Sie das Session-Profil,
configure.basis: "XZY"undconfigure.preset: arm_front. Beginnen Sie, für den festen Bodenset_cursor_forcezu senden. - Bei jedem Tick: Cursor-Y auslesen,
force_y = max(0, (floor_pos - y) * stiffness)berechnen und senden. - Wenn der Benutzer eine Mount-Rotationstaste drückt, setzen Sie
pending_configure = true. - Beim nächsten Tick erstellen Sie einen
configure.mount-Block mit einer Transformation, derenrotationein Einheits-Quaternion ist (Komposition Z-dann-X aus dem aktuellen Pitch- und Yaw-Wert). Lassen Siepresetweg – die beiden schließen sich auf Protokollebene gegenseitig aus. - Die Reset-Taste (
R) hebt das Überschreiben auf; die nächste configure-Anweisung greift wieder aufpresetzurück.
Parameter
| Name | Standard | Zweck |
|---|---|---|
BASIS | "XZY" | Achsenpermutation — Anwendungsrahmen mit Y-Achse nach oben |
DEVICE_PRESET / DEVICE_CONFIG_PRESET | "arm_front" | Benanntes Preset — Ursprung an der Gerätebasis |
FLOOR_POS_Y | 0.0 m | Feste Bodenebene (Anwendungs-Y) |
STIFFNESS | 1000 N/m | Federkonstante des Bodens |
MOUNT_STEP_DEG | 10° | Rotation pro Tastendruck |
PRINT_EVERY_MS | 200 | Drosselung der Telemetrie |
Steuerung
| Taste | Aktion |
|---|---|
W / S | Mount um ±10° um die +X-Achse des Geräts drehen (Pitch) |
A / D | Mount um ±10° um die +Z-Achse des Geräts drehen (Yaw) |
R | Mount zurücksetzen – zurück zum Preset |
H | Steuerung anzeigen |
Q | Beenden |
mount und preset schließen sich gegenseitig ausDer Dienst lehnt einen Block der Gerätekonfiguration ab, der beide enthält. Sobald der Benutzer den Mount überschreibt, lässt das Tutorial preset in jeder weiteren configure-Anweisung weg. Durch Drücken von R wird preset bei der nächsten configure-Anweisung wieder aktiviert und mount verworfen.
Die Varianten C++ lesen zeilenbasierte Eingaben in einem Hintergrund-stdin-Thread (nach jedem Buchstaben ENTER drücken). Python verwendet das Paket keyboard für die Echtzeit-Tastenabfrage in der asynchronen Hauptschleife – ENTER ist nicht nötig. Gleiche Tasten, gleiche Befehle.
Gelesene Zustandsfelder
Aus data.inverse3[i].state:
cursor_position.y—vec3, wird zur Berechnung der Bodendurchdringung verwendetcurrent_cursor_force— wird für die Telemetrie gemeldet
Senden / Empfangen
Die Struktur der Nutzlast ist in allen Varianten gleich; interessant sind die Unterschiede darin, wie jede Variante die sich gegenseitig ausschließenden Zweige mount / preset aufbaut und wie der Eingabe-Thread den Thread WebSocket signalisiert.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Einzelne asynchrone Schleife mit Echtzeit-Tastenabfrage über das Paket keyboard. pending_configure ist ein globales Flag, das von den Tastaturhandlern gesetzt und jedes Mal zurückgesetzt wird, wenn ein configure-Block gesendet wird.
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 + basis + preset
request_msg = {
"session": {"configure": {"profile": {"name": SLUG}}},
"inverse3": [{
"device_id": device_id,
"configure": build_configure_block(first_handshake=True),
# -> {"basis": {"permutation": "XZY"},
# "preset": {"preset": "arm_front"}}
}],
}
else:
handle_key_inputs() # may set pending_configure = True (classic, not shown)
y = data["inverse3"][0]["state"]["cursor_position"]["y"]
force_y = 0.0 if y > FLOOR_POS_Y else (FLOOR_POS_Y - y) * STIFFNESS
entry = {
"device_id": device_id,
"commands": {"set_cursor_force":
{"vector": {"x": 0.0, "y": force_y, "z": 0.0}}},
}
if pending_configure:
entry["configure"] = build_configure_block(first_handshake=False)
# -> {"mount": {...}} OR {"preset": {...}} (never both)
pending_configure = False
request_msg = {"inverse3": [entry]}
await websocket.send(json.dumps(request_msg))
Zwei-Thread-Modell: Ein Hintergrund-stdin-Thread liest Zeilen und setzt pending_configure (ein std::atomic<bool>) auf true; der libhv-I/O-Thread prüft es bei jedem Tick und gibt configure aus, sobald es gesetzt ist.
std::atomic<bool> pending_configure{false};
ws.onmessage = [&](const std::string &msg) {
const json data = json::parse(msg);
if (!data.contains("inverse3") || data["inverse3"].empty()) return;
const bool do_handshake = first_message;
if (first_message) first_message = false;
const bool do_configure = do_handshake || pending_configure.exchange(false);
json request = {};
if (do_handshake) {
request["session"] = {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:basis-and-mount"}}}}}};
}
request["inverse3"] = json::array();
for (auto &el : data["inverse3"].items()) {
json dev_cmd = {{"device_id", el.value()["device_id"]}};
if (do_configure) {
json cfg = {};
if (do_handshake) cfg["basis"] = {{"permutation", BASIS}};
if (mount_overridden) {
cfg["mount"] = {{"transform", {
{"position", {{"x", 0.0}, {"y", 0.0}, {"z", 0.0}}},
{"rotation", quat_from_xz_deg(mount_angle_x_deg, mount_angle_z_deg)},
{"scale", {{"x", 1.0}, {"y", 1.0}, {"z", 1.0}}},
}}};
} else {
cfg["preset"] = {{"preset", DEVICE_CONFIG_PRESET}};
}
dev_cmd["configure"] = cfg;
}
const float y = el.value()["state"]["cursor_position"]["y"].get<float>();
const float force_y = y > FLOOR_POS_Y ? 0.0f : (FLOOR_POS_Y - y) * STIFFNESS;
dev_cmd["commands"] = {{"set_cursor_force",
{{"vector", {{"x", 0.0}, {"y", force_y}, {"z", 0.0}}}}}};
request["inverse3"].push_back(dev_cmd);
}
ws.send(request.dump());
};
std::thread input_thr(input_thread_func); // stdin reader — flips pending_configure
ws.open("ws://localhost:10001");
while (running.load()) std::this_thread::sleep_for(50ms);
Die Regel der gegenseitigen Ausschließlichkeit lässt sich natürlich auf std::optional<preset_cfg> und std::optional<mount_cfg> abbilden: In der serialisierten Ausgabe von JSON erscheint nur der jeweils gesetzte dieser beiden Werte. Der Preset-Name wird als enum class modelliert, mit einer glz::meta-Spezialisierung, die ihn auf die Zeichenkette abbildet, die der Dienst erwartet.
// The preset set modelled as an enum
enum class device_preset {
defaults, arm_front, arm_front_centered,
led_front, led_front_centered, custom,
};
// Glaze meta — serialize the enum as the JSON string the service expects
template <> struct glz::meta<device_preset> {
using enum device_preset;
static constexpr auto value =
enumerate(defaults, arm_front, arm_front_centered,
led_front, led_front_centered, custom);
};
// Transform + the configure block
struct vec3 { float x{}, y{}, z{}; };
struct quat { float w{1.0f}, x{}, y{}, z{}; };
struct transform_t { vec3 position{}; quat rotation{}; vec3 scale{1,1,1}; };
struct preset_cfg { device_preset preset; };
struct basis_cfg { std::string permutation; };
struct mount_cfg { transform_t transform; };
struct device_configure {
std::optional<preset_cfg> preset; // mutually exclusive with mount
std::optional<basis_cfg> basis;
std::optional<mount_cfg> mount; // mutually exclusive with preset
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
if (data.inverse3.empty()) return;
const bool do_handshake = first_message.exchange(false);
const bool do_configure = do_handshake || pending_configure.exchange(false);
commands_message out_cmds{};
if (do_handshake) {
out_cmds.session = session_cmd{ /* profile = basis-and-mount */ };
}
for (const auto &dev : data.inverse3) {
device_commands dc{ .device_id = dev.device_id };
if (do_configure) {
device_configure cfg{};
if (do_handshake) cfg.basis = basis_cfg{BASIS};
if (mount_overridden) {
cfg.mount = mount_cfg{ .transform = transform_t{
.rotation = quat_from_xz_deg(mount_angle_x_deg, mount_angle_z_deg)}};
} else {
cfg.preset = preset_cfg{DEVICE_CONFIG_PRESET};
}
dc.configure = std::move(cfg);
}
const float y = dev.state.cursor_position.y;
const float force_y = y > FLOOR_POS_Y ? 0.0f : (FLOOR_POS_Y - y) * STIFFNESS;
dc.commands.set_cursor_force = set_cursor_force_cmd{{0.0f, force_y, 0.0f}};
out_cmds.inverse3.push_back(std::move(dc));
}
std::string out_json;
(void)glz::write_json(out_cmds, out_json);
ws.send(out_json);
};
std::thread input_thr(input_thread_func);
ws.open("ws://localhost:10001");
while (running.load()) std::this_thread::sleep_for(50ms);
Tutorial 07 ist auch lokal mit dem SDK installiert – zu finden unter tutorials/07-haply-inverse-basis-and-mount/ im Installationsverzeichnis des Dienstes.
Quelle: Python · C++ · C++ Glaze
Verwandt: Basis-Permutation · Mount & Workspace · Gerätekonfiguration · Steuerbefehle (set_cursor_force) · Typen (transform) · Tutorial 04 (Hello Floor)