Aller au contenu principal

08. Configurateur de session à distance

Reconfigurez une session déjà en cours d'exécution ailleurs — une autre application, une scène Unity, une démo Haply Hub — en envoyant des appels REST HTTP à son appareil. Ce tutoriel n'ouvre aucune WebSocket : il effectue seulement des GET, des POST et des DELETE pour modifier la base, le préréglage d'espace de travail ou la transformation de montage, pendant que l'autre application continue de produire les retours haptiques.

Cas d'utilisation​

  • Ajustement en direct d'une démo en cours. Lancez la démo Haply Hub Orb, puis exécutez ce tutoriel dans un second terminal pour changer la permutation de base, modifier le préréglage d'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 arrêter 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 échelle de mount afin que l'espace de travail virtuel s'aligne sur le bureau de l'utilisateur.
  • Menu de sélection d'appareil. Les mêmes fonctions utilitaires HTTP peuvent interroger GET /devices (voir le tutoriel 00) pour énumérer les appareils et construire un menu interactif — choisir un appareil, puis le reconfigurer — sans toucher au WebSocket de la session. Le tutoriel interroge /sessions et fixe *inverse/0 dans le code, mais remplacer ce choix par un sélecteur piloté par /devices ne demande qu'une modification locale.
  • Configuration scriptée. Automatisez les étapes préalables (définir la base, le préréglage et le montage) avant qu'une session ne commence l'enregistrement, sans intégrer la configuration dans chaque client.

Prérequis​

Le tutoriel 08 reconfigure une session déjà en cours d'exécution. Il vous faut une session haptique active quelconque — un autre tutoriel, une scène Unity ou une démo Haply Hub.

Moyen le plus rapide de lancer une session

Ouvrez Haply Hub et lancez la démo Orb, puis ciblez-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. Parcourir en boucle la base ou le préréglage, ou ajuster légèrement la transformation de montage avec le tutoriel 08, déplacera 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 le montage actuels de la session, puis attend des frappes de touches — chaque frappe envoie un seul appel REST.

Définissez un nom de profil dans votre simulation

Les sessions sans nom de profil ne peuvent être ciblées que par 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 comme --session :my_profile:0 à chaque exécution. Voir Sessions — nom de profil.

Raccourcis clavier​

ToucheAction
BParcourir 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 à la fois (toujours disponible)
DeleteDELETE base + préréglage + montage — rétablir les valeurs par défaut de l'appareil
HAfficher l'aide
EscQuitter (Ctrl+C fonctionne aussi)

Verbes HTTP — GET, POST, DELETE​

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

VerbeRôleChemins utilisés
GETLire l'état actuel — liste des sessions, recherche d'une session ciblée, 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 une valeur de configuration à la valeur par défaut 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 (ramène 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}"

Découverte 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 arrêter.
  • Aucune option → GET /sessions (liste) → afficher les sessions avec leur nom de profil → demander un index → construire le sélecteur final (préférer :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 caractères génériques sur le nom de 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 porte sur un appareil précis. Le tutoriel utilise un sélecteur joker de famille + index :

/*inverse/0/config/<key>
  • *inverse correspond à tout appareil de la famille Inverse (inverse3, inverse3x, minverse) — le tutoriel fonctionne sans modification, quel que soit le modèle précis.
  • 0 est l'index, à partir de zéro, dans cette famille — le tutoriel ne touche jamais qu'au premier Inverse.

Changer de cible ne demande qu'une seule modification de chaîne : passez --device (le chemin en est construit) :

/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 « le 0e inverse gaucher », et non « le 0e inverse, espérons-le gaucher ». Voir Sélecteurs — les filtres de classe sont résolus avant l'indexation.

Le tutoriel 08 se termine lorsque l'appareil ciblé est introuvable

Le tutoriel émet 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 uniquement droitière), le service répond 404 no device matches selector '<sel>' ; le tutoriel affiche Device '<sel>' not found in session '<sel>'. et se termine avec 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 de coder en dur, énumérez les appareils avec GET /devices?session=<selector> (Tutoriel 00) et intégrez le device_id choisi dans les chemins de configuration.

Configuration POST — basis, preset, mount​

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

Basis​

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

Preset​

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

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

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

Un POST sur l'une des deux clés efface l'autre sur le périphérique. Le tutoriel n'en fait pas le suivi explicitement : chaque POST est autonome, et le serveur résout le conflit. Voir Tutoriel 07 pour la même règle, du côté WebSocket.

Réinitialisation DELETE — trois appels​

reset déclenche un DELETE par clé de configuration. Chacun renvoie un 200 avec la valeur par défaut restaurée 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 forme de triplet d'Euler intrinsèque Z-Y-X (tangage autour de X, lacet autour de Z, roulis autour de Y — tous en 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, à repère droitier, à partie scalaire en premier (w) — même convention que le reste du service, voir quaternion. L'ordre de composition est Z-Y-X intrinsèque (q = q_z * q_y * q_x) : on applique d'abord le tangage autour de X, puis le roulis autour de Y, puis le lacet autour de 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 démarre à (0, 0, 0), quel que soit l'état déjà présent dans la session — le premier mount POST remplace la valeur existante.

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

L'intégration de HTTP est l'essentiel ; l'ergonomie du clavier est secondaire. Deux raccourcis délibérés :

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

Source​

Inclus dans le programme d'installation du SDK

Le tutoriel 08 est aussi 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)