07. Fundament und Montage: Testgelände
Steuere interaktiv die Montageumwandlung des Geräts und zeigt, wie configure.basis, configure.preset et configure.mount arbeiten auf Protokollebene zusammen. Behält einen festen horizontalen Hintergrund bei, damit der interaktive Fokuspunkt auf den Konfigurationselementen verbleibt.
Was Sie lernen werden:
- Eine Grundpermutation (
"XZY"→ Anwendungsmarkierung mit der Y-Achse nach oben) - Einen auswählen Voreinstellung (
arm_front) und verstehen, was damit konfiguriert wird - Die Laufzeit-Implementierung durch ein Rotationsquaternion (über die Tastatur gesteuert) ersetzen
- Die Regel der gegenseitigen Ausschließung:
mountetpresetkönnen nicht im selben Block von Gerätekonfiguration - Punktuelle Semantik von
configure: Bei jedem Tastendruck wird genau eine Konfigurationsnachricht gesendet - (C++ Glaze) Modellierung sich gegenseitig ausschließender Felder mit
std::optional
Arbeitsablauf
- Bei der ersten Nachricht: Senden Sie die Sitzungsprofil,
configure.basis: "XZY"etconfigure.preset: arm_front. Beginnen Sie mit dem Versendenset_cursor_forcefür den festen Boden. - In jedem Zyklus: Lesen Sie den Wert Y des Schiebereglers aus und berechnen Sie
force_y = max(0, (floor_pos - y) * stiffness)und senden Sie sie ab. - Wenn der Benutzer eine Dreh-Taste der Halterung drückt, legen Sie Folgendes fest:
pending_configure = true. - Im nächsten Schritt: Erstellen Sie einen Block
configure.mountmit einer Transformation, bei derrotationist ein unitäres Quaternion (Zusammensetzung aus Z und X der aktuellen Nick- bzw. Gierwinkel). Lassen Siepreset— Beide schließen sich auf Protokollebene gegenseitig aus. - Die Reset-Taste (
R) hebt die Ersetzung auf; die folgende Konfiguration entsprichtpreset.
Einstellungen
| Nom | Fehler | Rolle |
|---|---|---|
BASIS | "XZY" | Achsenverschiebung – Koordinatensystem mit Y-Achse nach oben |
DEVICE_PRESET / DEVICE_CONFIG_PRESET | "arm_front" | Benannte Voreinstellung – Ursprung an der Gerätebasis |
FLOOR_POS_Y | 0.0 m | Fester Grundriss (Y der Anwendung) |
STIFFNESS | 1000 k. A. | Bodensteifigkeitskonstante |
MOUNT_STEP_DEG | 10° | Drehen per Tastendruck |
PRINT_EVERY_MS | 200 | Begrenzung der Telemetriedatenrate |
Bestellungen
| Touche | Aktion |
|---|---|
W / S | Die Halterung um ±10° um die +X-Achse des Geräts drehen (Neigung) |
A / D | Die Halterung um ±10° um die +Z-Achse des Geräts drehen (Gieren) |
R | Montage zurücksetzen – zur Voreinstellung zurückkehren |
H | Bestellungen anzeigen |
Q | Drückeberger |
mount et preset schließen sich gegenseitig ausDer Dienst lehnt einen Block von Gerätekonfiguration das beides enthält. Sobald der Benutzer die Montage ausgetauscht hat, lässt das Tutorial preset jeder der folgenden Konfigurationen. Drücken Sie R reaktiv preset bei der folgenden Konfiguration und entferne mount.
Die C++-Varianten lesen die Eingabe zeilenweise in einem stdin-Hintergrund-Thread ein (drücken Sie nach jedem Buchstaben die Eingabetaste). Python verwendet das Paket keyboard für die Echtzeit-Tastenabfrage in der asynchronen Hauptschleife – keine Eingabetaste erforderlich. Gleiche Tasten, gleiche Befehle.
Meister der Les-États
Ab data.inverse3[i].state :
cursor_position.y—vec3, das zur Berechnung der Eindringtiefe in den Boden verwendet wirdcurrent_cursor_force— für die Telemetrie gekennzeichnet
Versand / Empfang
Die Struktur der Nutzlast ist bei allen Varianten gleich; die wesentlichen Unterschiede betreffen die Art und Weise, wie die einzelnen Varianten die sich gegenseitig ausschließende Verzweigung aufbauen. mount / preset und wie der Eingangsdraht den WebSocket-Draht signalisiert.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Einzigartige asynchrone Schleife mit Echtzeit-Tastenabfrage über das Paket keyboard. pending_configure ist ein globaler Indikator, der von den Tastenverwaltern definiert und jedes Mal zurückgesetzt wird, wenn ein Block configure wird versendet.
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))
Modell mit zwei Ausführungssträngen: Ein stdin-Strang im Hintergrund liest die Zeilen ein und wechselt pending_configure (un std::atomic<bool>); der libhv-E/A-Thread überprüft diesen Flag in jedem Zyklus und gibt configure wenn er angehoben wird.
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 führt natürlich zu std::optional<preset_cfg> et std::optional<mount_cfg> : Nur der angegebene Wert erscheint in der JSON-Serialisierung. Der Name der Voreinstellung ist als enum class mit einer Spezialisierung glz::meta das ihn mit der vom Dienst erwarteten Zeichenfolge verknüpft.
// 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);
Das Tutorial 07 wird ebenfalls lokal mit dem SDK installiert – siehe tutorials/07-haply-inverse-basis-and-mount/ im Installationsverzeichnis des Dienstes.
Quelle: Python · C++ · C++ Glaze
Siehe auch: Grundpermutation · Montage und Arbeitsbereich · Gerätekonfiguration · Steuerbefehle (set_cursor_force) · Typen (transformieren) · Tutorial 04 (Hallo Boden)