07. Der Spielplatz von „Basis“ und „Mount“
Geräte Montage-Transformation interaktiv bedienen undconfigure.basis、configure.preset、configure.mount Dies veranschaulicht, wie die Elemente auf dem Draht miteinander interagieren. Um den Fokus der interaktiven Bedienung auf den Befehl „configure“ zu richten, wurde der horizontale Boden fixiert.
Was Sie in diesem Tutorial lernen werden:
- Neuanordnung der BasiselementeEinstellungen (
"XZY"→ (Anwendungsrahmen, bei dem die Y-Achse oben liegt) - Voreinstellungen(
arm_front)die Auswahl und das Verständnis dessen, was damit festgelegt wird - Überschreiben der Laufwerkszuordnung zur Laufzeit mithilfe von rotierenden Quaternionen (über die Tastatur)
- Ausschlussregel:
mountundpresetist dasselbe Gerät konfigurieren Können nicht innerhalb eines Blocks koexistieren - One-Shot-
configureFunktionsweise: Bei jedem Tastendruck wird genau eine „configure“-Meldung gesendet. - (C++ Glaze)
std::optionalModellierung exklusiver Felder unter Verwendung von
Workflow
- In der ersten Nachricht,Sitzungsprofil、
configure.basis: "XZY"、configure.preset: arm_frontwird gesendet. Für den festen Bodenset_cursor_forceDie Übertragung wird gestartet. - Bei jedem Tick wird die Y-Koordinate des Cursors ausgelesen, und
force_y = max(0, (floor_pos - y) * stiffness)berechnen und senden. - Wenn der Benutzer die Taste zum Drehen der Halterung drückt,
pending_configure = trueSetzt das Flag. - Beim nächsten Tick,
configure.mountErstellen Sie einen Block. Die Transformation dieses BlocksrotationEs handelt sich um ein Einheits-Quaternion (das aus dem aktuellen Pitch und Yaw in der Reihenfolge Z und anschließend X zusammengesetzt wurde).presetwird weggelassen. Denn beide sind auf dem Draht gegenseitig ausschließend. - Reset-Taste (
R) die Überschreibung aufheben. Beim nächsten `configure` wird dann wiederpresetwird verwendet.
Parameter
| Name | Standardwert | Verwendungszweck |
|---|---|---|
BASIS | "XZY" | Anordnung der Achsen — Anwendungsfenster mit der Y-Achse oben |
DEVICE_PRESET / DEVICE_CONFIG_PRESET | "arm_front" | Benannte Voreinstellungen – Der Ursprung ist die Basis des Geräts |
FLOOR_POS_Y | 0.0 m | Die Ebene des feststehenden Bodens (Y-Achse der Anwendung) |
STIFFNESS | 1000 k. A. | Federkonstante des Bettes |
MOUNT_STEP_DEG | 10° | Drehwinkel pro Tastendruck |
PRINT_EVERY_MS | 200 | Telemetrie-Abtastintervall |
Bedienung
| Schlüssel | Betrieb |
|---|---|
W / S | Drehen (Neigen) der Halterung um ±10° um die +X-Achse des Geräts |
A / D | Drehen (Gieren) der Halterung um ±10° um die +Z-Achse des Geräts |
R | Halterung zurücksetzen — Auf die Voreinstellung zurücksetzen |
H | Anleitung anzeigen |
Q | Ende |
mount und preset können nicht gleichzeitig angegeben werdenDer Service umfasst beides Gerät konfigurieren Block wird abgelehnt. Wenn der Benutzer die Einbindung überschreibt, wird in diesem Tutorial bei allen nachfolgenden `configure`-Befehlen preset wird weggelassen.R Wenn Sie darauf klicken, wird beim nächsten „configure“ preset wird wieder aktiviert, undmount wird gelöscht.
Die C++-Variante liest die Eingabe zeilenweise in einem stdin-Thread im Hintergrund ein (drücken Sie nach jedem Zeichen die Eingabetaste). Python erfasst Tastatureingaben in Echtzeit in der asynchronen Hauptschleife, daherkeyboard Verwenden Sie das Paket. Die Eingabetaste ist nicht erforderlich. Die Tasten und Befehle sind identisch.
Feld „Lesezustand“
data.inverse3[i].state Aus dem folgenden Text geht hervor:
cursor_position.y—vec3。Wird zur Berechnung der Eindringtiefe in den Boden verwendetcurrent_cursor_force— Wird für Telemetriezwecke gemeldet
Senden/Empfangen
Das Format der Nutzlast ist bei allen Varianten identisch. Ein bemerkenswerter Unterschied besteht darin, dass jede Implementierung exklusive mount / preset Es geht darum, wie die Verzweigung aufgebaut wird und wie der Eingabe-Thread den WebSocket-Thread benachrichtigt.
- Python
- C++ (nlohmann)
- C++ (Glaze)
keyboard Es handelt sich um eine einzelne asynchrone Schleife, die in einem Paket die Tastatureingaben in Echtzeit abfragt.pending_configure ist ein globales Flag, das vom Key-Handler gesetzt wird, undconfigure Es wird bei jeder Übertragung eines Blocks gelöscht.
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))
Es handelt sich um eine Konfiguration mit zwei Threads. Der stdin-Thread im Hintergrund liest die Zeilen undpending_configure(std::atomic<bool>) umschaltet. Der I/O-Thread von libhv überprüft dies bei jedem Tick und, falls es eingestellt ist, configure senden.
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 Ausschlussregel lautet:std::optional<preset_cfg> und std::optional<mount_cfg> lässt sich auf natürliche Weise darstellen. In der serialisierten JSON-Datei erscheinen nur die Felder, für die ein Wert festgelegt wurde. Der Name der Voreinstellung lautet enum class wird modelliert als,glz::meta Durch die Spezialisierung wird eine Zuordnung zu den vom Dienst erwarteten Zeichenfolgen hergestellt.
// 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 zusammen mit dem SDK auch lokal installiert. Im Installationsverzeichnis des Dienstes unter tutorials/07-haply-inverse-basis-and-mount/ Bitte überprüfen Sie dies.
Quelle: Python · C++ · C++ Glaze
Siehe auch: Neuanordnung der Basiselemente · Halterung und Arbeitsbereich · Geräteeinstellungen · Steuerbefehle (set_cursor_force) · Typ (transform) · Tutorial 04 (Hello Floor)