07. Basis & Mount – Übungsplatz
Geräte interaktiv steuern Mount-Transformation, und demonstrieren configure.basis、configure.preset und configure.mount Wie lässt sich die Zusammenarbeit auf der Ebene der Kommunikationsnachrichten gestalten? Halten Sie eine einheitliche Ebene ein, damit der Schwerpunkt der Interaktion stets auf dem Befehl „configure“ liegt.
Du wirst Folgendes lernen:
- Einstellungen Basis-Achsenverschiebung(
"XZY"→ (Anwendungskoordinatensystem auf der Y-Achse) - Auswahl Voreinstellungen(
arm_front), und zu verstehen, welche Einstellungen dadurch vorgenommen werden - Überschreiben von „mount“ zur Laufzeit durch Drehen von Quaternionen (über die Tastatur)
- Regeln zur gegenseitigen Ausschließung:
mountundpresetkönnen nicht gleichzeitig in demselben Gerätekonfiguration im Block - Einmalig
configureBedeutung: Bei jedem Tastendruck wird nur eine „configure“-Nachricht gesendet. - (C++ Glaze) Verwendung
std::optionalModellierung von exklusiv-oder-Feldern
Arbeitsablauf
- Bei Erhalt der ersten Nachricht: Senden Sitzungsprofil、
configure.basis: "XZY"sowieconfigure.preset: arm_front。Um den Boden zu fixieren, wird nun kontinuierlich gesendetset_cursor_force。 - Bei jedem Tick: Die Y-Koordinate des Cursors auslesen und berechnen
force_y = max(0, (floor_pos - y) * stiffness)und senden. - Wenn der Benutzer die Dreh-Taste „mount“ drückt, wird
pending_configure = truePositionieren. - Im nächsten Tick: Erstellen eines
configure.mountBlock, dessen TransformationrotationEinheit: Quaternion (Kombination nach dem aktuellen Nick- und Gierwinkel, wobei Z vor X kommt). Weggelassenpreset——Beide schließen sich im Kommunikationsprotokoll gegenseitig aus. - Reset-Taste (
R) Überschreibende Einstellungen löschen; beim nächsten Aufruf von „configure“ wird wieder aufpreset。
Parameter
| Bezeichnung | Standardwert | Verwendungszweck |
|---|---|---|
BASIS | "XZY" | Achsenverschiebung – Anwendungskoordinatensystem entlang der Y-Achse |
DEVICE_PRESET / DEVICE_CONFIG_PRESET | "arm_front" | Benannte Voreinstellung – Ursprung am Gerätefuß |
FLOOR_POS_Y | 0.0 m | Feste Bodenebene (Y-Achse des Anwendungskoordinatensystems) |
STIFFNESS | 1000 k. A. | Bodenfederkonstante |
MOUNT_STEP_DEG | 10° | Drehwinkel pro Tastendruck |
PRINT_EVERY_MS | 200 | Telemetrie-Drosselungsintervall |
Steuerung
| Taste | Bedienung |
|---|---|
W / S | Drehung der Vorrichtung um die X-Achse (Neigung) um ±10° |
A / D | Drehung der Vorrichtung um die Z-Achse (Gierung) um ±10° |
R | Mount zurücksetzen – auf die Standardeinstellung zurücksetzen |
H | Anzeigesteuerungstasten |
Q | Beenden |
mount und preset ExklusivitätDer Dienst lehnt es ab, beides gleichzeitig zu enthalten. Gerätekonfiguration Block. Sobald der Benutzer „mount“ überschrieben hat, wird dieser Schritt in diesem Tutorial bei jedem nachfolgenden „configure“-Vorgang übersprungen. preset。Drücken Sie R Danach wird es beim nächsten „configure“-Befehl wieder aktiviert. preset und entfernen mount。
Die C++-Variante liest die zeilenweise Eingabe im Hintergrund im stdin-Thread ein (nach jedem Buchstaben muss die Eingabetaste gedrückt werden). Python hingegen verwendet in der asynchronen Hauptschleife keyboard Es erfolgt eine Echtzeit-Tastenabfrage, ohne dass die ENTER-Taste gedrückt werden muss. Bei gleicher Tastenbelegung sind auch die Befehle identisch.
Gelesene Statusfelder
Aus data.inverse3[i].state:
cursor_position.y—vec3, zur Berechnung der Eindringtiefe in den Bodencurrent_cursor_force— zur Fernmessung und Datenübermittlung
Senden / Empfangen
Die Laststruktur ist bei allen Varianten identisch; der eigentliche Unterschied liegt darin, wie jede Variante die gegenseitige Ausschließung umsetzt. mount / preset Zweige sowie die Art und Weise, wie der Eingabe-Thread den WebSocket-Thread benachrichtigt.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Ein einziger asynchroner Schleifenzyklus, durch keyboard Das Paket führt eine Echtzeit-Tastenabfrage durch.pending_configure Es handelt sich um ein globales Flag, das von der Tastenbehandlungsfunktion gesetzt wird, und zwar bei jeder Übertragung configure Nach dem Block löschen.
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))
Zweithread-Modell: Der stdin-Thread im Hintergrund liest die Eingabe zeilenweise ein und setzt die Bits pending_configure(Ein std::atomic<bool>); Der libhv-I/O-Thread überprüft dieses Flag bei jedem Tick und löst, sobald es gesetzt ist, configure。
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 Exklusivitätsregel lässt sich auf natürliche Weise abbilden auf std::optional<preset_cfg> und std::optional<mount_cfg> Oben: In der serialisierten JSON-Datei erscheint nur derjenige Wert, dem ein Wert zugewiesen wurde. Der Standardname wird als ein enum class, und durch glz::meta Es wird speziell in die vom Server erwartete Zeichenfolge umgewandelt.
// 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 zusammen mit dem SDK lokal installiert – bitte sehen Sie im Installationsverzeichnis des Dienstes nach. tutorials/07-haply-inverse-basis-and-mount/。
Quellcode: Python · C++ · C++ Glaze
Verwandte Themen: Basis-Achsenverschiebung · Mount und Arbeitsbereich · Gerätekonfiguration · Steuerbefehle (set_cursor_force) · Typ (transform) · Tutorial 04 (Hello Floor)