04. Hallo Boden
Ihr erster haptischer Effekt: ein virtueller horizontaler Boden, der zurückdrückt, sobald der Cursor in ihn eindringt. Die Kraft wird durch eine einfache Penalty-Feder dargestellt (stiffness × penetration_depth), die entlang der Z-Achse über set_cursor_force angewendet wird.
Was Sie lernen:
- Verwendung von
set_cursor_force, um eine Straf-Federkraft anzuwenden - Auslesen von
cursor_positionund Berechnung der Kraft in Echtzeit - (Python) Festlegen eines Voreinstellungen für den Arbeitsbereich (
arm_front_centered), sodass der Ursprung in der Mitte des Arbeitsbereichs liegt - (Python) Interaktive Feinabstimmung von Bodenhöhe und Steifigkeit über die Tastatur mithilfe des Pakets
keyboard
Ablauf
- Stellen Sie eine WebSocket-Verbindung zu
ws://localhost:10001und warten Sie auf den ersten Zustandsframe. - Erfassen Sie im ersten Frame das Sitzungsprofil. Die Python-Variante sendet zusätzlich
configure.preset: arm_front_centered, sodass der Ursprung in der Mitte des Arbeitsbereichs liegt; die C++-Varianten 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 Befehl „Kraft“ gesendet – das Sitzungsprofil ist ein einmaliger Handshake.
- (Python) Bei jedem Tick werden außerdem die Pfeiltasten der Tastatur abgefragt, und
floor_pos/stiffnesswerden in Echtzeit aktualisiert.
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 | Einschränkung der Telemetrie |
| Name des Sitzungsprofils | co.haply.inverse.tutorials:hello-floor | Kennzeichnet diese Simulation im „ Haply “-Hub |
Die Python-Variante verwendet das Paket keyboard (Unter Linux sind erweiterte Berechtigungen erforderlich):
↑/↓— Bodenebene anheben / absenken←/→— Steifigkeit verringern / erhöhenR— auf Standardwerte zurücksetzen
Kräfte wirken beim Dienst-Tick additiv – sie werden aus allen Quellen addiert, 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.
Ausgelesene 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 das Profil und configure.preset, sodass floor_pos = 0.1 mit den Koordinaten der Mitte des Arbeitsbereichs ü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 wird blockiert, bis die ENTER-Taste gedrückt wird. Die C++-Varianten 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
Das gleiche libhv-Callback-Modell – nur der Funktionskörper ändert sich. Typisierte Strukturen sowohl für den Zustand als auch für Befehle. std::optional<session_cmd> behält das einmalige Profil bei – 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 (Voreinstellungen) · Typ (vec3) · Sitzungen · Tutorial 07 (Grundlagen & Montage)