Zum Hauptinhalt springen

08. Konfigurator für Remote-Sitzungen

Wir rekonstruieren eine Sitzung, die bereits an einem anderen Ort ausgeführt wird– beispielsweise in einer anderen App, einer Unity-Szene oder einer Demoim „Haply Hub “. Dies geschieht durch das Senden von HTTP-REST-Aufrufen an das Gerät. In diesem Tutorial werden keine WebSockets geöffnet. Wir verwenden ausschließlich GET-, POST- und DELETE-Aufrufe, um die Basis, die Arbeitsbereichsvoreinstellungen und die Befestigungstransformation zu ändern, während die andere App weiterhin Haptik-Effekte rendert.

Anwendungsfall​

  • Die laufende Demo live anpassen. Starten Sie zunächst die Orb-Demo im „ Haply Hub“ und führen Sie dann dieses Tutorial in einem zweiten Terminal aus. Wenn Sie die Sortierreihenfolge der Basiselemente umschalten, die Voreinstellungen des Arbeitsbereichs ändern oder die Befestigungstransformation feinjustieren, ändert sich der Koordinatenrahmen von Orb sofort, ohne dass die Demo angehalten werden muss.
  • Anpassung des Arbeitsbereichs für jeden Benutzer. Während die Haptik-Szene weiterhin auf dem Hauptrechner ausgeführt wird, kann ein Bediener im selben Netzwerk mount Ermöglicht das Senden von Versatz, Drehung und Skalierung. Dadurch lässt sich der virtuelle Arbeitsbereich an den Schreibtisch des Benutzers anpassen.
  • Optionsmenü mit Geräteauswahl. Mit demselben HTTP-Helper GET /devices(Tutorial 00 (siehe dort) aufrufen, um Geräte aufzulisten und ein interaktives Menü zu erstellen. Wählen Sie ein Gerät aus dem Menü aus und konfigurieren Sie es neu. Der WebSocket der Sitzung bleibt davon unberührt. Dieses Tutorial /sessions abfragen *inverse/0 ist fest codiert, aber/devices Um ihn durch den Basis-Picker zu ersetzen, reicht eine lokale Änderung aus.
  • Automatisierung per Skript. Vor dem Start der Sitzungsaufzeichnung werden die Vorbereitungsschritte (Einstellungen für Basis, Voreinstellungen und Mount) automatisiert. Es ist nicht erforderlich, die Konfiguration auf jedem einzelnen Client zu installieren.

Voraussetzungen​

In Tutorial 08 wirdeine bereits laufende Sitzungneu konfiguriert. Dazu ist eine aktive Haptik-Sitzung erforderlich. Dabei kann es sich um ein anderes Tutorial, eine Unity-Szene oder eine Demo aus dem „Haply -Hub“ handeln.

So starten Sie eine Sitzung am schnellsten

Haply Öffnen Sie Hub, starten Siedie Orb-Demo und wählen Sie diese dann direkt aus:

./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 zeichnet eine Kugel im Arbeitsbereich des Geräts. Wenn Sie in Tutorial 08 zwischen Basis und Voreinstellungen wechseln oder die Befestigungstransformation feinjustieren, ändert sich das Koordinatensystem von „Orb“ in Echtzeit sichtbar.

Gebrauchsanweisung​

# 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

Dieses Tutorial zeigt beim Start die aktuellen Werte für „basis“, „preset“ und „mount“ der Sitzung an und wartet anschließend auf Tasteneingaben. Bei jedem Tastendruck wirdgenau ein REST-Aufruf gesendet.

Profilnamen in der Simulation festlegen

Sitzungen ohne Profilnamen können nur über eine numerische ID angegeben werden. Diese ID ändert sich bei jeder Ausführung. In der ersten Nachricht der Hauptanwendung session.configure.profile.name Wenn man so vorgeht, dass man … aufruft,--session :my_profile:0 Solche stabilen Selektoren können bei jeder Ausführung wiederverwendet werden. Weitere Informationen finden Sie unter Sitzung — Profilname Siehe dazu.

Tastenbelegung​

SchlüsselBedienung
BDie Permutationen von „basis“ nacheinander vertauschen
PNacheinander zwischen den Arbeitsbereichsvoreinstellungen wechseln
W / E / RMontage-Bearbeitungsmodus auswählen — Position (mm) / Drehung (°) / Skalierung (%)
← / →Im aktuellen Modus in Richtung −X / +X bewegen
↑ / ↓Im aktuellen Modus in Richtung +Y / −Y bewegen
Page Up / Page DownIm aktuellen Modus in Richtung +Z / −Z bewegen
= / -Alle drei Achsen gleichzeitig gleichmäßig vergrößern/verkleinern (±) (jederzeit verfügbar)
DeleteDELETE Setzen Sie „basis“, „preset“ und „mount“ zurück, um die Standardeinstellungen des Geräts wiederherzustellen.
HHilfe anzeigen
EscEnde (Ctrl+C (ist aber auch möglich)

HTTP-Methoden – GET, POST, DELETE​

In diesem Tutorial werden nur drei der HTTP-Methoden verwendet. Alle Aufrufe erfolgen über die Standard- JSON-Envelope gibt zurück (bei Erfolg {"ok": true, "data": {...}}、im Falle eines Fehlers {"ok": false, "error": "..."}). Außerdem kann der Statuscode einer der folgenden drei Typen sein: 200 Erfolg,400 Ungültiges Anfrageformat,404 Es gibt keine Übereinstimmung mit dem Selektor.

MethodeRolleZu verwendender Pfad
GETAktuellen Status abrufen – Sitzungsübersicht, Suche nach einer bestimmten Sitzung, aktuelle Einstellwerte/sessions、/sessions/<selector>、/<device_selector>/config/{basis,preset,mount}?session=...
POSTEinstellungen überschreiben — Der Hauptteil besteht aus JSON/<device_selector>/config/{basis,preset,mount}?session=...
DELETEDie Einstellungen auf die Werkseinstellungen des Geräts zurücksetzen/<device_selector>/config/{basis,preset,mount}?session=...

HTTP-Helfer​

Hier wurden die drei oben genannten Methoden leicht umschlossen, damit der Rest des Tutorials als Kernlogik gelesen werden kann:

In Python gilt für HTTP-Keep-Alive Folgendes: requests.Session() wird verwendet (Verringerung der Latenz pro Anfrage von ca. 50 ms auf ca. 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}"

Erkennung von Sitzungen — GET /sessions​

--session Je nachdem, ob dies angegeben ist oder nicht, verzweigt sich die Verarbeitung:

  • --session SELECTOR wenn dies angegeben wird → GET /sessions/<SELECTOR> Führen Sie dies einmal aus.200 In diesem Fall wird das Ergebnis verwendet, und404 In diesem Fall wird das Programm mit einem Fehler beendet.
  • Ohne Flag → GET /sessions(Liste) ausführen → Sitzung mit Profilnamen anzeigen → Eingabe des Indexes anfordern → den endgültigen Selektor zusammenstellen (sofern verfügbar) :profile:0 hat Vorrang, alles andere #id (als Ausweichlösung).

SELECTOR は Selektor — Sitzungsselektor Akzeptiert alle dort definierten Formate: :profile:instance、#id、:-1、:0, nur den Profilnamen oder co.haply.hub::*:0 wie Platzhalter für Profilnamen Das ist das Muster. In diesem Tutorial wird die Zeichenkette unverändert übergeben, die Analyse erfolgt auf der Serviceseite.

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äteauswahl — *inverse/0​

Alle Aufrufe im Zusammenhang mit der Konfiguration werden gerätebezogen festgelegt. In diesem Tutorial wird der Selektor„Familien-Platzhalter + Index“ verwendet:

/*inverse/0/config/<key>
  • *inverse jedes Gerät, das zur Inverse-Familie gehört (inverse3、inverse3x、minverse) entspricht. Dieses Tutorial funktioniert unverändert, unabhängig vom jeweiligen Modell.
  • 0 ist der 0-basierte Index innerhalb dieser Familie. In diesem Tutorial wird immer nur die erste Inverse bearbeitet.

Um das Ziel zu ändern, muss lediglich eine Zeichenfolge geändert werden.--device Bitte geben Sie Folgendes ein (der Pfad wird daraus zusammengestellt):

/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

:left / :right Die Klasse dient der Indizierung vonvorherGilt für — *inverse:left/0 ist „die 0.“Linkshänder-Seriebedeutet „das Gegenteil von …“ und nicht „das Gegenteil von Nummer 0, also wahrscheinlich das linke“. Weitere Informationen finden Sie unter Selektor – Klassenfilter werden vor der Indizierung aufgelöst Siehe dazu.

Wenn kein passendes Gerät gefunden wird, wird Tutorial 08 beendet.

Das Tutorial wird beim Start GET …/config/basis wird ausgegeben. In der Sitzung --device Wenn kein passendes Gerät gefunden wird (z. B. bei einer Konfiguration, die nur für die rechte Hand vorgesehen ist) --device '*inverse:left/0' (falls angegeben), dann wird der Dienst 404 no device matches selector '<sel>' zurückgibt, und das Tutorial ist Device '<sel>' not found in session '<sel>'. ausgeben und Status 1 und endet damit. Es wird nicht mit den alten Standardwerten in die Tastaturschleife eingetreten.

Was die vollständige Grammatik betrifft, soSelektor – Geräteauswahl Siehe dazu. Um anstelle einer festen Programmierung ein Menü zur Geräteauswahl zu erstellen,GET /devices?session=<selector>(Tutorial 00) eine Liste abrufen und die ausgewählte device_id wird in den Suchpfad aufgenommen.

POST-Konfiguration – Basis, Voreinstellung, Einbindung​

Die drei Schlüssel haben das gleiche Anfrageformat, unterscheiden sich jedoch im Schema des Hauptteils. Alle POST-Anfragen geben den Ergebniswert data enthalten 200 gibt zurück. Wenn kein Eintrag im Sitzungs-/Geräte-Selektor übereinstimmt, 404 gibt zurück.

Grundlage​

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})

Voreinstellung​

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})

Halterung​

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 gültige, normalisierte Transformation unverändert 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 sind sich gegenseitig ausschließend

Wenn eine Seite per POST gesendet wird, wird die andere Seite auf dem Gerät gelöscht. In diesem Tutorial wird dies nicht explizit nachverfolgt. Jeder POST-Vorgang ist unabhängig, und Konflikte werden vom Server gelöst. Eine Erläuterung derselben Regel aus Sicht von WebSocket finden Sie inTutorial 07.

Zurücksetzen mit DELETE — 3 Aufrufe​

reset Es wird für jeden Einstellungsschlüssel einmal die Funktion DELETE aufgerufen. Bei jedem Aufruf wird der Wert, der nun den Standardwert angenommen hat, data enthalten 200 gibt zurück.

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"))

Zusammenstellung der Drehungen der Halterung​

transform.rotation Bei der Datenübertragung handelt es sich um Einheitsquaternionen. In diesem Tutorial werden die Drehungen als Dreiergruppe von Z-Y-X-eigenen Eulerwinkeln (Nick um die X-Achse, Gieren um die Z-Achse, Rollen um die Y-Achse – alle in Grad) gespeichert, und bei jedem POST-Aufruf werden die Quaternionen neu zusammengesetzt.

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,
}
Nutzungsbedingungen von Quaternion

Einheits-Hamilton-Quaternionen, rechtshändiges Koordinatensystem, Skalar zuerst (w). Es gelten dieselben Nutzungsbedingungen wie für die übrigen Bereiche des Dienstes. Weitere Informationen finden Sie unter quaternion Siehe dort. Die Synthesefolge lautetZ-Y-X intrinsisch(q = q_z * q_y * q_x) Das heißt: Zuerst wird die Neigung um die X-Achse angewendet, dann die Rollbewegung um die Y-Achse und schließlich die Gierbewegung um die Z-Achse.

Im Tutorial werden die für jede Statuszeile abgeleiteten Quaternionen zusammen mit einem Dreierpaar von Euler-Winkeln angezeigt, sodass Sie die Zusammensetzung überprüfen können, bevor sich das Gerät dreht. Der lokale Euler-Zustand wird unabhängig von den bereits in der Sitzung gespeicherten Werten (0, 0, 0) beginnt mit. Das erste mount POST Dadurch werden die bisherigen Werte überschrieben.

Eingabemodell (Übersicht)​

Die HTTP-Verkabelung ist entscheidend, die Benutzererfahrung der Tastatur ist zweitrangig. Es gibt zwei Punkte, die bewusst vereinfacht wurden:

  • Python は keyboard Ich verwende ein Paket. Es ist plattformübergreifend und verarbeitet die wiederholte Eingabe durch langes Drücken einer Taste nativ. Pfeiltasten,Page Up / Page Down、= / - bewegt die Befestigungsachse, solange die Taste gedrückt wird.B und P Bei der steigenden Flanke werden nacheinander die Basis und die Voreinstellung umgeschaltet.
  • C++ は std::getline(std::cin, ...) und eine prägnante Token-Grammatik (x+20、sx-5、u+10) verwenden. Auch wenn sich diese für kontinuierliche Feinabstimmungen nur schwer eignet, nutzen wir die Konsolen-APIs der jeweiligen Plattformen, #ifdef Da keine Verzweigungen erforderlich sind, ist das System portierbar.

Quelle​

Im SDK-Installationsprogramm enthalten

Auch Tutorial 08 wurde zusammen mit dem SDK lokal installiert. Es befindet sich im Installationsverzeichnis des Dienstes unter tutorials/08-haply-inverse-http-remote-config/ Bitte überprüfen Sie dies.

Siehe auch: Sitzung – Fernsteuerung · Selektor · Gerätekonfiguration · Basisersetzung · Mounts und Arbeitsbereiche · JSON-Konventionen · Tutorial 00 – Geräteliste · Tutorial 07 – Basis und Mounts (WebSocket-Version)