07. L'aire de jeux de Basis et Mount
Appareil Transformation de montage en le manipulant de manière interactive, etconfigure.basis,configure.preset,configure.mount Cela montre comment ils interagissent sur le fil. Afin de maintenir l'attention sur la commande « configure » lors des opérations interactives, le plancher horizontal a été figé.
Ce que vous apprendrez dans ce tutoriel :
- Réorganisation de la baseParamètres (
"XZY"→ (cadre de l'application où l'axe Y est en haut) - Préréglages(
arm_front) et la compréhension de ce que cela permet de configurer - Remplacement d'un montage à l'exécution à l'aide de quaternions de rotation (à l'aide du clavier)
- Règle d'exclusivité :
mountetpresetest, le même configuration de l'appareil Ils ne peuvent pas coexister au sein d'un bloc - En une seule prise
configureFonctionnement : à chaque pression sur la touche, un seul message « configure » est envoyé. - (C++ Glaze)
std::optionalModélisation de champs exclusifs à l'aide de
Flux de travail
- Dans le premier message,Profil de session,
configure.basis: "XZY",configure.preset: arm_frontJe l'envoie. Pour un sol fixe,set_cursor_forceCommence l'envoi de. - À chaque tick, on lit la coordonnée Y du curseur, et
force_y = max(0, (floor_pos - y) * stiffness)Nous le calculons et l'envoyons. - Lorsque l'utilisateur appuie sur la touche de rotation de la monture,
pending_configure = trueactive le drapeau. - Au prochain tick,
configure.mountConstruire un bloc. La transformation de ce dernierrotationIl s'agit d'un quaternion unitaire (obtenu en combinant, à partir des angles de tangage et de lacet actuels, d'abord l'axe Z, puis l'axe X).presetJe l'omets. En effet, ces deux éléments sont incompatibles sur le câble. - Touche de réinitialisation (
R) pour annuler la substitution. Lors de la prochaine exécution de `configure`, il faudra à nouveaupresetest utilisé.
Paramètres
| Nom | valeur par défaut | Utilisation |
|---|---|---|
BASIS | "XZY" | Réorganisation des axes — Cadre d'application avec l'axe Y en haut |
DEVICE_PRESET / DEVICE_CONFIG_PRESET | "arm_front" | Préréglages nommés — Le point d'origine correspond à la base de l'appareil |
FLOOR_POS_Y | 0.0 m | Plan du plancher fixe (axe Y de l'application) |
STIFFNESS | 1000 N/m | Constante de ressort du lit |
MOUNT_STEP_DEG | 10° | Angle de rotation par pression sur une touche |
PRINT_EVERY_MS | 200 | Intervalle d'échantillonnage de la télémétrie |
Opération
| Clé | Fonctionnement |
|---|---|
W / S | Faire pivoter le support de ±10° autour de l'axe +X de l'appareil (inclinaison) |
A / D | Faire pivoter le support de ±10° autour de l'axe +Z de l'appareil (lacet) |
R | Réinitialiser le montage — Rétablir les préréglages |
H | Afficher les instructions d'utilisation |
Q | Fin |
mount et preset Il n'est pas possible de les spécifier simultanément.Le service comprend les deux configuration de l'appareil Refuser le bloc. Si l'utilisateur remplace le montage, dans ce tutoriel, à partir de toutes les commandes `configure` suivantes, preset est omis.R Si vous appuyez sur cette touche, lors de la prochaine commande « configure », preset est à nouveau active, etmount sera supprimé.
La variante C++ lit les entrées ligne par ligne dans un thread stdin s'exécutant en arrière-plan (en appuyant sur la touche Entrée après chaque caractère). Python, quant à lui, récupère les saisies au clavier en temps réel dans la boucle asynchrone principale, c'est pourquoikeyboard Utilisez le paquet. Il n'est pas nécessaire d'appuyer sur ENTER. Les touches et les commandes sont les mêmes.
Champ d'état de lecture
data.inverse3[i].state Extrait de :
cursor_position.y—vec3。Il sert à calculer la profondeur de pénétration dans le sol.current_cursor_force— Ces données sont transmises à des fins de télémétrie
Envoi / Réception
Le format de la charge utile est identique pour toutes les variantes. La différence notable réside dans le fait que chaque implémentation est exclusive. mount / preset Il s'agit de savoir comment mettre en place cette branche, et comment le thread d'entrée peut informer le thread WebSocket.
- Python
- C++ (nlohmann)
- C++ (Glaze)
keyboard Il s'agit d'une boucle asynchrone unique qui interroge les entrées clavier en temps réel au sein du package.pending_configure est un indicateur global défini par le gestionnaire de touches, etconfigure Il est effacé à chaque fois qu'un 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))
Il s'agit d'une architecture à deux threads. Le thread « stdin » s'exécutant en arrière-plan lit les lignes, etpending_configure(std::atomic<bool>) est activé. Le thread d'E/S de libhv vérifie cela à chaque tick et, s'il est activé, configure Je vais l'envoyer.
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'exclusivité est la suivante :std::optional<preset_cfg> et std::optional<mount_cfg> peut être exprimé naturellement. Dans le JSON sérialisé, seules les propriétés dont la valeur est définie apparaissent. Le nom du préréglage est enum class est modélisé comme suit, etglz::meta Grâce à cette spécialisation, les services sont associés aux chaînes de caractères attendues.
// 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/ Veuillez vérifier.
Source : Python · C++ · C++ Glaze
Voir aussi : Réorganisation de la base · Support et espace de travail · Paramètres de l'appareil · Commandes de contrôle (set_cursor_force) · Type (transform) · Tutoriel 04 (Hello Floor)