Skip to main content
Version : 3.5.x

08. Configurateur de session à distance

Reconfigurez une session déjà en cours d'exécution ailleurs (dans une autre application, une scène Unity ou une démo Haply ) en envoyant des requêtes HTTP REST à son périphérique. Ce tutoriel n'ouvre aucun WebSocket : il utilise uniquement des requêtes GET, POST et DELETE pour modifier la base, le préréglage de l'espace de travail ou la transformation de montage, tandis que l'autre application continue de générer les sensations haptiques.

Cas d'utilisation

  • Modifiez en temps réel une démo en cours d'exécution. Lancez la démo Haply Orb, puis exécutez ce tutoriel dans un deuxième terminal pour changer la permutation de base, modifier le préréglage de l'espace de travail ou ajuster légèrement la transformation de montage : le repère de coordonnées de l'Orb s'adapte immédiatement sans interrompre la démo.
  • Réglage de l'espace de travail par utilisateur. Laissez la scène haptique tourner sur la machine principale et demandez à un opérateur connecté au même réseau d'envoyer une mount décalage / rotation / mise à l'échelle afin que l'espace de travail virtuel s'aligne avec le bureau de l'utilisateur.
  • Menu des options avec sélection de l'appareil. Ces mêmes aides HTTP permettent d'interroger GET /devices (voir Tutoriel 00) pour répertorier les périphériques et créer un menu interactif — sélectionnez un périphérique, puis reconfigurez-le — sans toucher au WebSocket de la session. Le tutoriel interroge /sessions et les code en dur *inverse/0, mais en passant à un /devicesLe sélecteur basé sur les conditions est une modification locale.
  • Reconfiguration automatisée. Automatisez les étapes de préparation (définition de la base + préréglage + montage) avant le début de l'enregistrement d'une session, sans avoir à intégrer cette configuration dans chaque client.

Conditions préalables

Le tutoriel 08 permet de reconfigurer une session déjà en cours. Vous devez disposer d'une session haptique active — qu'il s'agisse d'un autre tutoriel, d'une scène Unity ou d'une démo Haply .

La manière la plus rapide de lancer une session

Ouvrez Haply et lancez la démo Orb, puis sélectionnez-la directement :

./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"

La scène « Orb » affiche une sphère dans l'espace de travail de l'appareil. En passant d'une base à l'autre, en sélectionnant un préréglage ou en ajustant légèrement la transformation de la monture à l'aide du tutoriel 08, le repère de coordonnées de l'Orb se déplacera visuellement en temps réel.

Utilisation

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

Le tutoriel affiche la base, le préréglage et le montage actuels de la session au démarrage, puis attend que l'utilisateur appuie sur une touche — chaque pression déclenche exactement un appel REST.

Définissez un nom de profil dans votre simulation

Les sessions sans nom de profil ne peuvent être identifiées qu'à l'aide d'un identifiant numérique, qui change à chaque exécution. Faites en sorte que votre application principale appelle session.configure.profile.name lors de son premier message, et vous pouvez réutiliser un sélecteur stable tel que --session :my_profile:0 à chaque exécution. Voir Sessions — nom du profil.

Raccourcis clavier

CléAction
BPermutation par cycles
PPréréglage de l'espace de travail Cycle
W / E / RSélectionner le mode d'édition de la monture — position (mm) / rotation (°) / échelle (%)
/ Incrémenter de −X / +X en mode actuel
/ Incrémenter de +Y / Décrémenter de −Y en mode actuel
Page Up / Page DownIncrémenter de +Z / Diminuer de −Z en mode actuel
= / -Échelle uniforme ± sur les trois axes simultanément (toujours disponible)
DeleteDELETE base + préréglage + montage — rétablir les paramètres par défaut de l'appareil
HAfficher l'aide
EscQuitter (Ctrl+C (ça marche aussi)

Verbes HTTP — GET, POST, DELETE

Ce tutoriel utilise trois verbes HTTP, et uniquement ces trois-là. Chaque requête renvoie la réponse standard Enveloppe JSON ({"ok": true, "data": {...}} en cas de réussite, {"ok": false, "error": "..."} en cas d'échec) et l'un des trois codes d'état suivants : 200 succès, 400 demande incorrecte, 404 Le sélecteur n'a donné aucun résultat.

VerbeRôleChemins utilisés
GETLire l'état actuel — liste des sessions, recherche d'une session spécifique, valeurs de configuration actuelles/sessions, /sessions/<selector>, /<device_selector>/config/{basis,preset,mount}?session=...
POSTRemplacer une valeur de configuration — le corps est au format JSON/<device_selector>/config/{basis,preset,mount}?session=...
DELETERétablir la valeur par défaut de l'appareil pour un paramètre de configuration/<device_selector>/config/{basis,preset,mount}?session=...

Aides HTTP

Une fine couche d'encapsulation autour des trois verbes, de sorte que le reste du tutoriel se présente comme de la logique métier :

Utilisations de Python requests.Session() pour le protocole HTTP Keep-Alive (réduit le temps de latence par requête d'environ 50 ms à environ 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}"

Détection de session — GET /sessions

Branches sur --session:

  • --session SELECTOR étant donné que → un GET /sessions/<SELECTOR>. 200 → utilisez-le ; 404 → générer une erreur.
  • Pas de drapeauGET /sessions (liste) → afficher les sessions avec leurs noms de profil → demander un index → construire le sélecteur final (de préférence :profile:0 si disponible ; sinon, utiliser #id).

SELECTOR accepte toutes les formes définies dans Sélecteurs — Sélecteur de session: :profile:instance, #id, :-1, :0, un nom de profil simple, ou un nom-de-profil avec caractère générique motif de type co.haply.hub::*:0. Le tutoriel transmet la chaîne telle quelle ; le service l'analyse.

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

Sélecteur d'appareil — *inverse/0

Chaque appel de configuration est associé à un périphérique. Le tutoriel utilise un sélecteur de famille avec caractère générique + index:

/*inverse/0/config/<key>
  • *inverse correspond à n'importe quel appareil de la gamme Inverse (inverse3, inverse3x, minverse) — le tutoriel fonctionne de la même manière, quel que soit le modèle utilisé.
  • 0 correspond à l'index (à partir de 0) dans cette famille — le tutoriel n'aborde que le premier Inverse.

Le reciblage se résume à une simple modification de chaîne :

/verse_grip/0/config/basis?session=... # target first wired VerseGrip
/*verse_grip/*/config/basis?session=... # target every grip, wired + wireless
/inverse3/A14/config/mount?session=... # target Inverse3 with id A14

Voir Sélecteurs — Sélecteur de périphériques pour consulter la grammaire complète. Pour créer un menu de sélection de périphériques au lieu d'utiliser du code fixe, utilisez la liste GET /devices?session=<selector> (Tutoriel 00) et raccorder le device_id dans les chemins de configuration.

Configuration POST — base, préréglage, montage

Trois clés, même format de requête, schéma de corps différent. Chaque requête POST renvoie un 200 avec la valeur obtenue dans dataou 404 si le sélecteur de session/appareil n'a donné aucun résultat.

Base

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

{"permutation": "XZY"}

Réponse : {"ok": true, "data": {"permutation": "XZY"}}

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

Préréglage

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

{"preset": "arm_front_centered"}

Réponse : {"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})

Mont

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

Réponse : {"ok": true, "data": {"transform": { ... }}} — reflète la transformation effective après normalisation.

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 et preset s'excluent mutuellement

L'envoi d'une requête efface l'autre sur l'appareil. Le tutoriel ne le précise pas explicitement : chaque requête POST est autonome, et c'est le serveur qui résout le conflit. Voir le tutoriel 07 pour la même règle du côté WebSocket.

Réinitialisation DELETE — trois appels

reset lance une commande DELETE par clé de configuration. Chacune renvoie 200 avec la valeur désormais par défaut dans 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"))

Définition de la rotation de la monture

transform.rotation est un quaternion unitaire en cours de transmission. Le tutoriel stocke la rotation sous la forme d'un triplet d'Euler intrinsèque Z-Y-X (tangage autour de l'axe X, lacet autour de l'axe Z, roulis autour de l'axe Y — tous les degrés) et recompose le quaternion à chaque POST.

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,
}
Convention des quaternions

Quaternion de Hamilton unitaire, à main droite, avec le scalaire en premier (w) — même convention que pour le reste du service, voir quaternion. L'ordre des éléments est Z-Y-X intrinsèque (q = q_z * q_y * q_x) : commencez par faire pivoter l'appareil autour de l'axe X, puis autour de l'axe Y, et enfin autour de l'axe Z.

Le tutoriel affiche le quaternion dérivé à côté du triplet d'Euler sur chaque ligne d'état, ce qui vous permet de vérifier la composition avant que l'appareil ne pivote. L'état d'Euler local commence à (0, 0, 0) quels que soient les éléments déjà présents dans la session — le premier mount POST remplace tout ce qui s'y trouvait.

Modèle d'entrée (résumé)

L'essentiel réside dans la gestion HTTP ; l'expérience utilisateur du clavier est secondaire. Deux raccourcis délibérés :

  • Python utilise le keyboard bibliothèque — multiplateforme, gère nativement les répétitions par maintien de touche. Touches fléchées, Page Up / Page Downet = / - déplacer les axes de montage tout en les maintenant enfoncés ; B et P par cycle et déclenché sur le front montant.
  • C++ utilisations std::getline(std::cin, ...) et une grammaire par jetons compacte (x+20, sx-5, u+10) — moins ergonomique pour les réglages fréquents, mais portable sans #ifdef- en développant des API de console spécifiques à chaque plateforme.

Source

Fourni avec le programme d'installation du SDK

Le tutoriel 08 est également installé localement avec le SDK — regardez dans tutorials/08-haply-inverse-http-remote-config/ dans le répertoire d'installation du service.

Connexes : Sessions — Télécommande · Sélecteurs · Configuration des appareils · Permutation de base · Montage et espace de travail · Conventions JSON · Tutoriel 00 — Liste des appareils · Tutoriel 07 — Base et montage (version WebSocket)