04. Hallo Boden
Dein erster haptischer Effekt: ein virtueller horizontaler Boden, der zurückdrückt, wenn der Cursor darauf drückt. Die Kraft wird durch eine einfache Feder mit negativer Federkonstante erzeugt — stiffness × penetration_depth — entlang Z über set_cursor_force.
Was Sie lernen werden:
- Verwendung von
set_cursor_forceeine Federkraft aufzubringen - Lesen
cursor_positionund die Rechenleistung in Echtzeit - (Python) Festlegen einer Arbeitsbereichsvoreinstellung (
arm_front_centered), sodass der Ursprung in der Mitte des Arbeitsbereichs liegt - (Python) Interaktive Anpassung von Bodenhöhe und -steifigkeit über die
keyboardPaket
Arbeitsablauf
- Öffne einen WebSocket zu
ws://localhost:10001und auf den ersten Status-Frame warten. - Im ersten Frame: Registriere die Sitzungsprofil. Die Python-Variante sendet zusätzlich
configure.preset: arm_front_centeredDer Ursprung befindet sich also in der Mitte des Arbeitsbereichs; die C++-Varianten verwenden die auf dem Gerät bereits aktive Konfiguration. - Bei jedem Bild: lesen
cursor_position.z, berechnenforce_z = max(0, (floor_pos - z) * stiffness), und sende es alsset_cursor_forceBefehl. - Nachfolgende Ticks senden nur den Befehl „force“ – das Sitzungsprofil ist ein einmaliger Handshake.
- (Python) Bei jedem Tick werden auch die Pfeiltasten abgefragt und die Anzeige aktualisiert
floor_pos/stiffnesslive.
Parameter
| Name | Standard | Zweck |
|---|---|---|
floor_pos | 0.10 m | Z-Koordinate der virtuellen Bodenebene |
stiffness | 1000 k. A. | Federkonstante (1 mm Eindringtiefe → 1 N) |
PRINT_EVERY_MS | 100–200 | Telemetrie-Drosselklappe |
| Name des Sitzungsprofils | co.haply.inverse.tutorials:hello-floor | Identifiziert diese Simulation im Haply |
Die Python-Variante verwendet die keyboard Paket (unter Linux sind erweiterte Berechtigungen erforderlich):
↑/↓— Bodenebene anheben / absenken←/→— Steifigkeit verringern / erhöhenR— Auf Standardwerte zurücksetzen
Die Kräfte werden im Service-Tick addiert – sie werden über alle Quellen hinweg summiert, bevor sie an das Gerät gesendet werden. Ein Tutorial wie dieses kann neben anderen Kraftgeneratoren laufen, ohne dass sich diese gegenseitig blockieren.
Statusfelder gelesen
Von data.inverse3[i].state:
cursor_position.z—vec3, zur Berechnung der Eindringtiefecurrent_cursor_force— für die Telemetrie gemeldet
Senden / Empfangen
Bei jedem Tick: Z-Koordinate des Cursors lesen, berechnen force_z = max(0, (floor_pos - z) * stiffness)und senden Sie eine set_cursor_force. Die erste ausgehende Nachricht enthält außerdem das Sitzungsprofil (alle Varianten) und für Python configure.preset: arm_front_centered.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Einzelne asynchrone Schleife. Der Handshake im ersten Frame enthält das Profil + configure.preset also floor_pos = 0.1 richtet sich nach den Koordinaten der Arbeitsbereichsmitte aus.
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 + preset (one-shot)
request_msg = {
"session": {"configure": {"profile": {"name": "co.haply.inverse.tutorials:hello-floor"}}},
"inverse3": [{
"device_id": device_id,
"configure": {"preset": {"preset": "arm_front_centered"}}
}]
}
else:
# Per tick: compute force along Z, send set_cursor_force
z = data["inverse3"][0]["state"]["cursor_position"]["z"]
force_z = 0.0 if z > floor_pos else (floor_pos - z) * stiffness
request_msg = {
"inverse3": [{
"device_id": device_id,
"commands": {"set_cursor_force":
{"vector": {"x": 0.0, "y": 0.0, "z": force_z}}}
}]
}
await websocket.send(json.dumps(request_msg))
libhv-Callback-Modell – „onmessage“ wird im WebSocket-E/A-Thread ausgeführt; der Hauptthread wird bei „ENTER“ blockiert. C++-Varianten verwenden den minimalen Handshake (nur Sitzungsprofil, keine Voreinstellung).
ws.onmessage = [&](const std::string &msg) {
const json data = json::parse(msg);
// If no Inverse3 yet, ask the service to re-send the full state
if (!data.contains("inverse3") || data["inverse3"].empty()) {
ws.send(R"({"session":{"force_render_full_state":{}}})");
return;
}
json request = {};
if (first_message) {
first_message = false;
request["session"] = {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:hello-floor"}}}}}};
}
request["inverse3"] = json::array();
for (auto &el : data["inverse3"].items()) {
const float z = el.value()["state"]["cursor_position"]["z"].get<float>();
const float force_z = z > floor_pos ? 0.0f : (floor_pos - z) * stiffness;
request["inverse3"].push_back({
{"device_id", el.value()["device_id"]},
{"commands", {{"set_cursor_force",
{{"vector", {{"x", 0.0}, {"y", 0.0}, {"z", force_z}}}}}}},
});
}
ws.send(request.dump());
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
Dasselbe libhv-Callback-Modell – nur der Hauptteil ändert sich. Typisierte Strukturen sowohl für den Status als auch für die Befehle. std::optional<session_cmd> enthält das One-Shot-Profil – Glaze lässt es im serialisierten JSON weg, wenn es nicht gesetzt ist.
// Struct models
struct vec3 { float x{}, y{}, z{}; };
struct inverse_state { vec3 cursor_position{}, current_cursor_force{}; };
struct inverse_device { std::string device_id; inverse_state state; };
struct devices_message { std::vector<inverse_device> inverse3; };
struct set_cursor_force_cmd { vec3 vector; };
struct commands_message {
std::optional<session_cmd> session;
std::vector<device_commands> inverse3;
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
if (data.inverse3.empty()) {
ws.send(R"({"session":{"force_render_full_state":{}}})");
return;
}
commands_message out_cmds{};
if (first_message) {
first_message = false;
out_cmds.session = session_cmd{ /* profile = hello-floor */ };
}
for (const auto &dev : data.inverse3) {
const float z = dev.state.cursor_position.z;
const float force_z = z > floor_pos ? 0.0f : (floor_pos - z) * stiffness;
device_commands dc{ .device_id = dev.device_id };
dc.commands.set_cursor_force = set_cursor_force_cmd{{0.0f, 0.0f, force_z}};
out_cmds.inverse3.push_back(std::move(dc));
}
std::string out_json;
(void)glz::write_json(out_cmds, out_json);
ws.send(out_json);
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
Quelle: Python · C++ · C++ Glaze
Siehe auch: Steuerbefehle (set_cursor_force) · Mount & Arbeitsbereich (Voreinstellungen) · Typen (vec3) · Sitzungen · Anleitung 07 (Basis & Halterung)