06. Ensemble (Inverse3 + Wireless VerseGrip)
Tutoriel avec deux appareils : alignez la poignée et maintenez une touche enfoncée pour déplacer le curseur depuis Inverse3 dans cette direction. Le curseur est confiné dans un espace de travail sphérique.
Ce que vous apprendrez :
- Lecture de deux types d'appareils à partir d'une même trame d'état (
inverse3etwireless_verse_grip) - Détermination du sens d'orientation de la poignée à partir de sa Quaternion (local
+Y-axe) - Utilisation de
set_cursor_position, pour déplacer le curseur vers une cible calculée - Limitation de la cible à une sphère de travail sécurisée – Minverse utilise un rayon plus petit que Inverse3
- Définition d'un Préréglages de l'espace de travail (
arm_front_centered), afin que le point d'origine se trouve au milieu de la portée
Déroulement
- Les deux appareils détectent :
- C++- Les variantes demandent les informations au démarrage via HTTP
GET /devicesab, affichent ensuite une invite de calibrage et attendent que l'on appuie sur ENTER. - Python lit les deux identifiants de périphérique à partir de la première trame d'état de WebSocket.
- C++- Les variantes demandent les informations au démarrage via HTTP
- Enregistrez le Fiche de séance et indiquez-le dans votre premier message
configure.preset: arm_front_centered(poignée de main unique). - À chaque cycle : lisez les
orientationdes griffes et l'état debuttons.{a, b}. - Si une touche de déplacement est maintenue enfoncée, calculez la direction de la poignée dans le système de coordonnées mondial (
R(q) · ĵ– l'axe unitaire pivoté (+Y) et additionnez-le, mis à l'échelle parSPEED, jusqu'à la position cible. - Limitez le point cible à l'intérieur de la sphère de l'espace de travail et envoyez-le via
set_cursor_position. - (Python uniquement) Ajustez le rayon de la sphère en fonction de
config.typede l'appareil –minverse= 0,04 m, toutes les autres = 0,10 m.
Paramètres
| Nom | Valeur par défaut | Objet |
|---|---|---|
SPEED | 0.01 m/tick | Incrément, tant qu'une touche est maintenue enfoncée |
RADIUS_INVERSE3 | 0.10 m | Rayon de délimitation de l'espace de travail pour Inverse3 / Inverse3x |
RADIUS_MINVERSE | 0.04 m | Rayon de délimitation de l'espace de travail pour Minverse (Python uniquement – le C++ est fixé à 0.10 (codé) |
PRINT_EVERY_MS | 200 | Limitation de la télémétrie |
| Nom du profil de session | co.haply.inverse.tutorials:combined | Marquez cette simulation dans la plateforme « Haply Hub» |
- Laissez l'Inverse3 e s'étalonner automatiquement (ou posez le stylet sur l'Inkwell et attendez que la LED reste allumée en permanence).
- Retirez la poignée de l'encrier.
- Maintenez la touche A ou B enfoncée et tournez la manette : le curseur se déplace dans la direction indiquée par la manette.
Champs d'état lus
À partir du cadre d'état de chaque cycle :
data.inverse3[0].state.cursor_position—vec3data.wireless_verse_grip[0].state.orientation—quaterniondata.wireless_verse_grip[0].state.buttons.{a, b, c}— Les booléens- (Python, uniquement dans le premier cadre)
data.inverse3[0].config.type— sélectionne le rayon pour « Inverse3 » ou Minverse - (Python, uniquement dans le premier cadre)
data.inverse3[0].status.calibrated— invite l'utilisateur à agir lorsque la valeur est « false »
Envoyer / Recevoir
Le calcul de la direction à partir du quaternion (rotation de +Y mit R(q)) et la restriction à la sphère relèvent de l'algèbre linéaire classique – voir les fichiers sources. La partie API inverse comprend la procédure d'établissement de la connexion et les données envoyées à chaque cycle set_cursor_position.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Une seule boucle asynchrone. Python lit les deux identifiants de périphérique à partir de la première trame d'état ; la négociation dépend du profil et configure.preset: arm_front_centered au premier set_cursor_position-Appel à.
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))
Au démarrage, C++ détermine les deux identifiants de périphérique via GET /devices (HTTP), puis ouvre un WebSocket. onmessage s'exécute dans le thread d'E/S de libhv ; le thread principal reste bloqué jusqu'à ce que l'on appuie sur la touche ENTER.
// 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 même modèle de callback que celui de libhv. Des structures typées représentent à la fois les trames d'état et la commande sortante – deux std::vector<> dans devices_message permettent, à l'aide d'un seul glz::read lire 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 – recherchez-le dans tutorials/06-haply-inverse-combined/ dans le répertoire d'installation du service.
Source : Python · C++ · C++ Glaze
Voir aussi : Commandes de contrôle (set_cursor_position) · Types (quaternion, vec3) · Montage et espace de travail (préréglages) · Tutoriel 03 (VG sans fil) · Tutoriel n° 5 (Régulation de position)