07. Base et support – Terrain d'entraînement
Commander un appareil de manière interactive transformation de montage, et faire une démonstration configure.basis,configure.preset et configure.mount Comment travailler en collaboration au niveau des messages de communication ? Maintenir un niveau de base constant afin que l'interaction reste toujours centrée sur la commande « configure ».
Vous apprendrez :
- Paramètres translation de l'axe de référence(
"XZY"→ (système de coordonnées de l'application sur l'axe Y) - Sélectionner Préréglages(
arm_front), et comprendre quels éléments il configure - Remplacer « mount » à l'exécution en faisant pivoter un quaternion (à l'aide du clavier)
- Règles d'exclusion mutuelle :
mountetpresetNe peuvent pas apparaître simultanément dans le même Configuration du matériel dans le bloc - à usage unique
configureSignification : une seule commande « configure » est envoyée à chaque pression sur une touche. - (C++ Glaze) Utilisation
std::optionalModélisation des champs mutuellement exclusifs
Processus de travail
- À la réception du premier message : envoyer Fichier de configuration de session,
configure.basis: "XZY"ainsi queconfigure.preset: arm_front。Pour stabiliser le sol, commencer à envoyer en continuset_cursor_force。 - À chaque tick : lire la coordonnée Y du curseur, calculer
force_y = max(0, (floor_pos - y) * stiffness)et l'envoyer. - Lorsque l'utilisateur appuie sur la touche de rotation « mount »,
pending_configure = truePosition. - Au prochain tick : créer un
configure.mountbloc, dont la transformationrotationPour les quaternions unitaires (composés selon l'ordre Z puis X en fonction des angles actuels de tangage et de lacet). Omispreset——Ces deux éléments s'excluent mutuellement dans le protocole de communication. - Touche de réinitialisation (
R) Effacer les paramètres de remplacement ; lors de la prochaine exécution de `configure`, le système reviendra àpreset。
Paramètres
| Nom | Valeur par défaut | Utilisation |
|---|---|---|
BASIS | "XZY" | Décalage des axes — Système de coordonnées de l'application sur l'axe Y |
DEVICE_PRESET / DEVICE_CONFIG_PRESET | "arm_front" | Préréglage nommé — Point d'origine situé à la base de l'appareil |
FLOOR_POS_Y | 0.0 m | Plan de référence fixe (axe Y du système de coordonnées utilisé) |
STIFFNESS | 1000 N/m | Constante de ressort au sol |
MOUNT_STEP_DEG | 10° | Angle de rotation à chaque pression sur une touche |
PRINT_EVERY_MS | 200 | Intervalle de régulation par télémesure |
Contrôle
| touche | Opération |
|---|---|
W / S | Rotation autour de l'axe +X (inclinaison) de l'équipement : ±10° |
A / D | Rotation autour de l'axe +Z (lacet) de l'équipement : ±10° |
R | Réinitialiser le montage — Rétablir les paramètres par défaut |
H | Touches de commande d'affichage |
Q | Quitter |
mount et preset Exclusion mutuelleLe service refusera les demandes contenant à la fois les deux éléments. Configuration du matériel Bloc. Dès que l'utilisateur aura remplacé le fichier « mount », ce tutoriel l'omettra lors de chaque commande « configure » suivante. preset。Appuyez sur R Ensuite, la prochaine commande « configure » réactivera cette option. preset et supprimer mount。
La variante C++ lit les entrées ligne par ligne sur le thread stdin en arrière-plan (il faut appuyer sur ENTRÉE après chaque lettre). Python, quant à lui, utilise dans sa boucle asynchrone principale keyboard Le système effectue une interrogation en temps réel des touches, sans qu'il soit nécessaire d'appuyer sur la touche ENTRÉE. Les touches sont les mêmes, tout comme les commandes.
Champs d'état lus
Source : data.inverse3[i].state:
cursor_position.y—vec3, servant à calculer la profondeur de pénétration dans le solcurrent_cursor_force— Destiné à la transmission de données de télémétrie
Envoyer / Recevoir
La structure de chargement est identique pour toutes les variantes ; la véritable différence réside dans la manière dont chacune d'entre elles met en place les mécanismes d'exclusion mutuelle. mount / preset les branches, ainsi que la manière dont les threads d'entrée informent les threads WebSocket.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Une boucle asynchrone unique, via keyboard Effectuer une interrogation en temps réel des touches du boîtier.pending_configure Il s'agit d'un indicateur global, activé par la fonction de gestion des touches, à chaque envoi configure Effacer après le bloc.
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 stdin en arrière-plan lit les données d'entrée ligne par ligne et les stocke dans des registres pending_configure(Un std::atomic<bool>) ; le thread d'E/S de libhv vérifie cet indicateur à chaque tick et, dès qu'il est activé, il déclenche configure。
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);
Les règles d'exclusion mutuelle peuvent être transposées naturellement à std::optional<preset_cfg> et std::optional<mount_cfg> Ci-dessus : dans le JSON sérialisé, seul celui auquel une valeur a été attribuée apparaît. Le nom par défaut est modélisé comme un enum class, et via glz::meta Spécialisé pour le convertir en une chaîne de caractères conforme aux attentes du serveur.
// 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 pouvez le retrouver dans le répertoire d'installation du service. tutorials/07-haply-inverse-basis-and-mount/。
Code source : Python · C++ · C++ Glaze
Contenu associé : Décalage de l'axe de base · Mount et l'espace de travail · Configuration du matériel · Commandes de contrôle (set_cursor_force) · Type (transformation) · Tutoriel 04 (Hello Floor)