08. Configurateur de session à distance
Reconfigurez une session déjà en cours ailleurs – par exemple, une autre application, une scène dans Unity ou une démo d’Haply Hub – en envoyant des appels REST HTTP vers l’appareil correspondant. Ce tutoriel n’ouvre pas de WebSocket : seules des requêtes GET, POST et DELETE sont envoyées 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 fournir des retours haptiques.
Cas d'utilisation
- Modifier la démo en cours en direct. Lancez la démo Orb dans Haply Hub, puis suivez ce tutoriel dans un deuxième terminal pour changer la permutation de base, modifier le préréglage de l'espace de travail ou déplacer légèrement la transformation de montage : le système de coordonnées de la scène Orb se déplace immédiatement, sans qu'il soit nécessaire d'arrêter la démo.
- Calibrage de l'espace de travail par utilisateur. Lancez la simulation tactile sur l'ordinateur principal et permettez à un opérateur se trouvant sur le même réseau de
mount- Transmettre un décalage / une rotation / une mise à l'échelle afin que l'espace de travail virtuel corresponde au bureau de l'utilisateur. - Menu des options avec sélection des appareils. Ces mêmes fonctions d'aide HTTP peuvent
GET /devicesinterroger (voir Tutoriel 00), afin de répertorier les appareils et de créer un menu interactif – sélectionner un appareil puis le reconfigurer –, sans interférer avec le WebSocket de la session. Le tutoriel demande/sessionsà partir de et codé en dur*inverse/0; le passage à une/devicesCette sélection contrôlée constitue toutefois une modification locale. - Reconfiguration pilotée par script. Automatisez les étapes préliminaires (configuration de base, préréglage et montage) avant le début d'une session d'enregistrement, sans avoir à intégrer cette configuration dans chaque client.
Conditions préalables
Le tutoriel n° 8 permet de reconfigurer une session déjà en cours. Vous devez disposer d'une session haptique active, quelle qu'elle soit : un autre tutoriel, une scène dans Unity ou une démo sur Haply Hub.
Accédez à la plateformeHaply , lancez la démo Orb, puis adressez-vous directement à elle :
./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. Lorsque vous passez du tutoriel 08 « Base » au préréglage ou que vous déplacez légèrement la transformation « Mount », le système de coordonnées de la scène « Orb » se déplace de manière visible 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 actuelle, le préréglage ou le montage de la session, puis attend que l'utilisateur appuie sur une touche — chaque pression sur une touche envoie exactement une requête REST.
Les sessions sans nom de profil ne peuvent être identifiées qu’à l’aide d’un identifiant numérique — et celui-ci change à chaque exécution. Laissez votre application principale, lors de son premier message, session.configure.profile.name appeler ; vous pourrez ensuite utiliser un sélecteur stable tel que --session :my_profile:0 réutiliser pour toutes les exécutions. Voir Sessions — Nom du profil.
Configuration du clavier
- Python
- C++
| Goût | Action |
|---|---|
B | Activer la permutation de base |
P | Activer le préréglage d'espace de travail |
W / E / R | Sélectionner le mode d'usinage — Position (mm) / Rotation (°) / Mise à l'échelle (%) |
← / → | Dans le mode actuel, un pas de −X / +X |
↑ / ↓ | Dans le mode actuel, avancer d'un pas +Y / −Y |
Page Up / Page Down | Dans le mode actuel, avancer d'un pas +Z / −Z |
= / - | Mise à l'échelle uniforme ± sur les trois axes simultanément (toujours disponible) |
Delete | DELETE pour Basis + Preset + Mount — Réinitialisation aux paramètres par défaut de l'appareil |
H | Afficher l'aide |
Esc | Quitter (Ctrl+C (ça fonctionne aussi) |
Mode ligne par ligne — Saisissez la commande, puis appuyez sur ENTRÉE.
| Commande | Action |
|---|---|
b | Activer la permutation de base |
p | Activer le préréglage d'espace de travail |
w / e / r | Sélectionner le mode d'usinage — Position (mm) / Rotation (°) / Mise à l'échelle (%) |
x+[N] … z-[N] | L'axe actuel autour de N déplacer dans l'unité naturelle du mode actif (x+ sans valeur = valeur par défaut 5) |
sx+[N] … sz-[N] | Raccourci pour une mise à l'échelle 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 pour la base + le préréglage + le support |
h | Afficher l'aide |
Pour quitter, appuyez sur Ctrl+C (ou Ctrl+D / EOF).
Méthodes HTTP — GET, POST, DELETE
Ce tutoriel utilise trois méthodes HTTP, et uniquement ces trois-là. Chaque appel renvoie la réponse par défaut Enveloppe JSON ({"ok": true, "data": {...}} en cas de succès, {"ok": false, "error": "..."} en cas d'erreur) ainsi que l'un des trois codes d'état suivants : 200 Succès, 400 demande erronée, 404 Selektor n'a rien trouvé.
| Méthode | Objet | Chemins utilisés |
|---|---|---|
GET | Lire l'état actuel — Liste des sessions, requête ciblée sur une session, valeurs de configuration actuelles | /sessions, /sessions/<selector>, /<device_selector>/config/{basis,preset,mount}?session=... |
POST | Remplacer une valeur de configuration — Le corps de la requête est au format JSON | /<device_selector>/config/{basis,preset,mount}?session=... |
DELETE | Réinitialiser une valeur de configuration aux paramètres par défaut de l'appareil | /<device_selector>/config/{basis,preset,mount}?session=... |
Fonctions d'aide HTTP
Un wrapper léger autour des trois méthodes, afin que le reste du tutoriel se lise comme de la pure logique métier :
- Python
- C++ (nlohmann)
- C++ (Glaze)
Python utilise requests.Session() pour HTTP-Keep-Alive (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 présente requests::get / requests::post / requests::Delete prêt (grand D — delete (c'est un mot-clé C++). Pour la méthode POST, une requête créée manuellement est nécessaire afin de Content-Type: application/json à définir :
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 ont toujours le {"ok", "data": T}-Enveloppe. Un seul modèle englobe chaque requête GET typée ; il en va de même HttpRequest-Muster traite les 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;
}
Détermination de la session — GET /sessions
Structuré selon --session:
--session SELECTORindiqué → unGET /sessions/<SELECTOR>.200→ utiliser ;404→ Afficher le message d'erreur.- Pas de drapeau →
GET /sessions(Liste) → Afficher les sessions avec un nom de profil → Rechercher un index → Créer le sélecteur définitif (de préférence:profile:0, si disponible ; sinon#id).
SELECTOR accepte toute forme figurant sous Selektoren — Sélecteur de session est définie comme suit : :profile:instance, #id, :-1, :0, un simple nom de profil ou un Caractère générique pour les noms de profil-Des modèles tels que co.haply.hub::*:0. Le tutoriel transmet la chaîne de caractères telle quelle ; le service 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;
}
Représentez la structure de réponse sous forme de « structs » ; Glaze la reflète automatiquement :
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'appareils — *inverse/0
Chaque appel de configuration est limité à un seul appareil. Le tutoriel utilise un sélecteur composé d'un caractère générique de famille et d'un index:
/*inverse/0/config/<key>
*inverseConvient à tous les appareils de la gamme Inverse (inverse3,inverse3x,minverse) — Le tutoriel reste valable tel quel, quel que soit le modèle concerné.0C'est l'index à base zéro au sein de cette famille — le tutoriel ne mentionne toujours que la première inverse.
Pour passer à un autre appareil, il suffit de modifier une chaîne de caractères — transmettez --device (le chemin d'accès est généré à partir de cela) :
/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 sera avant utilisé pour l'indexation — *inverse:left/0 signifie « la 0e gaucher « Inverse », et non « l'inverse de 0, espérons-le à gauche ». Voir Sélecteurs — Les filtres de classe sont résolus avant l'indexation.
Au démarrage, le tutoriel affiche une GET …/config/basis-Demande. Si la session ne dispose d'aucun
appareil qui --device correspond à (par exemple --device '*inverse:left/0' dans le cas d’une
configuration exclusivement pour droitiers), le service répond par 404 no device matches selector '<sel>'; le tutoriel explique Device '<sel>' not found in session '<sel>'. aus
et se termine par le statut 1. Aucune boucle de clavier avec des valeurs par défaut obsolètes n'est lancée.
Vous trouverez la syntaxe complète à l'adresse suivante : Sélecteurs — Sélecteur d'appareils. Pour créer un menu de sélection d'appareils au lieu d'utiliser des valeurs codées en dur, comptez les appareils avec GET /devices?session=<selector> auf (Tutoriel 00) et appliquent l'option sélectionnée device_id dans les chemins de configuration.
Configuration POST — base, préréglage, montage
Trois clés présentant la même structure de requête, mais un schéma de corps différent. Chaque requête POST renvoie un 200 avec la valeur obtenue dans data, ou un 404, si la recherche par session ou par 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"}}
- 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 envoyée vers l'un des deux champs supprime l'autre sur l'appareil. Le tutoriel n'aborde pas explicitement ce point : 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 point de vue de WebSocket.
Réinitialisation DELETE — trois appels
reset envoie une commande DELETE par clé de configuration. Chacune renvoie 200 avec la valeur désormais par défaut 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"));
}
Assemblage de la rotation du support
transform.rotation est représenté par un quaternion unitaire lors de la transmission. Le tutoriel enregistre la rotation sous la forme d'un triplet d'Euler intrinsèque dans l'ordre 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 reconstruit 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 unitaire selon Hamilton, système de coordonnées à droite, ordre des scalaires (w) — même convention que pour le reste du site, voir quaternion. L'ordre des morceaux est le suivant : Z-Y-X intrinsèque (q = q_z * q_y * q_x) : Appliquez d'abord la rotation autour de l'axe X (pitch), puis celle autour de l'axe Y (roll) et enfin celle autour de l'axe Z (yaw).
Le tutoriel affiche, pour chaque ligne d'état, le quaternion dérivé ainsi que le triplet d'Euler, afin que vous puissiez vérifier la composition avant que l'appareil ne tourne. L'état d'Euler local commence à (0, 0, 0), quel que soit le contenu actuel de la session — le premier mount POST remplace la valeur existante.
Modèle de saisie (version abrégée)
L'intégration du protocole HTTP est au cœur du projet ; le contrôle via le clavier est secondaire. Deux abréviations délibérées :
- Python utilise le paquet
keyboard— multi-plateforme et avec prise en charge native de la répétition des touches en les maintenant enfoncées. Touches fléchées,Page Up/Page Downainsi que=/-déplacent les axes de la monture tant que la touche reste enfoncée ;BetPbasculent de manière cyclique entre « Base » et « Preset » lors du front montant. - C++ utilisé
std::getline(std::cin, ...)et une grammaire de tokens compacte (x+20,sx-5,u+10) — moins pratique pour les ajustements fins en continu, mais sans spécificités liées à la plateforme#ifdef- Les ramifications pour les API de console sont portables.
Code source
Le tutoriel 08 est également installé localement avec le SDK — recherchez-le dans le répertoire d'installation du service, sous tutorials/08-haply-inverse-http-remote-config/.
Voir aussi : 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)