Aller au contenu principal

08. Configurateur de session à distance

Reconfigurez une session déjà en cours d'exécution ailleurs — dans une autre application, une scène Unity, une démo Haply Hub — en envoyant des requêtes REST HTTP à son appareil. Ce tutoriel n'ouvre aucun WebSocket : il effectue uniquement des requêtes GET, POST et DELETE pour modifier la base de données, 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 retours haptiques.

Cas d'utilisation​

  • Ajustement en temps réel d'une démo en cours d'exécution. Lancez la démo « Haply Hub Orb », puis exécutez ce tutoriel dans un deuxième terminal pour modifier la permutation de base, changer 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 se déplace immédiatement, sans interrompre la démo.
  • Étalonnage de l'espace de travail par utilisateur. Gardez la scène haptique active sur la machine principale et laissez un opérateur du même réseau transmettre un décalage, une rotation ou une mise à l'échelle de mount afin que l'espace de travail virtuel s'aligne sur le bureau de l'utilisateur.
  • Menu de sélection de l'appareil. Ces mêmes fonctions utilitaires HTTP peuvent interroger GET /devices (voir le tutoriel 00) pour répertorier les appareils et créer un menu interactif — choisir un appareil, puis le reconfigurer — sans modifier le WebSocket de la session. Le tutoriel demande /sessions et fixe *inverse/0 dans le code, mais remplacer ce choix par un sélecteur piloté par /devices ne nécessite qu'une modification locale.
  • Configuration scriptée. Automatisez les étapes préalables (définition de la base, du préréglage et du montage) avant qu'une session ne commence l'enregistrement, sans avoir à intégrer la configuration dans chaque client.

Prérequis​

Le tutoriel n° 8 permet de reconfigurer une session déjà en cours d'exécution. Vous devez disposer d'une session haptique active, quelle qu'elle soit : un autre tutoriel, une scène Unity ou une démo de l'Haply Hub.

Le moyen le plus rapide de démarrer une session

Ouvrez l'Haply Hub 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 parcourant en boucle la base ou le préréglage, ou en ajustant légèrement la transformation de montage à l'aide du tutoriel 08, vous déplacerez visuellement le repère de coordonnées de l'Orb 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"

# 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

Au démarrage, le tutoriel affiche la base, le préréglage et la configuration actuels de la session, puis attend que l'utilisateur appuie sur une touche — chaque pression sur une touche envoie un seul 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 appeler session.configure.profile.name par votre application principale dès son premier message : vous pourrez alors réutiliser un sélecteur stable tel que --session :my_profile:0 à chaque exécution. Voir Sessions — nom de profil.

Raccourcis clavier​

ToucheAction
BDécouvrir les permutations de base
PParcourir les préréglages d'espace de travail
W / E / RSélectionner le mode d'édition du montage — position (mm) / rotation (°) / échelle (%)
← / →Avancer de −X / +X dans le mode actuel
↑ / ↓Avancer de +Y / −Y dans le mode actuel
Page Up / Page DownAvancer de +Z / −Z dans le mode actuel
= / -Mise à l'é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
EscLâcheur (Ctrl+C (ça fonctionne aussi)

Verbes HTTP — GET, POST, DELETE​

Ce tutoriel utilise trois verbes HTTP, et uniquement ceux-là. Chaque requête renvoie l'enveloppe JSON standard ({"ok": true, "data": {...}} en cas de réussite, {"ok": false, "error": "..."} (en cas d'échec) ainsi que l'un des trois codes d'état suivants : 200 succès, 400 requête mal formulée, 404 Aucun résultat pour le sélecteur.

VerbeRôleChemins utilisés
GETConsulter 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 d'un paramètre de configuration de l'appareil/<device_selector>/config/{basis,preset,mount}?session=...

Fonctions utilitaires HTTP​

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

Python utilise requests.Session() pour le keep-alive HTTP (réduit la 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}"

Présentation des sessions — GET /sessions​

Le comportement dépend de --session :

  • --session SELECTOR fourni → un seul GET /sessions/<SELECTOR>. 200 → l'utiliser ; 404 → signaler une erreur et s'arrêter.
  • Aucune option → GET /sessions (liste) → afficher les sessions avec leur nom de profil → demander un index → construire le sélecteur final (privilégier :profile:0 lorsqu'il est 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 motif avec des caractères génériques dans le nom du profil comme co.haply.hub::*:0. Le tutoriel transmet la chaîne telle quelle ; c'est le service qui 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 concerne un appareil précis. Le tutoriel utilise un sélecteur générique de type « famille + index » :

/*inverse/0/config/<key>
  • *inverse correspondent à n'importe quel appareil de la gamme Inverse (inverse3, inverse3x, minverse) — Le tutoriel fonctionne tel quel, quel que soit le modèle utilisé.
  • 0 est l'indice, à partir de zéro, dans cette famille — le tutoriel ne traite que du premier Inverse.

Pour changer de cible, il suffit d'apporter une seule modification à la chaîne : passez à --device (le chemin est déjà tracé) :

/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

La classe :left / :right est appliquée avant l'indexation — *inverse:left/0 désigne « l'inverse 0e » gaucher », et non « l'inverse 0e, espérons-le gaucher ». Voir Sélecteurs — les filtres de classe sont résolus avant l'indexation.

Le tutoriel n° 8 se termine lorsque l'appareil cible est introuvable

Le tutoriel affiche un GET …/config/basis au démarrage. Si la session ne contient aucun appareil correspondant à --device (par ex. --device '*inverse:left/0' (avec une configuration exclusivement pour droitiers), le service répond 404 no device matches selector '<sel>' ; le tutoriel affiche Device '<sel>' not found in session '<sel>'. et se termine par le statut 1. Aucune boucle clavier n'est lancée avec des valeurs par défaut obsolètes.

Consultez Sélecteurs — sélecteur d'appareil pour la grammaire complète. Pour créer un menu de sélection d'appareil au lieu d'utiliser une valeur fixe, énumérez les appareils avec GET /devices?session=<selector> (Tutoriel 00) et intégrez le device_id sélectionné dans les chemins de configuration.

POST de configuration — base, préréglage, montage​

Trois clés de configuration, même structure de requête, schéma de corps différent. Chaque requête POST renvoie un 200 avec la valeur obtenue dans data, ou un 404 si le sélecteur de session ou d'appareil ne correspond à aucun élément.

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": { ... }}} — renvoie 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

Une requête POST sur l'une des deux clés efface l'autre sur le périphérique. Le tutoriel ne le précise pas explicitement : chaque requête POST est indépendante, 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 déclenche une opération DELETE par clé de configuration. Chacune renvoie un 200 avec la valeur par défaut rétablie 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"))

Composition de la rotation du montage​

transform.rotation est un quaternion unitaire transmis sur le réseau. 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 exprimés en degrés) et reconstitue 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, dans un repère droitier, avec la partie scalaire en premier (w) — même convention que pour le reste du service, voir quaternion. L'ordre de composition est le suivant : Z-Y-X intrinsèque (q = q_z * q_y * q_x) : on applique d'abord le tangage autour de l'axe X, puis le roulis autour de l'axe Y, puis le lacet autour de l'axe Z.

Le tutoriel affiche le quaternion dérivé à côté du triplet d'Euler sur chaque ligne d'état, afin que vous puissiez vérifier la composition avant que le périphérique ne tourne. L'état d'Euler local commence à (0, 0, 0), quel que soit l'état déjà présent dans la session — le premier mount POST remplace la valeur actuelle.

Modèle d'entrée (court)​

L'intégration du protocole HTTP est essentielle ; l'ergonomie du clavier est secondaire. Deux choix délibérés :

  • Python utiliser le paquet keyboard — multiplateforme, il prend en charge nativement la répétition des touches maintenues enfoncées. Les touches fléchées, Page Up / Page Down et = / - déplacent les axes de montage tant qu'elles sont maintenues ; B et P pour faire défiler les valeurs de « basis » et de « preset » sur le front montant.
  • C++ utiliser std::getline(std::cin, ...) et une grammaire compacte à jetons (x+20, sx-5, u+10) — moins ergonomique pour les réglages en continu, mais portable sans avoir recours à des #ifdef propres à chaque plateforme pour les API de console.

Source​

Inclus dans le programme d'installation du SDK

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

Voir aussi : Sessions — Contrôle à distance · Sélecteurs · Configuration des périphériques · Permutation de la base · Montage et espace de travail · Conventions JSON · Tutoriel 00 — Liste des périphériques · Tutoriel 07 — Basis et Mount (version WebSocket)