04. Hallo Boden
Ihr erster haptischer Effekt: ein virtueller horizontaler Boden, der zurückdrückt, sobald der Cursor in ihn eindringt. Die Kraft ist eine einfache Penalty-Feder (stiffness × penetration_depth), die entlang der Z-Achse über set_cursor_force angewendet wird.
Was Sie lernen:
- Verwendung von
set_cursor_force, um eine Penalty-Federkraft anzuwenden - Auslesen von
cursor_positionund Berechnen der Kraft in Echtzeit - (Python) Festlegen eines Presets für den Arbeitsbereich (
arm_front_centered), sodass der Ursprung in der Mitte des Arbeitsbereichs liegt - (Python) Interaktive Feinabstimmung von Bodenhöhe und Steifigkeit per Tastatur über das Paket
keyboard
Ablauf
- Öffnen Sie eine WebSocket-Verbindung zu
ws://localhost:10001und warten Sie auf den ersten Zustandsframe. - Registrieren Sie beim ersten Frame das Session-Profil. Die Variante Python sendet zusätzlich
configure.preset: arm_front_centered, sodass der Ursprung in der Mitte des Arbeitsbereichs liegt; die Varianten C++ verwenden die Konfiguration, die auf dem Gerät bereits aktiv ist. - Bei jedem Frame lesen Sie
cursor_position.zaus, berechnenforce_z = max(0, (floor_pos - z) * stiffness)und senden das Ergebnis als Befehlset_cursor_forcezurück. - Bei den folgenden Ticks wird nur noch der Kraftbefehl gesendet – das Session-Profil ist ein einmaliger Handshake.
- (Python) Bei jedem Tick werden außerdem die Pfeiltasten der Tastatur abgefragt, und
floor_pos/stiffnesswerden live aktualisiert.
Parameter
| Name | Standard | Zweck |
|---|---|---|
floor_pos | 0.10 m | Z-Koordinate der virtuellen Bodenebene |
stiffness | 1000 N/m | Federkonstante (1 mm Eindringtiefe → 1 N) |
PRINT_EVERY_MS | 100–200 | Drosselung der Telemetrie |
| Name des Session-Profils | co.haply.inverse.tutorials:hello-floor | Kennzeichnet diese Simulation in Haply Hub |
Die Variante Python verwendet das Paket keyboard (auf Linux sind erhöhte Berechtigungen erforderlich):
↑/↓— Bodenebene anheben / absenken←/→— Steifigkeit verringern / erhöhenR— auf Standardwerte zurücksetzen
Kräfte sind beim Dienst-Tick additiv – sie werden aus allen Quellen summiert, bevor sie an das Gerät gesendet werden. Ein Tutorial wie dieses kann neben anderen Kraftquellen bestehen, ohne dass eine der beiden die andere blockiert.
Gelesene Zustandsfelder
Aus data.inverse3[i].state:
cursor_position.z—vec3, wird zur Berechnung der Eindringtiefe verwendetcurrent_cursor_force— wird für die Telemetrie gemeldet
Senden / Empfangen
Bei jedem Tick: Z-Wert des Cursors auslesen, force_z = max(0, (floor_pos - z) * stiffness) berechnen und einen Befehl set_cursor_force senden. Die erste ausgehende Nachricht enthält außerdem das Session-Profil (alle Varianten) und bei Python zusätzlich configure.preset: arm_front_centered.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Eine einzige asynchrone Schleife. Der Handshake beim ersten Frame enthält Profil und configure.preset, sodass floor_pos = 0.1 mit den Koordinaten der Arbeitsbereichsmitte übereinstimmt.
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 läuft auf dem I/O-Thread von WebSocket; der Hauptthread blockiert, bis ENTER gedrückt wird. Die Varianten C++ verwenden den minimalen Handshake (nur das Session-Profil, ohne Preset).
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 Funktionsrumpf ändert sich. Typisierte Structs sowohl für den Zustand als auch für Befehle. std::optional<session_cmd> hält das einmalige Profil – Glaze lässt es beim Serialisieren von 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
Tutorial 04 wird außerdem lokal mit dem SDK installiert – zu finden unter tutorials/04-haply-inverse-hello-floor/ im Installationsverzeichnis des Dienstes.
Quelle: Python · C++ · C++ Glaze
Verwandte Themen: Steuerbefehle (set_cursor_force) · Montage & Arbeitsbereich (Presets) · Typen (vec3) · Sitzungen · Tutorial 07 (Basis & Montage)