01. Afficher Inverse3
Se connecte à la simulation WebSocket et transmet la position du curseur, la vitesse et la force à partir du premier « Inverse3 » signalé par le service.
Ce que vous allez apprendre :
- Ouvrir une connexion WebSocket et recevoir le message initial contenant l'état complet
- Envoyer un signal de maintien en activité (keepalive) de force nulle avec
set_cursor_forcepour que la session reste active - Enregistrer un profil de session afin que Haply Hub reconnaisse votre simulation
- Modèle de « poignée de main » limité au premier message : supprimer « session/configure » après le premier envoi
- Lire le
transformde l'espace de travail (position, rotation, échelle) avec la sémantique de mise à jour partielle - Limiter la vitesse d'affichage dans la console à une cadence lisible
Déroulement
- Établir une connexion WebSocket vers
ws://localhost:10001. Le service envoie immédiatement un trame d'état complète qui répertorie les appareils connectés. - À la première trame, choisir le
device_iddu premier Inverse3 et créer un message de requête en deux parties :session.configure.profile.name: enregistre la simulation auprès de Hub Haply.- Une commande
set_cursor_forcepar appareil, avec un vecteur nul. Le service s'en sert comme signal de maintien : il continue d'émettre des trames d'état tant que des commandes lui parviennent.
- Renvoyer le message. Supprimer le champ
sessionavant le cycle suivant : le profil de session correspond à une poignée de main ponctuelle, et les cycles suivants n'envoient que la commande. - Pour chaque trame d'état suivante : afficher les champs
vec3du curseur (position, vitesse, force), à une fréquence limitée à environ 10 Hz, puis renvoyer le signal de maintien à force nulle.
Paramètres
| Nom | Défaut | Rôle |
|---|---|---|
URI | ws://localhost:10001 | URL du canal de simulation WebSocket |
PRINT_EVERY_MS | 100 | Limite d'affichage dans la console |
| Nom du profil de session | co.haply.inverse.tutorials:print-inverse3 | Identifiez cette simulation dans le Hub Haply |
Champs d'État lus
Depuis data.inverse3[0].state :
cursor_position,cursor_velocity,current_cursor_force: de typevec3chacuntransform: transformation de l'espace de travail ; sous-objet avecposition(vec3),rotation(quaternion) etscale(vec3)
Les sous-champs dont la valeur est égale à leur valeur d'identité (position: {0,0,0}, rotation: {w:1,x:0,y:0,z:0}, scale: {1,1,1}) sont omis de la charge utile afin d'économiser la bande passante. Indiquez toujours une valeur par défaut pour la lecture (par ex. .value("position", default_pos) en C++, .get("position", default_pos) (en Python). Activez serialization/explicit_fields pour continuer à recevoir chaque champ.
Envoi et réception
La boucle WebSocket : recevoir une trame d'état, la traiter puis renvoyer une trame de commande. Le premier flux de commande transporte la poignée de main de session et un signal de maintien à force nulle avec set_cursor_force; chaque trame suivante ne transporte que le signal de maintien (la session est interrompue).
- Python
- C++ (nlohmann)
- C++ (Glaze)
Boucle asynchrone unique : recv() → créer la commande → send() → répéter.
async with websockets.connect(URI) as websocket:
while True:
msg = await websocket.recv()
data = json.loads(msg)
if first_message:
first_message = False
device_id = data["inverse3"][0]["device_id"]
request_msg = {
"session": {"configure": {"profile": {
"name": "co.haply.inverse.tutorials:print-inverse3"}}},
"inverse3": [{
"device_id": device_id,
"commands": {"set_cursor_force":
{"vector": {"x": 0.0, "y": 0.0, "z": 0.0}}},
}]
}
await websocket.send(json.dumps(request_msg))
request_msg.pop("session", None) # one-shot handshake
libhv gère le WebSocket sur son propre fil d'E/S ; le traitement par trame se trouve dans ws.onmessage. Le thread principal attend simplement que l'on appuie sur la touche Entrée.
ws.onmessage = [&](const std::string &msg) {
const json data = json::parse(msg);
if (first_message) {
first_message = false;
device_id = data["inverse3"][0].at("device_id").get<std::string>();
request_msg = {
{"session", {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:print-inverse3"}}}}}}},
{"inverse3", json::array({
{{"device_id", device_id},
{"commands", {{"set_cursor_force",
{{"vector", {{"x", 0.0}, {"y", 0.0}, {"z", 0.0}}}}}}}},
})},
};
}
ws.send(request_msg.dump());
request_msg.erase("session"); // one-shot handshake
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
Même modèle de rappel (callback) libhv que la variante nlohmann : seul le corps change. Glaze utilise la réflexion à la compilation : déclarez des structures qui reflètent la structure du JSON, puis appelez glz::read / glz::write_json. std::optional<session_cmd> transmet la poignée de main à usage unique ; lorsqu'elle n'est pas définie, Glaze omet ce champ lors de la sérialisation JSON.
// Struct models
struct vec3 { float x{}, y{}, z{}; };
struct inverse_state {
vec3 cursor_position{}, cursor_velocity{}, current_cursor_force{};
/* + body_orientation, angular_position, angular_velocity */
};
struct inverse_device { std::string device_id; inverse_state state; };
struct devices_message { std::vector<inverse_device> inverse3; };
struct set_cursor_force_cmd { vec3 vector; };
struct commands_message {
std::optional<session_cmd> session; // omitted from JSON when unset
std::vector<device_commands> inverse3;
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
commands_message out_cmds{};
if (first_message) {
first_message = false;
out_cmds.session = session_cmd{ /* profile = print-inverse3 */ };
}
// ... populate out_cmds.inverse3 with zero-force keepalive ...
std::string out_json;
(void)glz::write_json(out_cmds, out_json);
ws.send(out_json);
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
Options de ligne de commande (Python)
La variante Python accepte deux options qui modifient ce qui s'affiche dans le tutoriel :
| Option | Effet |
|---|---|
--full | Affiche la charge utile brute au format JSON de chaque trame d'état, mise en forme, au lieu d'un résumé sur une seule ligne. Utile pour découvrir les champs transmis par le service. |
--query-config | Réinjecter session.force_render_full_state: {} à chaque cycle sortant afin que le service génère à nouveau un instantané complet (y compris le bloc config : type d'appareil, micrologiciel, préréglage, montage, filtres, …) à chaque image. Sans cette option, config n'apparaît que sur la première trame, et les trames suivantes ne contiennent que des mises à jour différentielles diffusées en continu. |
Les deux options se combinent : python 01-haply-inverse-print-inverse3.py --full --query-config affiche une charge utile JSON complète avec le bloc config visible à chaque cycle, ce qui est pratique pour observer en temps réel les modifications des paramètres depuis l'Haply Hub ou via l'API HTTP. Voir session.force_render_full_state pour la commande WebSocket sous-jacente.
Les variantes C++ ne proposent pas ces options : elles affichent toujours le résumé d'une ligne et ne reçoivent config que sur la première trame.
Le tutoriel 01 est également installé localement avec le SDK : consultez tutorials/01-haply-inverse-print-inverse3/ dans le répertoire d'installation du service.
Source : Python · C++ · C++ Glaze
Voir aussi : Protocole WebSocket · Commandes de contrôle (set_cursor_force) · Séances · Types (vec3)