07. Base et aire de jeux « Mount »
Contrôle de manière interactive la Transformation de montage de l'appareil et explique comment configure.basis, configure.preset et configure.mount interagir au niveau du protocole. Un fond horizontal fixe est conservé afin que l'interaction reste centrée sur les commandes « configure ».
Ce que vous allez apprendre :
- Définition d'une Permutation de bases (
"XZY"→ Cadre d'application avec l'axe Y vers le haut) - Choix d'un Préréglages (
arm_front) et comprendre ce qu'il configure - Remplacement du montage à l'exécution par un quaternion de rotation (contrôlé via le clavier)
- Le principe d'exclusivité réciproque :
mountetpresetne peuvent pas figurer ensemble dans le même bloc de la Configuration de l'appareil se produire - Unique
configure- Semantik : chaque pression sur une touche envoie exactement un message « configure ». - (C++ Glaze) Modélisation de champs mutuellement exclusifs avec
std::optional
Déroulement
- Pour le premier message : envoyez le Profil de session,
configure.basis: "XZY"etconfigure.preset: arm_front. Commencez par le sol fermeset_cursor_forceà envoyer. - À chaque tick : lire la coordonnée Y du curseur,
force_y = max(0, (floor_pos - y) * stiffness)calculer et envoyer. - Lorsque l'utilisateur appuie sur une touche de rotation de montage, définissez
pending_configure = true. - Au prochain tick, vous créerez un
configure.mount-Bloc comportant une transformation dontrotationest un quaternion unitaire (composition Z puis X à partir des valeurs actuelles de tangage et de lacet). Laissezpresetweg – ces deux éléments s'excluent mutuellement au niveau du protocole. - Le bouton de réinitialisation (
R) annule le remplacement ; la commande « configure » suivante reprend alorspresetRetour.
Paramètres
| Nom | Standard | Objet |
|---|---|---|
BASIS | "XZY" | Permutation des axes — Cadre d'application avec l'axe Y orienté vers le haut |
DEVICE_PRESET / DEVICE_CONFIG_PRESET | "arm_front" | Préréglage nommé — Origine au niveau de la base de l'appareil |
FLOOR_POS_Y | 0.0 m | Niveau du sol fixe (Y d'application) |
STIFFNESS | 1000 N/m | Constante de Feder du sol |
MOUNT_STEP_DEG | 10° | Rotation par simple pression sur une touche |
PRINT_EVERY_MS | 200 | Limitation de la télémétrie |
Commande
| Goût | Action |
|---|---|
W / S | Faire pivoter l'appareil de ±10° autour de l'axe +X (inclinaison) |
A / D | Faire pivoter l'appareil de ±10° autour de l'axe +Z (lacet) |
R | Réinitialiser le montage – Retour au préréglage |
H | Afficher les commandes |
Q | Quitter |
mount et preset s'excluent mutuellementLe service refuse de bloquer la Configuration de l'appareil ab, qui contient les deux. Dès que l'utilisateur remplace le montage, le tutoriel preset dans chaque instruction « configure » suivante. En appuyant sur R sera preset réactivée lors de la prochaine instruction « configure » et mount rejeté.
Les variantes en C++ lisent les entrées ligne par ligne dans un thread « stdin » s'exécutant en arrière-plan (appuyez sur ENTRÉE après chaque lettre). Python utilise le module keyboard pour la détection des touches en temps réel dans la boucle principale asynchrone – il n'est pas nécessaire d'appuyer sur ENTER. Mêmes touches, mêmes commandes.
Champs d'état lus
Aus data.inverse3[i].state:
cursor_position.y—vec3, est utilisé pour calculer la pénétration dans le solcurrent_cursor_force— est transmis dans le cadre de la télémétrie
Envoyer / Recevoir
La structure de la charge utile est identique dans toutes les variantes ; ce qui est intéressant, ce sont les différences dans la manière dont chaque variante traite les branches qui s'excluent mutuellement mount / preset et comment le thread d'entrée envoie un signal au thread WebSocket.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Boucle asynchrone unique avec interrogation des touches en temps réel via le paquet keyboard. pending_configure est un indicateur global qui est activé par les gestionnaires de clavier et réinitialisé à chaque fois qu'un configure- Le bloc est envoyé.
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"]
# Handshake: profile + basis + preset
request_msg = {
"session": {"configure": {"profile": {"name": SLUG}}},
"inverse3": [{
"device_id": device_id,
"configure": build_configure_block(first_handshake=True),
# -> {"basis": {"permutation": "XZY"},
# "preset": {"preset": "arm_front"}}
}],
}
else:
handle_key_inputs() # may set pending_configure = True (classic, not shown)
y = data["inverse3"][0]["state"]["cursor_position"]["y"]
force_y = 0.0 if y > FLOOR_POS_Y else (FLOOR_POS_Y - y) * STIFFNESS
entry = {
"device_id": device_id,
"commands": {"set_cursor_force":
{"vector": {"x": 0.0, "y": force_y, "z": 0.0}}},
}
if pending_configure:
entry["configure"] = build_configure_block(first_handshake=False)
# -> {"mount": {...}} OR {"preset": {...}} (never both)
pending_configure = False
request_msg = {"inverse3": [entry]}
await websocket.send(json.dumps(request_msg))
Modèle à deux threads : un thread d'arrière-plan stdin lit les lignes et définit pending_configure (ein std::atomic<bool>) sur « true » ; le thread d'E/S de libhv le vérifie à chaque tick et renvoie configure dès qu'il est placé.
std::atomic<bool> pending_configure{false};
ws.onmessage = [&](const std::string &msg) {
const json data = json::parse(msg);
if (!data.contains("inverse3") || data["inverse3"].empty()) return;
const bool do_handshake = first_message;
if (first_message) first_message = false;
const bool do_configure = do_handshake || pending_configure.exchange(false);
json request = {};
if (do_handshake) {
request["session"] = {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:basis-and-mount"}}}}}};
}
request["inverse3"] = json::array();
for (auto &el : data["inverse3"].items()) {
json dev_cmd = {{"device_id", el.value()["device_id"]}};
if (do_configure) {
json cfg = {};
if (do_handshake) cfg["basis"] = {{"permutation", BASIS}};
if (mount_overridden) {
cfg["mount"] = {{"transform", {
{"position", {{"x", 0.0}, {"y", 0.0}, {"z", 0.0}}},
{"rotation", quat_from_xz_deg(mount_angle_x_deg, mount_angle_z_deg)},
{"scale", {{"x", 1.0}, {"y", 1.0}, {"z", 1.0}}},
}}};
} else {
cfg["preset"] = {{"preset", DEVICE_CONFIG_PRESET}};
}
dev_cmd["configure"] = cfg;
}
const float y = el.value()["state"]["cursor_position"]["y"].get<float>();
const float force_y = y > FLOOR_POS_Y ? 0.0f : (FLOOR_POS_Y - y) * STIFFNESS;
dev_cmd["commands"] = {{"set_cursor_force",
{{"vector", {{"x", 0.0}, {"y", force_y}, {"z", 0.0}}}}}};
request["inverse3"].push_back(dev_cmd);
}
ws.send(request.dump());
};
std::thread input_thr(input_thread_func); // stdin reader — flips pending_configure
ws.open("ws://localhost:10001");
while (running.load()) std::this_thread::sleep_for(50ms);
Le principe d'exclusivité réciproque s'applique bien sûr à std::optional<preset_cfg> et std::optional<mount_cfg> représenter : dans la sortie sérialisée au format JSON, seule la valeur définie parmi ces deux-là apparaît. Le nom du préréglage est indiqué sous la forme enum class modélisé, avec une glz::meta- une spécialisation qui le convertit en la chaîne de caractères attendue par le service.
// The preset set modelled as an enum
enum class device_preset {
defaults, arm_front, arm_front_centered,
led_front, led_front_centered, custom,
};
// Glaze meta — serialize the enum as the JSON string the service expects
template <> struct glz::meta<device_preset> {
using enum device_preset;
static constexpr auto value =
enumerate(defaults, arm_front, arm_front_centered,
led_front, led_front_centered, custom);
};
// Transform + the configure block
struct vec3 { float x{}, y{}, z{}; };
struct quat { float w{1.0f}, x{}, y{}, z{}; };
struct transform_t { vec3 position{}; quat rotation{}; vec3 scale{1,1,1}; };
struct preset_cfg { device_preset preset; };
struct basis_cfg { std::string permutation; };
struct mount_cfg { transform_t transform; };
struct device_configure {
std::optional<preset_cfg> preset; // mutually exclusive with mount
std::optional<basis_cfg> basis;
std::optional<mount_cfg> mount; // mutually exclusive with preset
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
if (data.inverse3.empty()) return;
const bool do_handshake = first_message.exchange(false);
const bool do_configure = do_handshake || pending_configure.exchange(false);
commands_message out_cmds{};
if (do_handshake) {
out_cmds.session = session_cmd{ /* profile = basis-and-mount */ };
}
for (const auto &dev : data.inverse3) {
device_commands dc{ .device_id = dev.device_id };
if (do_configure) {
device_configure cfg{};
if (do_handshake) cfg.basis = basis_cfg{BASIS};
if (mount_overridden) {
cfg.mount = mount_cfg{ .transform = transform_t{
.rotation = quat_from_xz_deg(mount_angle_x_deg, mount_angle_z_deg)}};
} else {
cfg.preset = preset_cfg{DEVICE_CONFIG_PRESET};
}
dc.configure = std::move(cfg);
}
const float y = dev.state.cursor_position.y;
const float force_y = y > FLOOR_POS_Y ? 0.0f : (FLOOR_POS_Y - y) * STIFFNESS;
dc.commands.set_cursor_force = set_cursor_force_cmd{{0.0f, force_y, 0.0f}};
out_cmds.inverse3.push_back(std::move(dc));
}
std::string out_json;
(void)glz::write_json(out_cmds, out_json);
ws.send(out_json);
};
std::thread input_thr(input_thread_func);
ws.open("ws://localhost:10001");
while (running.load()) std::this_thread::sleep_for(50ms);
Le tutoriel 07 est également installé localement avec le SDK – vous le trouverez à l'emplacement suivant : tutorials/07-haply-inverse-basis-and-mount/ dans le répertoire d'installation du service.
Source : Python · C++ · C++ Glaze
À lire également : Permutation de bases · Support et espace de travail · Configuration de l'appareil · Commandes de contrôle (set_cursor_force) · Types (transformation) · Tutoriel 04 (Bonjour Floor)