07. Basis & Mount Playground
de l'appareil Conversion des monturesde le contrôler de manière interactive, et configure.basis, configure.preset, configure.mountCela montre comment ils fonctionnent ensemble au niveau du protocole Wire. Le focus interactif reste sur la commande « configure » grâce à un fond horizontal fixe.
Contenu pédagogique :
- permutation de base Paramètres (
"XZY"→ (Cadre de l'application : direction vers le haut sur l'axe Y) - Préréglages(
arm_front) Sélectionner et comprendre le contenu de chaque préréglage - Redéfinition du montage à l'exécution à l'aide de quaternions de rotation (basée sur les entrées clavier)
- Règle d'exclusion mutuelle :
mountWapresetcomme l'argent Configuration de l'appareil Ils ne peuvent pas coexister dans un même bloc - ponctuel
configureFonctionnement : à chaque pression sur une touche, le message « configure » est envoyé exactement une fois. - (C++ Glaze)
std::optionalModélisation de champs mutuellement exclusifs à l'aide de
Flux de travail
- Dès que vous recevez le premier message, Profil de session,
configure.basis: "XZY",configure.preset: arm_fronttransmet. Pour un sol fixeset_cursor_forceLa transmission commence. - À chaque tick, lire la valeur Y du curseur et
force_y = max(0, (floor_pos - y) * stiffness)est calculée puis transmise. - Lorsque l'utilisateur appuie sur la touche de rotation du support,
pending_configure = trueRéglez-le sur . - Au prochain tick,
rotationqui contient cette transformation, qui est un quaternion unitaire (composé de Z puis de X pour les angles de tangage et de lacet actuels)configure.mountConstitue un bloc.presetest omis. Ces deux champs sont mutuellement exclusifs sur le câble. - Touche de réinitialisation (
R) désactive la redéfinition. Lors de la prochaine configuration, il faudra à nouveaupresetutilise.
Paramètres
| Nom | Valeur par défaut | Utilisation |
|---|---|---|
BASIS | "XZY" | Permutation des axes — Système de coordonnées orienté vers le haut sur l'axe Y |
DEVICE_PRESET / DEVICE_CONFIG_PRESET | "arm_front" | Préréglages nommés — Point d'origine : socle de l'appareil |
FLOOR_POS_Y | 0.0 m | Plan de sol fixe (application Y) |
STIFFNESS | 1000 N/m | Constante de ressort du plancher |
MOUNT_STEP_DEG | 10° | Angle de rotation par touche enfoncée |
PRINT_EVERY_MS | 200 | Accélérateur télémétrique |
Contrôle
| clé | Mouvement |
|---|---|
W / S | Faire pivoter le support de ±10° par rapport à l'axe X (pas) de l'appareil |
A / D | Faire pivoter le support de ±10° par rapport à l'axe Z (lacet) de l'appareil |
R | Réinitialisation du montage — Retour aux préréglages |
H | Affichage des touches de contrôle |
Q | Fin |
mountWa presetsont mutuellement exclusivesLe service comprend les deux champs Configuration de l'appareil Le bloc est rejeté. Si l'utilisateur redéfinit le montage, le tutoriel, lors de toutes les étapes de configuration suivantes, presetest omis. REn cliquant sur ce bouton, lors de la prochaine configuration, presetcelui-ci est réutilisé et mountsont exclues.
La version C++ lit les entrées ligne par ligne dans un thread stdin s'exécutant en arrière-plan (appuyez sur ENTRÉE après chaque caractère saisi). Python, quant à lui, utilise la boucle asynchrone principale pour keyboard Comme le package interroge les frappes en temps réel, il n'est pas nécessaire d'appuyer sur la touche ENTER. Les touches et les commandes sont identiques.
Champ « État de lecture »
data.inverse3[i].stateValeur lue à :
cursor_position.y—vec3. Il sert à calculer le débit de passage au sol.current_cursor_force— Ces données sont transmises à des fins de télémétrie.
Émission / Réception
La structure de la charge utile est identique dans toutes les variantes. La différence réside dans le fait que chaque variante est mutuellement exclusive. mount / preset Il s'agit de la manière dont la branche est constituée et de la façon dont le thread d'entrée envoie des signaux au thread WebSocket.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Dans une boucle asynchrone unique, keyboard Le paquet interroge les entrées clavier en temps réel. pending_configureest un indicateur global défini par le gestionnaire de touches, et configure Il est réinitialisé à chaque fois qu'un bloc est transmis.
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))
Il s'agit d'un modèle à deux threads. Un thread d'arrière-plan lit les lignes depuis stdin et pending_configure(std::atomic<bool>) est défini, le thread d'E/S de libhv le vérifie à chaque tick et, lorsque la valeur est définie, configureest transmis.
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);
La règle d'exclusion mutuelle est std::optional<preset_cfg>Wa std::optional<mount_cfg>est exprimé naturellement. Seuls les champs dont les valeurs ont été renseignées sont inclus dans le JSON sérialisé. Le nom du préréglage est enum classen modélisant, et glz::meta On utilise la spécialisation pour établir une correspondance avec les chaînes de caractères requises 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. Dans le répertoire d'installation du service, sous tutorials/07-haply-inverse-basis-and-mount/Vous pouvez le trouver ici.
Source : Python · C++ · C++ Glaze
Sujets connexes : Permutation de base · Montage et espace de travail · Configuration de l'appareil · Commandes de contrôle (set_cursor_force) · Type (transformation) · Tutoriel n° 4 (Bonjour le sol)