01. Sortie « Inverse3 »
En se connectant au WebSocket de simulation, on diffuse en continu la position du curseur, la vitesse et la force du premier Inverse3 signalées par le service.
Contenu pédagogique :
- Ouvrir une connexion WebSocket et recevoir le message d'état initial complet
- Aucun effort pour maintenir la session active
set_cursor_forceEnvoyer un « Keep Alive » - Haply Enregistrer un profil de session pour permettre au Hub de reconnaître la simulation
- Modèle de « handshake » exécuté uniquement lors du premier message — Suppression de « session/configure » après la première transmission
- Espace de travail avec mise à jour partielle sémantique
transformLecture (position, rotation, échelle) - Réguler le débit de sortie de la console à une vitesse de lecture confortable
Flux de travail
ws://localhost:10001Ouvre une connexion WebSocket. Le service affiche la liste des appareils connectés. Trame d'état complèteest envoyé immédiatement.- Dans la première image, le premier Inverse3 de
device_idSélectionnez cette option, puis créez un message de requête composé de deux parties :session.configure.profile.name— Hub HaplyEnregistrez la simulation.- Par appareil, à l'aide du vecteur zéro
set_cursor_forceCommande. Le service l'utilise comme mécanisme de maintien de connexion et continue d'envoyer des trames d'état tant que des commandes continuent d'arriver.
- Le message est renvoyé. Avant le prochain tick,
sessionSupprimer le champ — Le profil de session correspondant à une poignée de main unique ; par conséquent, lors des ticks suivants, seules les commandes sont transmises. - Par la suite, à chaque image d'état, le curseur
vec3Les données de champ (position, vitesse, force) sont transmises à une fréquence d'environ 10 Hz, et un signal de maintien à force nulle est à nouveau envoyé.
Paramètres
| Nom | Valeur par défaut | Utilisation |
|---|---|---|
URI | ws://localhost:10001 | URL WebSocket du canal de simulation |
PRINT_EVERY_MS | 100 | Limitation de la sortie de la console |
| Nom du profil de session | co.haply.inverse.tutorials:print-inverse3 | Haply Identifier cette simulation dans Hub |
Champ « État de lecture »
data.inverse3[0].stateOn peut y lire ce qui suit :
cursor_position,cursor_velocity,current_cursor_force— respectivementvec3transform— Sous-objet de conversion d'espace de travail.position(vec3),rotation(quaternion),scale(vec3) contient ce champ.
les sous-champs dont la valeur par défaut est identique à celle de l'élément de base (position: {0,0,0}, rotation: {w:1,x:0,y:0,z:0}, scale: {1,1,1}) supprime, dans la charge utile, afin d'économiser de la bande passante, (omission). Veillez à toujours spécifier une valeur par défaut lors de la lecture d'une valeur (par exemple, en C++ : .value("position", default_pos), en Python .get("position", default_pos)). Pour toujours recevoir tous les champs, serialization/explicit_fieldsVeuillez l'activer.
Émission / Réception
Boucle WebSocket : réception d'une trame d'état, Cadre de commandeet la retransmet. La première trame de commande contient la négociation de session et une puissance nulle set_cursor_force Il inclut la fonction « Keep-Alive », et toutes les trames suivantes contiennent Uniquement KeepAlive est inclus (la session est supprimée).
- Python
- C++ (nlohmann)
- C++ (Glaze)
Boucle asynchrone unique — recv() → Créer une 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 les communications WebSocket dans son propre thread d'E/S — le traitement par trame est ws.onmessageIl se trouve là. Le thread principal est bloqué et attend uniquement la pression de la touche ENTER.
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
Il utilise le même modèle de callback libhv que la variante nlohmann ; seule la partie du corps diffère. Glaze utilise la réflexion à la compilation. Autrement dit, il déclare une structure qui reflète fidèlement le format JSON et glz::read / glz::write_jsonappelle. std::optional<session_cmd> Ce type contient une poignée de main ponctuelle ; si aucune valeur n'est définie, Glaze omet ce champ dans le JSON sérialisé.
// 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 prend en charge deux indicateurs permettant de modifier la sortie du tutoriel :
| drapeau | Effet |
|---|---|
--full | Au lieu d'un résumé en une ligne, cette commande affiche de manière claire le contenu JSON brut de toutes les trames d'état. Elle est utile pour vérifier quels champs sont exportés par le service. |
--query-config | À chaque tick sortant session.force_render_full_state: {}en le réinjectant, afin que le service soit présent à chaque trame config Permet de réexporter un instantané complet, y compris les blocs (type de périphérique, micrologiciel, préréglages, montage, filtres, etc.). Si cet indicateur n'est pas spécifié, confign'est transmis que dans la première image ; les images suivantes sont des images delta de streaming. |
Ces deux indicateurs peuvent être utilisés conjointement — python 01-haply-inverse-print-inverse3.py --full --query-configLorsque vous l'exécutez, à chaque tick, configLa charge utile JSON complète indiquée s'affiche. Cela s'avère utile pour observer les modifications en temps réel des paramètres via l'Haply Hub ou l'API HTTP. La commande WebSocket sous-jacente est la suivante : session.force_render_full_stateVeuillez consulter [...].
La variante C++ ne propose pas cette option : elle n'affiche toujours qu'un résumé d'une ligne, et confign'est reçu que dans la première image.
Le tutoriel 01 est également installé localement avec le SDK — dans le répertoire d'installation du service, sous tutorials/01-haply-inverse-print-inverse3/Vous pouvez le consulter ici.
Source : Python · C++ · C++ Glaze
À ce sujet : Protocole WebSocket · Commandes de contrôle (set_cursor_force) · session · Type (vec3)