01. Sortie d'Inverse3
Se connecter au WebSocket de simulationet diffuser en continu la position, la vitesse et la force du curseur à partir du premier Inverse3 renvoyé par le service.
Contenu pédagogique :
- Ouvrir une connexion WebSocket et recevoir le message d'état initial complet
- Force nulle
set_cursor_forceEnvoyer un « keep-alive » pour maintenir la session active - Haply Enregistrerle profil de sessionafin que le hub puisse reconnaître la simulation
- Modèle de « handshake » effectué uniquement lors du premier message — supprimer « session/configure » après l'envoi du premier message
- Dans le cadre de la sémantique des mises à jour partielles, l'espace de travail
transformLire (la position, la rotation, la mise à l'échelle) - Limiter la fréquence des messages affichés sur la console afin d'en faciliter la lecture
Flux de travail
ws://localhost:10001Ouvre une connexion WebSocket. Le service affiche immédiatement la liste des appareils connectés.Cadre en parfait étatJe vais l'envoyer.- Dans la première image, le premier Inverse3
device_idet crée un message de requête composé de deux parties :session.configure.profile.name— La simulationHub HaplyJe m'inscris.- Par périphérique, en spécifiant un vecteur nul
set_cursor_forceCommande. Le service l'utilise comme signal de maintien de connexion. Tant que la commande continue d'arriver, il continue d'envoyer des trames d'état.
- Je vais vous renvoyer un message.Avant le prochain tick
sessionVeuillez supprimer ce champ。Le profil de session correspond à une poignée de main unique ; lors des ticks suivants, seules les commandes sont envoyées. - À partir de là, à chaque image d'état, le curseur
vec3Les données de champ (position, vitesse, force) sont transmises à une fréquence limitée à environ 10 Hz, et le signal de maintien de la force nulle est retransmis.
Paramètres
| Nom | Par défaut | Utilisation |
|---|---|---|
URI | ws://localhost:10001 | URL du canal de simulation WebSocket |
PRINT_EVERY_MS | 100 | Intervalle entre les sorties sur la console |
| Nom du profil de session | co.haply.inverse.tutorials:print-inverse3 | Haply Identifiez cette simulation au sein du Hub |
Champ d'état de lecture
data.inverse3[0].state On lit ce qui suit :
cursor_position,cursor_velocity,current_cursor_force— Dans tous les casvec3transform— Conversion de l'espace de travail. En tant que sous-objetposition(vec3),rotation(quaternion),scale(vec3)
Valeur par défaut de l'identité (élément neutre) (position: {0,0,0},rotation: {w:1,x:0,y:0,z:0},scale: {1,1,1}) sont supprimés de la charge utile afin d'économiser de la bande passante(omission). Veillez à toujours spécifier la valeur par défaut lors de la lecture (par exemple, en C++ : .value("position", default_pos), en Python, .get("position", default_pos)). Pour recevoir systématiquement toutes les données,serialization/explicit_fieldsVeuillez l'activer.
Émission et réception
Dans la boucle WebSocket, on reçoit une trame d'état, etCadre de commandeNous allons le créer et le renvoyer. La première trame de commande contient la négociation de connexion et la force nulle.set_cursor_forceComprend « Keep-Alive ». Les trames suivantes contiennentUniquement « Keep Alive »est inclus (la session est supprimée).
- Python
- C++ (nlohmann)
- C++ (Glaze)
Il s'agit d'une boucle asynchrone unique — recv() → Créer une commande → send() → Répétition.
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 WebSockets via son propre thread d'E/S. Le traitement par trame s'effectue ws.onmessage Elle est implémentée en interne. Le thread principal se contente de rester bloqué jusqu'à ce que la touche ENTER soit enfoncé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
Il s'agit du même modèle de callback libhv que la variante nlohmann. Seule la partie principale diffère. Glaze utilise la réflexion à la compilation. Il déclare une structure reflétant la forme du JSON, etglz::read / glz::write_json l'appelle.std::optional<session_cmd> Conserve une poignée de main unique. Si ce paramètre n'est pas défini, 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)
Dans la variante Python, vous pouvez spécifier deux indicateurs permettant de modifier la sortie du tutoriel.
| drapeau | Effet |
|---|---|
--full | Au lieu d'afficher un résumé sur une seule ligne, cet outil formate et affiche la charge utile JSON brute de chaque trame d'état. Cela s'avère pratique pour vérifier les champs envoyés par le service. |
--query-config | Pour chaque tick envoyé, session.force_render_full_state: {} et a ajouté une fonctionnalité permettant au service de générer un instantané complet à chaque image (config Bloc — (type de périphérique, micrologiciel, préréglages, montage, filtres, etc.) sera renvoyé. En l'absence de cet indicateur,config Elle n'est transmise que dans la première trame ; les trames suivantes ne contiennent que les modifications issues du streaming. |
Ces deux indicateurs peuvent être utilisés conjointement.python 01-haply-inverse-print-inverse3.py --full --query-config Lorsqu'on l'exécute, à chaque tick config La charge utile JSON complète s'affiche. Cela s'avère pratique pour vérifier en temps réel les modifications apportées aux paramètres depuisHaply Hub ou l'API HTTP. Pour en savoir plus sur les commandes WebSocket sous-jacentes,session.force_render_full_state Veuillez consulter [...].
Dans la variante C++, ces indicateurs ne sont pas disponibles. Le programme affiche toujours un résumé sur une seule ligne, etconfig Il ne reçoit que la première trame.
Le tutoriel 01 est également installé localement avec le SDK. Dans le répertoire d'installation du service, sous le dossier tutorials/01-haply-inverse-print-inverse3/ Veuillez vérifier.
Source : Python · C++ · C++ Glaze
À lire également : Protocole WebSocket · Commandes de contrôle (set_cursor_force) · Session · type (vec3)