Zum Hauptinhalt springen

08. Remote-Session-Konfigurator

Konfigurieren Sie eine bereits anderswo laufende Session neu – etwa eine andere App, eine Szene in Unity oder eine Demo von Haply Hub –, indem Sie per HTTP REST-Aufrufe gegen deren Gerät absetzen. Dieses Tutorial öffnet keine WebSocket: Es werden nur GET-, POST- und DELETE-Anfragen gesendet, um Basis, Workspace-Preset oder Mount-Transformation zu ändern, während die andere App weiterhin Haptik ausgibt.

Anwendungsfälle​

  • Laufende Demo live anpassen. Starten Sie die Orb-Demo in Haply Hub und führen Sie dann dieses Tutorial in einem zweiten Terminal aus, um die Basis-Permutation zu tauschen, das Workspace-Preset zu ändern oder die Mount-Transformation leicht zu verschieben – das Koordinatensystem der Orb-Szene verschiebt sich sofort, ohne dass die Demo gestoppt werden muss.
  • Workspace-Kalibrierung pro Benutzer. Lassen Sie die haptische Szene auf dem Hauptrechner laufen und ermöglichen Sie einem Bediener im selben Netzwerk, einen mount-Offset / eine Rotation / Skalierung zu übermitteln, damit der virtuelle Workspace mit dem Schreibtisch des Benutzers übereinstimmt.
  • Optionsmenü mit Geräteauswahl. Dieselben HTTP-Helfer können GET /devices abfragen (siehe Tutorial 00), um Geräte aufzulisten und ein interaktives Menü aufzubauen – ein Gerät auswählen und anschließend neu konfigurieren –, ohne das WebSocket der Session anzutasten. Das Tutorial fragt /sessions ab und hartkodiert *inverse/0; der Wechsel zu einer über /devices gesteuerten Auswahl ist jedoch eine lokale Änderung.
  • Skriptgesteuerte Neukonfiguration. Automatisieren Sie Vorabschritte (Basis, Preset und Mount setzen), bevor eine Session mit der Aufzeichnung beginnt, ohne die Einrichtung in jedem Client einzubetten.

Voraussetzungen​

Tutorial 08 konfiguriert eine bereits laufende Session neu. Sie benötigen eine beliebige aktive haptische Session – ein anderes Tutorial, eine Szene in Unity oder eine Demo in Haply Hub.

Schnellster Weg, eine Session zu starten

Öffnen Sie Haply Hub, starten Sie die Orb-Demo und sprechen Sie sie anschließend direkt an:

./08-haply-inverse-http-remote-config --session co.haply.hub::demo-orb
python 08-haply-inverse-http-remote-config.py --session "co.haply.hub::demo-orb"

Die Orb-Szene rendert eine Kugel im Workspace des Geräts. Wenn Sie mit Tutorial 08 Basis oder Preset durchschalten oder die Mount-Transformation leicht verschieben, verschiebt sich das Koordinatensystem der Orb-Szene sichtbar in Echtzeit.

Verwendung​

# Pick a session interactively (lists every session the service knows)
./08-haply-inverse-http-remote-config
python 08-haply-inverse-http-remote-config.py

# Target the Haply Hub Orb demo directly
./08-haply-inverse-http-remote-config --session co.haply.hub::demo-orb
python 08-haply-inverse-http-remote-config.py --session "co.haply.hub::demo-orb"

# Target one directly by selector
./08-haply-inverse-http-remote-config --session :my_profile:0
python 08-haply-inverse-http-remote-config.py --session "#42"

# Or by a wildcard profile pattern (first match) — handy when the exact profile is unknown
./08-haply-inverse-http-remote-config --session "co.haply.hub::*:0"

# Pick a specific device on the session (default is *inverse/0)
./08-haply-inverse-http-remote-config --device 'inverse3/#A12' # by exact id
./08-haply-inverse-http-remote-config --device '*inverse:left/0' # first left-handed inverse
./08-haply-inverse-http-remote-config --device '*inverse:right/0' # first right-handed inverse
./08-haply-inverse-http-remote-config --device '*verse_grip/0' # first grip-family device

Das Tutorial zeigt beim Start die aktuelle Basis / das Preset / den Mount der Sitzung an und wartet dann auf Tastendrücke — jeder Tastendruck sendet genau einen REST-Aufruf.

Legen Sie in Ihrer Simulation einen Profilnamen fest

Sitzungen ohne Profilnamen können nur über eine numerische ID angesprochen werden — und diese ändert sich bei jedem Lauf. Lassen Sie Ihre Hauptanwendung bei ihrer ersten Nachricht session.configure.profile.name aufrufen; danach können Sie einen stabilen Selektor wie --session :my_profile:0 über alle Läufe hinweg wiederverwenden. Siehe Sitzungen — Profilname.

Tastenbelegung​

TasteAktion
BBasis-Permutation durchschalten
PWorkspace-Preset durchschalten
W / E / RMount-Bearbeitungsmodus wählen — Position (mm) / Rotation (°) / Skalierung (%)
← / →Im aktuellen Modus einen Schritt −X / +X
↑ / ↓Im aktuellen Modus einen Schritt +Y / −Y
Page Up / Page DownIm aktuellen Modus einen Schritt +Z / −Z
= / -Gleichmäßige Skalierung ± auf allen drei Achsen gleichzeitig (immer verfügbar)
DeleteDELETE für Basis + Preset + Mount — Zurücksetzen auf die Gerätestandards
HHilfe anzeigen
EscBeenden (Ctrl+C funktioniert ebenfalls)

HTTP-Methoden — GET, POST, DELETE​

Das Tutorial verwendet drei HTTP-Methoden und nur diese drei. Jeder Aufruf liefert den standardmäßigen JSON-Envelope ({"ok": true, "data": {...}} bei Erfolg, {"ok": false, "error": "..."} bei Fehler) sowie einen von drei Statuscodes: 200 Erfolg, 400 fehlerhafte Anfrage, 404 Selektor hat nichts gefunden.

MethodeZweckVerwendete Pfade
GETAktuellen Zustand lesen — Sitzungsliste, gezielte Sitzungsabfrage, aktuelle Konfigurationswerte/sessions, /sessions/<selector>, /<device_selector>/config/{basis,preset,mount}?session=...
POSTEinen Konfigurationswert ersetzen — Request-Body ist JSON/<device_selector>/config/{basis,preset,mount}?session=...
DELETEEinen Konfigurationswert auf den Gerätestandard zurücksetzen/<device_selector>/config/{basis,preset,mount}?session=...

HTTP-Hilfsfunktionen​

Ein dünner Wrapper um die drei Methoden, damit sich der Rest des Tutorials wie reine Anwendungslogik liest:

Python verwendet requests.Session() für HTTP-Keep-Alive (verkürzt die Latenz pro Anfrage von ~50 ms auf ~5 ms):

http = requests.Session()

def api_get(path):
r = http.get(f"{BASE_URL}{path}", timeout=3)
return r.json() if r.status_code == 200 else None

def api_post(path, body):
r = http.post(f"{BASE_URL}{path}", json=body, timeout=3)
return r.json() if r.status_code == 200 else None

def api_delete(path):
r = http.delete(f"{BASE_URL}{path}", timeout=3)
return r.json() if r.status_code == 200 else None

def session_url(endpoint):
return f"{endpoint}?session={session_selector}"

Sitzungsermittlung — GET /sessions​

Verzweigt anhand von --session:

  • --session SELECTOR angegeben → ein GET /sessions/<SELECTOR>. 200 → verwenden; 404 → Fehler ausgeben.
  • Kein Flag → GET /sessions (Auflistung) → Sitzungen mit Profilnamen anzeigen → nach einem Index fragen → den endgültigen Selektor erstellen (bevorzugt :profile:0, wenn verfügbar; andernfalls #id).

SELECTOR akzeptiert jede Form, die unter Selektoren — Sitzungsselektor definiert ist: :profile:instance, #id, :-1, :0, einen einfachen Profilnamen oder ein Profilnamen-Wildcard-Muster wie co.haply.hub::*:0. Das Tutorial übergibt die Zeichenkette unverändert; der Dienst parst sie.

def discover_session(session_arg):
global session_selector

if session_arg:
# Direct lookup (e.g. ":my_profile:0", "#42", ":-1")
if api_get(f"/sessions/{session_arg}") is None:
return False
session_selector = session_arg
return True

# Otherwise: list and pick
data = api_get("/sessions")
sessions = data.get("data", {}).get("sessions", [])
for i, s in enumerate(sessions):
name = s.get("config", {}).get("profile", {}).get("name", "default")
print(f" [{i}] session #{s['session_id']} profile={name}")

picked = sessions[int(input("Pick session index: "))]
name = picked.get("config", {}).get("profile", {}).get("name", "")
# Prefer the profile selector — it survives restarts; id doesn't
session_selector = (f":{name}:0" if name and name != "default"
else f"#{picked['session_id']}")
return True

Geräte-Selektor — *inverse/0​

Jeder Konfigurationsaufruf ist auf ein Gerät beschränkt. Das Tutorial verwendet einen Selektor aus Familien-Wildcard + Index:

/*inverse/0/config/<key>
  • *inverse passt auf jedes Gerät der Inverse-Familie (inverse3, inverse3x, minverse) — das Tutorial funktioniert unverändert, unabhängig vom konkreten Modell.
  • 0 ist der nullbasierte Index innerhalb dieser Familie — das Tutorial spricht immer nur das erste Inverse an.

Das Umstellen auf ein anderes Gerät erfordert nur eine Zeichenkettenänderung — übergeben Sie --device (der Pfad wird daraus erstellt):

/verse_grip/0/config/basis?session=... # --device verse_grip/0
/*verse_grip/0/config/basis?session=... # --device '*verse_grip/0' — first grip, any kind
/inverse3/A14/config/mount?session=... # --device 'inverse3/#A14'
/*inverse:left/0/config/basis?session=... # --device '*inverse:left/0' — first left-handed inverse
/*inverse:right/0/config/basis?session=... # --device '*inverse:right/0' — first right-handed inverse

Die Klasse :left / :right wird vor der Indizierung angewendet — *inverse:left/0 bedeutet „die 0-te linkshändige Inverse“, nicht „die 0-te Inverse, hoffentlich links“. Siehe Selektoren — Klassenfilter werden vor der Indizierung aufgelöst.

Tutorial 08 wird beendet, wenn das angesprochene Gerät fehlt

Das Tutorial stellt beim Start eine GET …/config/basis-Anfrage. Hat die Sitzung kein Gerät, das --device entspricht (z. B. --device '*inverse:left/0' bei einem nur rechtshändigen Aufbau), antwortet der Dienst mit 404 no device matches selector '<sel>'; das Tutorial gibt Device '<sel>' not found in session '<sel>'. aus und beendet sich mit dem Status 1. Dabei wird keine Tastaturschleife mit veralteten Standardwerten gestartet.

Die vollständige Syntax finden Sie unter Selektoren — Gerätselektor. Um ein Geräte-Auswahlmenü statt fest codierter Werte zu erstellen, zählen Sie die Geräte mit GET /devices?session=<selector> auf (Tutorial 00) und übernehmen die gewählte device_id in die Konfigurationspfade.

POST-Konfiguration — basis, preset, mount​

Drei Schlüssel mit gleichem Anfrageaufbau, aber unterschiedlichem Body-Schema. Jeder POST liefert ein 200 mit dem resultierenden Wert in data, oder ein 404, wenn Sitzungs- oder Gerätselektor keinen Treffer ergab.

Basis​

POST /*inverse/0/config/basis?session=:my_profile:0
Content-Type: application/json

{"permutation": "XZY"}

Antwort: {"ok": true, "data": {"permutation": "XZY"}}

def post_basis():
perm, _ = BASIS_OPTIONS[basis_index]
api_post(session_url("/inverse3/0/config/basis"), {"permutation": perm})

Preset​

POST /*inverse/0/config/preset?session=:my_profile:0
Content-Type: application/json

{"preset": "arm_front_centered"}

Antwort: {"ok": true, "data": {"preset": "arm_front_centered"}}

def post_preset():
preset = PRESET_OPTIONS[preset_index]
api_post(session_url("/inverse3/0/config/preset"), {"preset": preset})

Mount​

POST /*inverse/0/config/mount?session=:my_profile:0
Content-Type: application/json

{
"transform": {
"position": {"x": 0.02, "y": 0.0, "z": 0.0},
"rotation": {"w": 0.966, "x": 0.0, "y": 0.259, "z": 0.0},
"scale": {"x": 1.0, "y": 1.0, "z": 1.0}
}
}

Antwort: {"ok": true, "data": {"transform": { ... }}} — gibt die effektive Transformation nach der Normalisierung zurück.

def post_mount():
body = {
"transform": {
"position": {"x": mount_pos[0], "y": mount_pos[1], "z": mount_pos[2]},
"rotation": quat_from_euler_deg(*mount_rot),
"scale": {"x": mount_scale[0], "y": mount_scale[1], "z": mount_scale[2]},
}
}
api_post(session_url("/inverse3/0/config/mount"), body)
mount und preset schließen sich gegenseitig aus

Ein POST für eines der beiden Felder löscht auf dem Gerät das jeweils andere. Das Tutorial verfolgt dies nicht explizit — jeder POST ist eigenständig, und der Server löst den Konflikt auf. Siehe Tutorial 07 für dieselbe Regel aus Sicht von WebSocket.

DELETE-Zurücksetzung — drei Aufrufe​

reset sendet pro Konfigurationsschlüssel einen DELETE. Jeder liefert 200 mit dem nun standardmäßigen Wert in data.

def reset_all():
api_delete(session_url("/inverse3/0/config/basis"))
api_delete(session_url("/inverse3/0/config/preset"))
api_delete(session_url("/inverse3/0/config/mount"))

Zusammensetzen der Mount-Rotation​

transform.rotation ist in der Übertragung ein Einheitsquaternion. Das Tutorial speichert die Rotation als intrinsisches Euler-Tripel in der Reihenfolge Z-Y-X (Nicken um X, Gieren um Z, Rollen um Y — alle in Grad) und setzt das Quaternion bei jedem POST neu zusammen.

def quat_from_euler_deg(pitch_x, yaw_z, roll_y):
"""Hamilton quaternion for q = q_z * q_y * q_x (apply X, then Y, then Z)."""
hx, hy, hz = (math.radians(a) * 0.5 for a in (pitch_x, roll_y, yaw_z))
cx, sx = math.cos(hx), math.sin(hx)
cy, sy = math.cos(hy), math.sin(hy)
cz, sz = math.cos(hz), math.sin(hz)
return {
"w": cz*cy*cx + sz*sy*sx,
"x": cz*cy*sx - sz*sy*cx,
"y": cz*sy*cx + sz*cy*sx,
"z": sz*cy*cx - cz*sy*sx,
}
Quaternion-Konvention

Einheitsquaternion nach Hamilton, rechtshändig, skalar-zuerst (w) — dieselbe Konvention wie im restlichen Dienst, siehe quaternion. Die Kompositionsreihenfolge ist Z-Y-X intrinsisch (q = q_z * q_y * q_x): Wenden Sie zuerst die Drehung um X (Pitch) an, danach die Drehung um Y (Roll) und zuletzt die Drehung um Z (Yaw).

Das Tutorial gibt bei jeder Statuszeile das abgeleitete Quaternion zusammen mit dem Euler-Tripel aus, damit Sie die Komposition überprüfen können, bevor sich das Gerät dreht. Der lokale Euler-Zustand beginnt bei (0, 0, 0), unabhängig davon, was die Session bereits enthält — der erste mount POST überschreibt den vorhandenen Wert.

Eingabemodell (kurz)​

Die Anbindung von HTTP steht im Mittelpunkt; die Tastatursteuerung ist zweitrangig. Zwei bewusste Abkürzungen:

  • Python verwendet das Paket keyboard — plattformübergreifend und mit nativer Behandlung von Tastenwiederholungen beim Halten. Pfeiltasten, Page Up / Page Down sowie = / - verschieben die Mount-Achsen, solange die Taste gedrückt wird; B und P wechseln bei der ansteigenden Flanke zyklisch durch Basis und Preset.
  • C++ verwendet std::getline(std::cin, ...) und eine kompakte Token-Grammatik (x+20, sx-5, u+10) — für fortlaufende Feinanpassungen weniger komfortabel, dafür ohne plattformspezifische #ifdef-Verzweigungen für Konsolen-APIs portierbar.

Quellcode​

Im SDK-Installer enthalten

Tutorial 08 wird auch lokal mit dem SDK installiert — suchen Sie im Installationsverzeichnis des Dienstes unter tutorials/08-haply-inverse-http-remote-config/.

Verwandt: Sessions — Fernsteuerung · Selektoren · Gerätekonfiguration · Basis-Permutation · Mount & Workspace · JSON-Konventionen · Tutorial 00 — Geräteliste · Tutorial 07 — Basis & Mount (Version WebSocket)