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
mountafin 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/sessionset fixe*inverse/0dans le code, mais remplacer ce choix par un sélecteur piloté par/devicesne 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.
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.
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
- Python
- C++
| Touche | Action |
|---|---|
B | Découvrir les permutations de base |
P | Parcourir les préréglages d'espace de travail |
W / E / R | Sé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 Down | Avancer de +Z / −Z dans le mode actuel |
= / - | Mise à l'échelle uniforme ± sur les trois axes simultanément (toujours disponible) |
Delete | DELETE base + préréglage + montage — rétablir les paramètres par défaut de l'appareil |
H | Afficher l'aide |
Esc | Lâcheur (Ctrl+C (ça fonctionne aussi) |
Par ligne — tapez, puis appuyez sur ENTRÉE.
| Commande | Action |
|---|---|
b | Parcourir les permutations de base |
p | Parcourir les préréglages d'espace de travail |
w / e / r | Sélectionner le mode d'édition du montage — position (mm) / rotation (°) / échelle (%) |
x+[N] … z-[N] | Développer l'axe actuel de N dans l'unité naturelle du mode actif (x+ seul = 5 par défaut) |
sx+[N] … sz-[N] | Raccourci permettant un redimensionnement non uniforme sur un axe (en pourcentage), toujours disponible |
u+[N] / u-[N] | Mise à l'échelle uniforme de ± N % sur les trois axes simultanément |
reset | DELETE base + préréglage + montage |
h | Afficher l'aide |
Appuyez sur Ctrl+C (ou Ctrl+D / EOF) pour quitter.
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.
| Verbe | Rôle | Chemins utilisés |
|---|---|---|
GET | Consulter 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=... |
POST | Remplacer une valeur de configuration — le corps est au format JSON | /<device_selector>/config/{basis,preset,mount}?session=... |
DELETE | Ré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
- C++ (nlohmann)
- C++ (Glaze)
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}"
libhv dénoncer requests::get / requests::post / requests::Delete (avec un D majuscule — delete est un mot-clé C++). La méthode POST nécessite une requête construite manuellement pour définir Content-Type: application/json :
static std::string session_url(const std::string &endpoint) {
return BASE_URL + endpoint + "?session=" + session_selector;
}
static json http_get(const std::string &url) {
auto resp = requests::get(url.c_str());
if (!resp || resp->status_code != 200) return {};
try { return json::parse(resp->body); } catch (...) { return {}; }
}
static bool http_post_json(const std::string &url, const json &body) {
auto req = std::make_shared<HttpRequest>();
req->method = HTTP_POST;
req->url = url;
req->content_type = APPLICATION_JSON;
req->body = body.dump();
auto resp = requests::request(req);
return resp && resp->status_code == 200;
}
static bool http_delete(const std::string &url) {
auto resp = requests::Delete(url.c_str());
return resp && resp->status_code == 200;
}
Les corps de réponse comportent toujours l'enveloppe {"ok", "data": T}. Un seul modèle englobe chaque requête GET typée ; le même schéma HttpRequest sert aux requêtes POST avec glz::write_json :
template <typename T> struct envelope { bool ok{}; T data{}; };
template <typename Payload>
static std::optional<Payload> http_get_envelope(const std::string &url) {
auto resp = requests::get(url.c_str());
if (!resp || resp->status_code != 200) return std::nullopt;
envelope<Payload> env{};
if (glz::read<glz_settings>(env, resp->body)) return std::nullopt;
return std::move(env.data);
}
template <typename Body>
static bool http_post_json(const std::string &url, const Body &body) {
std::string buf;
if (glz::write_json(body, buf)) return false;
auto req = std::make_shared<HttpRequest>();
req->method = HTTP_POST;
req->url = url;
req->content_type = APPLICATION_JSON;
req->body = std::move(buf);
auto resp = requests::request(req);
return resp && resp->status_code == 200;
}
static bool http_delete(const std::string &url) {
auto resp = requests::Delete(url.c_str());
return resp && resp->status_code == 200;
}
Présentation des sessions — GET /sessions
Le comportement dépend de --session :
--session SELECTORfourni → un seulGET /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:0lorsqu'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.
- Python
- C++ (nlohmann)
- C++ (Glaze)
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
static bool discover_session(const std::string &session_arg) {
if (!session_arg.empty()) {
const auto data = http_get(BASE_URL + "/sessions/" + session_arg);
if (data.is_null()) return false;
session_selector = session_arg;
return true;
}
const auto data = http_get(BASE_URL + "/sessions");
const json &list = data["data"]["sessions"];
for (size_t i = 0; i < list.size(); ++i) {
const int sid = list[i].value("session_id", 0);
std::string prof = "default";
if (list[i].contains("config") && list[i]["config"].contains("profile"))
prof = list[i]["config"]["profile"].value("name", std::string{"default"});
printf(" [%zu] session #%d profile=%s\n", i, sid, prof.c_str());
}
std::string line; std::getline(std::cin, line);
const json &picked = list[std::stoi(line)];
std::string prof;
if (picked.contains("config") && picked["config"].contains("profile"))
prof = picked["config"]["profile"].value("name", std::string{});
session_selector = (!prof.empty() && prof != "default")
? ":" + prof + ":0"
: "#" + std::to_string(picked.value("session_id", 0));
return true;
}
Modélise la forme de la réponse à l'aide de structs ; Glaze les génère automatiquement par réflexion :
struct profile_info { std::string name; };
struct session_config{ std::optional<profile_info> profile; };
struct session_info { int session_id{}; std::optional<session_config> config; };
struct sessions_list { int session_count{}; std::vector<session_info> sessions; };
static bool discover_session(const std::string &session_arg) {
if (!session_arg.empty()) {
auto resp = requests::get((BASE_URL + "/sessions/" + session_arg).c_str());
if (!resp || resp->status_code != 200) return false;
session_selector = session_arg;
return true;
}
auto list = http_get_envelope<sessions_list>(BASE_URL + "/sessions");
if (!list || list->sessions.empty()) return false;
for (size_t i = 0; i < list->sessions.size(); ++i) {
const auto &s = list->sessions[i];
std::string prof = "default";
if (s.config && s.config->profile) prof = s.config->profile->name;
printf(" [%zu] session #%d profile=%s\n", i, s.session_id, prof.c_str());
}
std::string line; std::getline(std::cin, line);
const auto &picked = list->sessions[std::atoi(line.c_str())];
std::string prof;
if (picked.config && picked.config->profile) prof = picked.config->profile->name;
session_selector = (!prof.empty() && prof != "default")
? ":" + prof + ":0"
: "#" + std::to_string(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>
*inversecorrespondent à n'importe quel appareil de la gamme Inverse (inverse3,inverse3x,minverse) — Le tutoriel fonctionne tel quel, quel que soit le modèle utilisé.0est 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 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"}}
- Python
- C++ (nlohmann)
- C++ (Glaze)
def post_basis():
perm, _ = BASIS_OPTIONS[basis_index]
api_post(session_url("/inverse3/0/config/basis"), {"permutation": perm})
static void post_basis() {
http_post_json(session_url("/inverse3/0/config/basis"),
{{"permutation", BASIS_OPTIONS[basis_index].first}});
}
struct basis_body { std::string permutation; };
static void post_basis() {
http_post_json(session_url("/inverse3/0/config/basis"),
basis_body{BASIS_OPTIONS[basis_index].first});
}
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"}}
- Python
- C++ (nlohmann)
- C++ (Glaze)
def post_preset():
preset = PRESET_OPTIONS[preset_index]
api_post(session_url("/inverse3/0/config/preset"), {"preset": preset})
static void post_preset() {
http_post_json(session_url("/inverse3/0/config/preset"),
{{"preset", PRESET_OPTIONS[preset_index]}});
}
struct preset_body { std::string preset; };
static void post_preset() {
http_post_json(session_url("/inverse3/0/config/preset"),
preset_body{PRESET_OPTIONS[preset_index]});
}
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.
- Python
- C++ (nlohmann)
- C++ (Glaze)
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)
static void post_mount() {
http_post_json(session_url("/inverse3/0/config/mount"), {
{"transform", {
{"position", {{"x", mount_pos[0]}, {"y", mount_pos[1]}, {"z", mount_pos[2]}}},
{"rotation", quat_from_euler_deg(mount_rot[0], mount_rot[1], mount_rot[2])},
{"scale", {{"x", mount_scale[0]}, {"y", mount_scale[1]}, {"z", mount_scale[2]}}},
}},
});
}
struct vec3 { float x{}, y{}, z{}; };
struct quat { float w{1.0f}, x{}, y{}, z{}; };
struct transform_t { vec3 position{}; quat rotation{}; vec3 scale{1.0f, 1.0f, 1.0f}; };
struct mount_body { transform_t transform; };
static void post_mount() {
http_post_json(session_url("/inverse3/0/config/mount"), mount_body{
transform_t{
.position = vec3{mount_pos[0], mount_pos[1], mount_pos[2]},
.rotation = quat_from_euler_deg(mount_rot[0], mount_rot[1], mount_rot[2]),
.scale = vec3{mount_scale[0], mount_scale[1], mount_scale[2]},
}});
}
mount et preset s'excluent mutuellementUne 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.
- Python
- C++ (nlohmann)
- C++ (Glaze)
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"))
static void reset_all() {
http_delete(session_url("/inverse3/0/config/basis"));
http_delete(session_url("/inverse3/0/config/preset"));
http_delete(session_url("/inverse3/0/config/mount"));
}
static void reset_all() {
http_delete(session_url("/inverse3/0/config/basis"));
http_delete(session_url("/inverse3/0/config/preset"));
http_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.
- Python
- C++ (nlohmann)
- C++ (Glaze)
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,
}
static json quat_from_euler_deg(float pitch_x, float yaw_z, float roll_y) {
constexpr float deg_to_rad = 3.14159265358979323846f / 180.0f;
const float hx = pitch_x * 0.5f * deg_to_rad;
const float hy = roll_y * 0.5f * deg_to_rad;
const float hz = yaw_z * 0.5f * deg_to_rad;
const float cx = std::cos(hx), sx = std::sin(hx);
const float cy = std::cos(hy), sy = std::sin(hy);
const float cz = std::cos(hz), sz = std::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},
};
}
static quat quat_from_euler_deg(float pitch_x, float yaw_z, float roll_y) {
constexpr float deg_to_rad = 3.14159265358979323846f / 180.0f;
const float hx = pitch_x * 0.5f * deg_to_rad;
const float hy = roll_y * 0.5f * deg_to_rad;
const float hz = yaw_z * 0.5f * deg_to_rad;
const float cx = std::cos(hx), sx = std::sin(hx);
const float cy = std::cos(hy), sy = std::sin(hy);
const float cz = std::cos(hz), sz = std::sin(hz);
return quat{
.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 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 Downet=/-déplacent les axes de montage tant qu'elles sont maintenues ;BetPpour 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#ifdefpropres à chaque plateforme pour les API de console.
Source
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)