06. Exemple d'intégration (Inverse3 + VerseGrip sans fil)
Ce tutoriel vous explique comment utiliser ces deux appareils. Lorsque vous maintenez le bouton enfoncé tout en orientant la poignée dans la direction souhaitée, le curseur se déplace dans cette direction Inverse3 . Le curseur est limité à l'intérieur de l'espace de travail sphérique.
Contenu pédagogique :
- Lecture de deux types d'appareils dans la même trame d'état (
inverse3etwireless_verse_grip) - quaternionExtraction de la direction indiquée par la poignée (locale
+Y(Félicitations) set_cursor_positionDéplacer le curseur vers le point cible calculé à l'aide de- Limiter le point cible à l'intérieur d'une zone de travail sécurisée — Minverse utilise un rayon plus petit que celui d'Inverse3
- de manière à ce que le point d'origine soit situé au centre de la zone de couverture Préréglages d'espace de travail (
arm_front_centered) Configuration
Flux de travail
- Recherche de deux appareils :
- C++ La version est récupérée via HTTP au démarrage
GET /devicesAprès avoir consulté [...], le système affiche un message d'instructions de correction et attend que la touche ENTER soit enfoncée. - Pythonlit les deux identifiants d'appareil dans la première trame d'état WebSocket.
- C++ La version est récupérée via HTTP au démarrage
- Dans le premier message, Profil de sessionen l'enregistrant et
configure.preset: arm_front_centeredConfigurez (poignée de main unique). - À chaque mouvement, la prise
orientationetbuttons.{a, b}Lit l'état. - Si le bouton de déplacement est enfoncé, l'orientation de la poignée dans l'espace mondial (
R(q) · ĵ— calculer l'unité pivotée (autour de l'axe +Y), puisSPEEDMettre à l'échelle et accumuler à l'emplacement cible. - Après avoir limité la zone cible à l'intérieur de l'espace de travail,
set_cursor_positionest transmis par le biais de. - (Uniquement pour Python) L'appareil
config.typeOn ajuste le rayon du cercle en fonction de —minverseest de 0,04 m, les autres mesurent 0,10 m.
Paramètres
| Nom | Valeur par défaut | Utilisation |
|---|---|---|
SPEED | 0.01 m/tic | Intervalle de déplacement tant que le bouton est enfoncé |
RADIUS_INVERSE3 | 0.10 m | Inverse3 / Rayon maximal de l'espace de travail d'Inverse3x |
RADIUS_MINVERSE | 0.04 m | Rayon maximal de l'espace de travail de Minverse (valable uniquement pour Python — pour C++, c'est 0.10(fixé à) |
PRINT_EVERY_MS | 200 | Limite de fréquence de transmission des données de télémétrie |
| Nom du profil de session | co.haply.inverse.tutorials:combined | Haply Valeur permettant d'identifier cette simulation dans le Hub |
- Laissez Inverse3 effectuer l'étalonnage automatique (ou placez le stylo au-dessus du réservoir d'encre et attendez que la LED s'allume).
- Retirez la poignée du pot d'encre.
- Faites pivoter la poignée tout en maintenant enfoncé le bouton A ou B — le curseur se déplace dans la direction indiquée par la poignée.
Champ « État de lecture »
Lisez les valeurs suivantes dans la trame d'état transmise à chaque tick :
data.inverse3[0].state.cursor_position—vec3data.wireless_verse_grip[0].state.orientation—quaterniondata.wireless_verse_grip[0].state.buttons.{a, b, c}— Valeur booléenne- (Python, uniquement pour la première image)
data.inverse3[0].config.type— Choisissez l'un des rayons suivants : Inverse3 ou Minverse - (Python, uniquement pour la première image)
data.inverse3[0].status.calibrated— Si la valeur est « false », un message d'avertissement s'affiche à l'utilisateur
Émission / Réception
Calcul permettant de déterminer l'orientation à partir de quaternions (R(q)vers +Y(opération de rotation) et le traitement des contraintes internes de la sphère constituent des opérations d’algèbre linéaire classiques. Pour plus de détails, veuillez consulter le fichier source. Du côté de l’API inverse, les fonctions appelées à chaque poignée de main et à chaque tick set_cursor_positionest composé de.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Il est constitué d'une seule boucle asynchrone. Python lit les deux identifiants d'appareil dans la première trame d'état. Lors de la négociation, la première set_cursor_position le profil de l'appel et configure.preset: arm_front_centeredJe vous le joins ci-joint.
async with websockets.connect(URI) as websocket:
while True:
msg = await websocket.recv()
data = json.loads(msg)
if first_message:
first_message = False
inverse3_id = data["inverse3"][0]["device_id"]
grip_id = data["wireless_verse_grip"][0]["device_id"]
radius = get_workspace_radius(data["inverse3"][0].get("config", {}))
# Handshake: profile + preset + first position command
request_msg = {
"session": {"configure": {"profile": {"name": SLUG}}},
"inverse3": [{
"device_id": inverse3_id,
"configure": {"preset": {"preset": "arm_front_centered"}},
"commands": {"set_cursor_position": {"position": position}},
}],
}
else:
# Per tick: update position from grip pointing direction (classic math, not shown), send
request_msg = {
"inverse3": [{
"device_id": inverse3_id,
"commands": {"set_cursor_position": {"position": position}},
}],
}
await websocket.send(json.dumps(request_msg))
Le C++, au démarrage, GET /devicesAprès avoir récupéré les identifiants des deux appareils via HTTP, le protocole WebSocket est ouvert. onmessageElle s'exécute dans le thread d'E/S de libhv, tandis que le thread principal reste bloqué jusqu'à ce que la touche ENTER soit enfoncée.
// Startup (synchronous):
const std::string inv3_device_id = get_first_device_id("inverse3");
const std::string grip_device_id = get_first_device_id("wireless_verse_grip");
// Per tick:
ws.onmessage = [&](const std::string &msg) {
json data = json::parse(msg);
// ... classic math (not shown): update local Inverse3State + WirelessVerseGripState,
// compute new position from grip orientation, clamp to sphere ...
json command;
command["inverse3"] = json::array();
command["inverse3"].push_back({
{"device_id", inv3_device_id},
{"commands", {{"set_cursor_position",
{{"position", {{"x", pos.x}, {"y", pos.y}, {"z", pos.z}}}}}}}
});
if (first_message) {
first_message = false;
command["session"] = {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:combined"}}}}}};
command["inverse3"][0]["configure"] = {
{"preset", {{"preset", "arm_front_centered"}}}};
}
ws.send(command.dump());
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
Le modèle de rappel de libhv est identique. Il modélise à la fois les trames d'état et les commandes d'émission à l'aide de structures typées (typed struct). devices_message Les deux à l'intérieur std::vector<> Grâce à cela, un seul glz::read Cette commande permet d'obtenir les deux types d'appareils.
// Struct models
struct quat { float w{1.0f}, x{}, y{}, z{}; };
struct button_state { bool a{}, b{}, c{}; };
struct wvg_state { quat orientation{}; uint8_t hall{}; button_state buttons{}; };
struct wvg_device { std::string device_id; wvg_state state; };
struct inverse_state { vec3 cursor_position{}, cursor_velocity{}; };
struct inverse_device { std::string device_id; inverse_state state; };
struct devices_message {
std::vector<inverse_device> inverse3;
std::vector<wvg_device> wireless_verse_grip;
};
struct set_cursor_position_cmd { vec3 position; };
struct commands_message {
std::optional<session_cmd> session;
std::vector<device_commands> inverse3;
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
const auto &wvg = data.wireless_verse_grip[0].state;
cursor_pos = data.inverse3[0].state.cursor_position;
// ... classic math (not shown): if (wvg.buttons.a || wvg.buttons.b)
// move in pointing dir; clamp to sphere ...
commands_message out_cmds{};
device_commands dc{ .device_id = inv3_device_id };
dc.commands.set_cursor_position = set_cursor_position_cmd{cursor_pos};
if (first_message) {
first_message = false;
out_cmds.session = session_cmd{ /* profile = combined */ };
dc.configure = device_configure{ .preset = preset_cfg{"arm_front_centered"} };
}
out_cmds.inverse3.push_back(std::move(dc));
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
Le tutoriel 06 est également installé localement avec le SDK — dans le répertoire d'installation du service, sous tutorials/06-haply-inverse-combined/ Vous pouvez le consulter dans le dossier.
Source : Python · C++ · C++ Glaze
Sujets connexes : Commandes de contrôle (set_cursor_position) · Type (quaternion, vec3) · Montage et espace de travail (préréglages) · Tutoriel 03 (VG sans fil) · Tutoriel n° 5 (Contrôle de position)