04. Hallo, Boden
Ihr erster haptischer Effekt: ein virtueller horizontaler Boden, der zurückdrückt, wenn der Cursor darauf drückt. Die Kraft entspricht einer einfachen Federkraft — stiffness × penetration_depth — Applikation nach Z über set_cursor_force.
Was Sie lernen werden:
- Verwenden
set_cursor_forceum eine Strafkraft anzuwenden - Lesen
cursor_positionund die Kraft in Echtzeit berechnen - (Python) Definieren eines Voreinstellung des Arbeitsbereichs (
arm_front_centered), damit der Ursprung in der Mitte des Arbeitsbereichs liegt - (Python) Interaktive Einstellung der Bodenhöhe und der Steifigkeit über die Tastatur mithilfe des Pakets
keyboard
Ablauf
- Stellen Sie eine WebSocket-Verbindung zu
ws://localhost:10001und warten Sie auf den ersten Status-Frame. - Im ersten Rahmen: Speichern Sie das Sitzungsprofil. Die Python-Variante sendet darüber hinaus
configure.preset: arm_front_centereddamit der Ursprung in der Mitte des Arbeitsbereichs liegt; die C++-Varianten verwenden die auf dem Gerät bereits aktive Konfiguration. - Bei jedem Bild: Lesen Sie
cursor_position.z, berechnen Sieforce_z = max(0, (floor_pos - z) * stiffness)und senden Sie sie als Bestellung zurückset_cursor_force. - Die folgenden Zyklen senden nur den Befehl zur Kraftübertragung – das Sitzungsprofil ist ein einmaliger Handshake.
- (Python) In jedem Durchlauf werden auch die Pfeiltasten der Tastatur abgefragt und aktualisiert
floor_pos/stiffnesslive.
Einstellungen
| Nom | Standardwert | Rolle |
|---|---|---|
floor_pos | 0.10 m | Z-Koordinate der virtuellen Bodenebene |
stiffness | 1000 k. A. | Steifigkeitskonstante (Eindringtiefe von 1 mm → 1 N) |
PRINT_EVERY_MS | 100–200 | Obergrenze für die Telemetrie-Aktualisierungsrate |
| Name des Sitzungsprofils | co.haply.inverse.tutorials:hello-floor | Identifiziere diese Simulation im „ Haply “-Hub |
Die Python-Variante verwendet das Paket keyboard (unter Linux sind erweiterte Berechtigungen erforderlich):
↑/↓— die Bodenplatte anheben / absenken←/→— die Steifigkeit verringern / erhöhenR— Standardwerte wiederherstellen
Die Kräfte addieren sich in jedem Dienstzyklus: Sie werden aus allen Quellen addiert, bevor sie an das Gerät weitergeleitet werden. Ein Tutorial wie dieses kann neben anderen Kraftquellen bestehen, ohne dass sich diese gegenseitig blockieren.
Meister der Les-États
Seit data.inverse3[i].state :
cursor_position.z—vec3, das zur Berechnung der Eindringtiefe verwendet wirdcurrent_cursor_force— für die Telemetrie gekennzeichnet
Versand / Empfang
In jedem Zyklus: Lesen Sie die Z-Koordinate des Cursors ab und berechnen Sie force_z = max(0, (floor_pos - z) * stiffness) und geben Sie eine Bestellung auf set_cursor_force. Die erste ausgehende Nachricht enthält außerdem das Sitzungsprofil (alle Varianten) und, für die Python-Variante, configure.preset: arm_front_centered.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Einzigartige asynchrone Schleife. Der Handshake des ersten Frames überträgt das Profil und configure.preset damit floor_pos = 0.1 richtet sich nach den Koordinaten des Mittelpunkts des Arbeitsbereichs 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-Beispiel – „onmessage“ wird auf dem WebSocket-E/A-Thread ausgeführt; der Hauptthread bleibt blockiert, bis die Eingabetaste gedrückt wird. Die C++-Varianten bieten den minimalen Handshake (nur Sitzungsprofil, ohne 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
Dasselbe Modell wie bei den libhv-Callbacks – nur der Hauptteil ändert sich. Typisierte Strukturen für den Status und die Befehle. std::optional<session_cmd> enthält das Einmal-Profil; Glaze überspringt die JSON-Serialisierung, wenn es nicht definiert 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
Das Tutorial 04 wird ebenfalls lokal mit dem SDK installiert – siehe tutorials/04-haply-inverse-hello-floor/ im Installationsverzeichnis des Dienstes.
Quelle: Python · C++ · C++ Glaze
Siehe auch: Steuerbefehle (set_cursor_force) · Montage und Arbeitsbereich (Voreinstellungen) · Typen (vec3) · Sitzungen · Tutorial 07 (Grundlagen und Montage)