07. Base et montage : terrain d'essai
Pilotez de manière interactive la transformation de montage de l'appareil et explique comment configure.basis, configure.preset et configure.mount fonctionnent ensemble au niveau du protocole. Conserve un plan horizontal fixe afin que le point d'intérêt interactif reste sur les commandes de configuration.
Ce que vous apprendrez :
- Définir une permutation de base (
"XZY"→ repère d'application orienté vers le haut selon l'axe Y) - Choisir un préréglage (
arm_front) et comprendre ce qu'il configure - Remplacer le montage à l'exécution par un quaternion de rotation (piloté par le clavier)
- La règle d'exclusion mutuelle :
mountetpresetne peuvent pas coexister dans le même bloc de configuration de l'appareil - Sémantique ponctuelle de
configure: chaque pression sur une touche envoie exactement un message de configuration - (C++ Glaze) modélisation de champs mutuellement exclusifs avec
std::optional
Flux de travail
- Dès le premier message : envoyez-le profil de session,
configure.basis: "XZY"etconfigure.preset: arm_front. Commencez à envoyerset_cursor_forcepour le sol fixe. - À chaque cycle : lisez la valeur Y du curseur, calculez
force_y = max(0, (floor_pos - y) * stiffness)et envoyez-la. - Lorsque l'utilisateur appuie sur une touche de rotation du montage, définissez
pending_configure = true. - À l'étape suivante : créez un bloc
configure.mountavec une transformation dontrotationest un quaternion unitaire (composition Z puis X des angles de tangage et de lacet actuels). Omettezpreset— les deux s'excluent mutuellement au niveau du protocole. - La touche de réinitialisation (
R) annule le remplacement ; la configuration suivante revient àpreset.
Paramètres
| Nom | Défaut | Rôle |
|---|---|---|
BASIS | "XZY" | Permutation des axes — repère d'application avec l'axe Y orienté vers le haut |
DEVICE_PRESET / DEVICE_CONFIG_PRESET | "arm_front" | Préréglage nommé — origine à la base de l'appareil |
FLOOR_POS_Y | 0.0 m | Plan du plancher fixe (Y de l'application) |
STIFFNESS | 1000 N/m | Constante de rigidité du sol |
MOUNT_STEP_DEG | 10° | Rotation par simple pression sur une touche |
PRINT_EVERY_MS | 200 | Limitation du débit de télémétrie |
Commandes
| Touche | Action |
|---|---|
W / S | Faire pivoter le montage de ±10° autour de l'axe +X de l'appareil (tangage) |
A / D | Faire pivoter le montage de ±10° autour de l'axe +Z de l'appareil (lacet) |
R | Réinitialiser le montage — revenir au préréglage |
H | Afficher les commandes |
Q | Lâcheur |
mount et preset sont mutuellement exclusifsLe service rejette un bloc de configuration de l'appareil qui contient les deux. Une fois que l'utilisateur a remplacé le montage, le tutoriel omet preset de chaque configuration suivante. Appuyez sur R réactive preset lors de la configuration suivante, retire mount.
Les variantes en C++ lisent les entrées ligne par ligne à partir d'un flux d'exécution stdin 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 asynchrone principale — pas besoin de la touche ENTRÉE. Mêmes touches, mêmes commandes.
Champs d'État lus
À partir de data.inverse3[i].state :
cursor_position.y—vec3, utilisé pour calculer la pénétration dans le solcurrent_cursor_force— signalé pour la télémétrie
Envoi / réception
La structure de la charge utile est identique pour toutes les variantes ; les différences notables concernent la manière dont chacune construit l'embranchement mutuellement exclusif mount / preset et la manière dont le fil d'entrée signale le fil 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 défini par les gestionnaires de touches et réinitialisé à chaque fois qu'un bloc configure 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 fils d'exécution : un fil stdin en arrière-plan lit les lignes et bascule pending_configure (non std::atomic<bool>) ; le thread d'E/S libhv vérifie cet indicateur à chaque cycle et émet configure lorsqu'il est levé.
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 se traduit naturellement par std::optional<preset_cfg> et std::optional<mount_cfg> : seul celui qui est renseigné apparaît dans la sérialisation JSON. Le nom du préréglage est modélisé comme un enum class avec une spécialisation glz::meta qui l'associe à la chaîne 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 — consultez tutorials/07-haply-inverse-basis-and-mount/ dans le répertoire d'installation du service.
Source : Python · C++ · C++ Glaze
Voir aussi : Permutation de base · Montage et espace de travail · Configuration de l'appareil · Commandes de contrôle (set_cursor_force) · Types (transformation) · Tutoriel 04 (Bonjour Floor)