04. Hallo Boden
Dein erster taktiler Effekt: ein virtueller, ebener Boden, der eine Gegenkraft erzeugt, wenn der Cursor darauf drückt. Diese Kraft basiert auf einem einfachen Federmodell –stiffness × penetration_depth——durch set_cursor_force Entlang der Z-Achse aufbringen.
Du wirst Folgendes lernen:
- Verwendung
set_cursor_forceeine Strafkraft auf die Feder ausüben - Lesen
cursor_positionund die Kraft in Echtzeit berechnen - (Python) Einstellungen Arbeitsbereichsvoreinstellungen(
arm_front_centered), sodass der Ursprung im Zentrum des Arbeitsraums liegt - (Python) Durch
keyboardInteraktive Anpassung der Bodenhöhe und -steifigkeit
Arbeitsablauf
- Öffnen bis
ws://localhost:10001WebSocket und warte auf den ersten Status-Frame. - Im ersten Frame: Registrierung Sitzungsprofil。Die Python-Version sendet außerdem
configure.preset: arm_front_centered, sodass der Ursprung in der Mitte des Arbeitsraums liegt; die C++-Version verwendet hingegen die aktuell auf dem Gerät aktivierte Konfiguration. - Jedes Einzelbild: Einlesen
cursor_position.z, Berechnungforce_z = max(0, (floor_pos - z) * stiffness), und durchset_cursor_forceBefehl, ihn zurückzusenden. - In jedem folgenden Zyklus werden nur Befehle gesendet – das Sitzungsprofil dient lediglich als einmaliger Handshake.
- (Python) In jedem Zyklus werden außerdem die Pfeiltasten der Tastatur abgefragt und die Anzeige in Echtzeit aktualisiert.
floor_pos/stiffness。
Parameter
| Bezeichnung | Standardwert | Verwendungszweck |
|---|---|---|
floor_pos | 0.10 m | Z-Koordinate der virtuellen Bodenebene |
stiffness | 1000 k. A. | Federkonstante (Durchdringung von 1 mm → 1 N) |
PRINT_EVERY_MS | 100–200 | Fernsteuerung der Drosselung |
| Name des Sitzungsprofils | co.haply.inverse.tutorials:hello-floor | Diese Simulation im „ Haply Hub“ kennzeichnen |
Verwendung der Python-Version keyboard Paket (unter Linux sind erweiterte Berechtigungen erforderlich):
↑/↓——Anheben / Absenken der Bodenebene←/→——Steifigkeit verringern / erhöhenR——Auf Standardwerte zurücksetzen
Während des Dienstzyklus sind Kräfteaddierbar– bevor sie an das Gerät gesendet werden, werden die Kräfte aus den verschiedenen Quellen zusammengefasst. Beispiele wie dieses Tutorial können neben anderen Kraftquellen existieren, ohne sich gegenseitig zu behindern.
Gelesene Statusfelder
Aus data.inverse3[i].state:
cursor_position.z—vec3, zur Berechnung der Eindringtiefecurrent_cursor_force— zur Fernmessung und Datenübermittlung
Senden / Empfangen
In jedem Zyklus: Die Z-Koordinate des Cursors auslesen und berechnen force_z = max(0, (floor_pos - z) * stiffness), und sende eine set_cursor_force Befehl. Die in Artikel 1 gesendete Nachricht enthält außerdem ein Sitzungskonfigurationsprofil (alle Versionen) sowie – bei Python-Versionen – zusätzlich configure.preset: arm_front_centered。
- Python
- C++ (nlohmann)
- C++ (Glaze)
Einzelner asynchroner Zyklus. Der Handshake im ersten Frame enthält das Profil und configure.preset, damit floor_pos = 0.1 An den Koordinaten des Arbeitsbereichszentrums ausrichten.
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; der Hauptthread wartet blockierend auf die ENTER-Taste. Die C++-Variante bietet nur den minimalistischen Handshake-Ablauf (umfasst lediglich die Sitzungskonfiguration, 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
Genau wie beim libhv-Callback-Modell – lediglich der Hauptteil unterscheidet sich. Sowohl Zustände als auch Befehle verwenden stark typisierte Strukturen.std::optional<session_cmd> Einmalige Konfigurationsdatei speichern – Wenn diese Option nicht festgelegt ist, lässt Glaze sie aus dem 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
Tutorial 04 wird ebenfalls zusammen mit dem SDK lokal installiert – bitte im Installationsverzeichnis des Dienstes unter tutorials/04-haply-inverse-hello-floor/ dort suchen.
Quellcode: Python · C++ · C++ Glaze
Verwandte Themen: Steuerbefehle (set_cursor_force) · Einbindungen und Arbeitsbereiche (Voreinstellungen) · Typ (vec3) · Gespräch · Tutorial 07 (Sockel und Befestigung)