04. Hallo Boden
Der erste haptische Effekt ist ein virtueller horizontaler Boden. Wenn der Cursor auf den Boden drückt, übt dieser eine Gegenkraft aus und stößt ihn ab. Die Kraft beträgt stiffness × penetration_depthEs handelt sich um eine einfache Straf-Feder, die auf … basiert, und set_cursor_forcewird entlang der Z-Achse angewendet.
Lerninhalte:
set_cursor_forceAnwendung der Federkraft mithilfe von „Penalty Spring“cursor_positionLesen und Kraftberechnung in Echtzeit- (Python) So, dass der Ursprung in der Mitte des Arbeitsbereichs liegt Arbeitsbereichsvoreinstellungen(
arm_front_centered) Einrichten - (Python)
keyboardAnpassung der Bodenhöhe und Steifigkeit über das Paket mithilfe einer interaktiven Tastatur
Arbeitsablauf
ws://localhost:10001Es wird eine WebSocket-Verbindung geöffnet und auf den ersten Status-Frame gewartet.- Im ersten Bild Sitzungsprofilregistriert. Bei der Python-Variante wird zusätzlich so angepasst, dass der Ursprung in der Mitte des Arbeitsbereichs liegt.
configure.preset: arm_front_centeredwird übertragen, während die C++-Variante die auf dem Gerät bereits aktivierte Konfiguration unverändert übernimmt. - In jedem Einzelbild
cursor_position.znach dem Lesen von,force_z = max(0, (floor_pos - z) * stiffness)nach der Berechnung wird diesset_cursor_forceIch sende es per Befehl erneut. - In den folgenden Ticks werden nur Befehle zur Kraftübertragung gesendet. Das Sitzungsprofil ist ein einmaliger Handshake.
- (Python) Bei jedem Tick werden die Pfeiltasten der Tastatur abgefragt, um
floor_pos/stiffnesswird in Echtzeit aktualisiert.
Parameter
| Name | Standardwert | Verwendungszweck |
|---|---|---|
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 | Begrenzung des Intervalls für die Telemetrieausgabe |
| Name des Sitzungsprofils | co.haply.inverse.tutorials:hello-floor | Haply Diese Simulation im Hub identifizieren |
Python-Varianten sind keyboard Verwenden Sie das Paket (unter Linux sind erweiterte Berechtigungen erforderlich):
↑/↓— Bodenebene anheben / absenken←/→— Abnahme / Zunahme der FestigkeitR— Auf Standardwerte zurücksetzen
Die Kräfte werdenim Service-Tick addiert. Das heißt, die Kräfte aus verschiedenen Quellen werden summiert, bevor sie an das Gerät übertragen werden. Code, der wie in diesem Tutorial geschrieben ist, kann mit anderen Kraftgeneratoren zusammenarbeiten, ohne dass sie sich gegenseitig blockieren.
Feld „Lesestatus“
data.inverse3[i].stateDer dort abgelesene Wert:
cursor_position.z—vec3, zur Berechnung der Eindringtiefe verwendetcurrent_cursor_force— Als Telemetriedaten gemeldet
Senden / Empfangen
Bei jedem Tick wird der Z-Wert des Cursors gelesen, und force_z = max(0, (floor_pos - z) * stiffness)berechnen und anschließend, set_cursor_forcewird gesendet. Die erste gesendete Nachricht enthält (in allen Varianten) das Sitzungsprofil; im Fall von Python ist dies configure.preset: arm_front_centered… ist ebenfalls enthalten.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Es handelt sich um eine einzelne asynchrone Schleife. Beim Handshake des ersten Frames werden das Profil und configure.preset Enthält Einstellungen floor_pos = 0.1Es wird an die Mittelkoordinaten dieses Arbeitsbereichs ausgerichtet.
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-I/O-Thread ausgeführt, während der Hauptthread auf eine ENTER-Eingabe wartet und dabei blockiert wird. Die C++-Variante bietet einen minimalen Handshake (nur Sitzungsprofil, keine Voreinstellungen).
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
Es handelt sich um dasselbe libhv-Callback-Modell; lediglich der Hauptteil ändert sich. Sowohl für den Status als auch für den Befehl werden typisierte Strukturen verwendet. std::optional<session_cmd>Es wird ein einmaliges Profil gespeichert; wenn kein Wert festgelegt ist, lässt Glaze diesen im serialisierten JSON weg.
// 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
Auch Tutorial 04 ist zusammen mit dem SDK lokal installiert. Im Installationsverzeichnis des Dienstes unter tutorials/04-haply-inverse-hello-floor/können Sie dort nachlesen.
Quelle: Python · C++ · C++ Glaze
Verwandte Dokumente: Steuerbefehl (set_cursor_force) · Mount & Workspace (Voreinstellung) · Typ (vec3) · Sitzung · Tutorial 07 (Basis & Halterung)