07. Basis & Mount Spielplatz
Steuert das Gerät interaktiv Montage-Transformation und zeigt, wie configure.basis, configure.presetund configure.mount Gemeinsam am Befehlszeilen-Interpreter arbeiten. Es wird eine feste horizontale Zeile beibehalten, damit der interaktive Fokus auf den Konfigurationsbefehlen bleibt.
Was Sie lernen werden:
- Festlegen einer Basispermutation (
"XZY"→ Y-up-Anwendungsrahmen) - Die Wahl eines Voreinstellung (
arm_front) und zu verstehen, was damit konfiguriert wird - Das Reittier zur Laufzeit durch ein Rotationsquaternion überschreiben (über die Tastatur)
- Die Regel des gegenseitigen Ausschlusses:
mountundpresetkönnen nicht nebeneinander existieren Gerät konfigurieren Block - Einmalig
configureSemantik: Jeder Tastendruck löst genau eine Konfigurationsnachricht aus - (C++ Glaze) Modellierung von sich gegenseitig ausschließenden Feldern mit
std::optional
Arbeitsablauf
- Bei der ersten Nachricht: sende die Sitzungsprofil,
configure.basis: "XZY"undconfigure.preset: arm_front. Mit dem Senden beginnenset_cursor_forcefür den festen Boden. - Bei jedem Tick: Y-Koordinate des Cursors lesen, berechnen
force_y = max(0, (floor_pos - y) * stiffness), schick es. - Wenn der Benutzer eine Taste zur Drehung der Halterung drückt, wird das Flag gesetzt
pending_configure = true. - Beim nächsten Takt: Erstelle eine
configure.mountBlock mit einer Transformation, derenrotationist ein Einheitsquaternion (Z-dann-X-Komposition der aktuellen Nick- und Gierwinkel). Weglassenpreset— Die beiden schließen sich im Netzwerk gegenseitig aus. - Reset-Taste (
R) hebt die Überschreibung auf; bei der nächsten Konfiguration wird aufpresetwieder.
Parameter
| Name | Standard | Zweck |
|---|---|---|
BASIS | "XZY" | Achsenpermutation – Y-up-Anwendungsrahmen |
DEVICE_PRESET / DEVICE_CONFIG_PRESET | "arm_front" | Benannte Voreinstellung – Ursprung am Gerätefuß |
FLOOR_POS_Y | 0.0 m | Feste Bodenebene (Anwendung Y) |
STIFFNESS | 1000 k. A. | Bodenfederkonstante |
MOUNT_STEP_DEG | 10° | Drehung pro Tastendruck |
PRINT_EVERY_MS | 200 | Telemetrie-Drosselklappe |
Bedienelemente
| Schlüssel | Aktion |
|---|---|
W / S | Halterung um ±10° um die +X-Achse des Geräts drehen (Neigung) |
A / D | Halterung um ±10° um die +Z-Achse des Geräts drehen (Gieren) |
R | Montage zurücksetzen – auf Voreinstellung zurücksetzen |
H | Steuerelemente anzeigen |
Q | Beenden |
mount und preset schließen sich gegenseitig ausDer Dienst lehnt eine Gerät konfigurieren Block, der beides enthält. Sobald der Benutzer die Einbindung überschreibt, lässt das Tutorial preset bei jedem weiteren Konfigurationsvorgang. Durch Drücken der R wieder aktivieren preset bei den nächsten Konfigurationen und Löschvorgängen mount.
Die C++-Varianten lesen zeilenbasierte Eingaben in einem Stdin-Hintergrundthread ein (drücken Sie nach jedem Buchstaben die Eingabetaste). Python verwendet die keyboard Paket für die Echtzeit-Tastenabfrage in der asynchronen Hauptschleife – kein ENTER-Tastenanschlag erforderlich. Gleiche Tasten, gleiche Befehle.
Statusfelder gelesen
Von data.inverse3[i].state:
cursor_position.y—vec3, zur Berechnung der Durchdringung des Bodenscurrent_cursor_force— für die Telemetrie gemeldet
Senden / Empfangen
Die Form der Nutzlast ist bei allen Varianten gleich; interessant sind die Unterschiede darin, wie jede Variante die sich gegenseitig ausschließenden mount / preset Verzweigungen und wie der Eingabe-Thread dem WebSocket-Thread Signale sendet.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Einzelne asynchrone Schleife mit Echtzeit-Tastenabfrage über die keyboard Paket. pending_configure ist ein globales Flag, das von den Schlüssel-Handlern gesetzt und bei jedem configure Der Block wird gesendet.
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 Stdin-Thread im Hintergrund liest Zeilen ein und wechselt pending_configure (ein std::atomic<bool>); der libhv-I/O-Thread überprüft dies bei jedem Takt und gibt configure wenn eingestellt.
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ßung lässt sich nahtlos auf std::optional<preset_cfg> und std::optional<mount_cfg>: Nur der ausgefüllte Name erscheint im serialisierten JSON. Der Name der Voreinstellung ist als enum class mit einem glz::meta eine Spezialisierung, die sie in die vom Dienst erwartete Zeichenfolge umwandelt.
// 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);
Quelle: Python · C++ · C++ Glaze
Siehe auch: Basispermutation · Halterung & Arbeitsbereich · Gerätekonfiguration · Steuerbefehle (set_cursor_force) · Typen (transformieren) · Tutorial 04 (Hallo Boden)