07. Base et montage : terrain d'essai
Pilote de manière interactive la transformation de montage de l'appareil et montre comment configure.basis, configure.preset et configure.mount fonctionnent ensemble au niveau du protocole. Conserve un sol 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é Y vers le haut) - 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é au 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 de touche envoie exactement un message de configuration - (C++ Glaze) modélisation de champs mutuellement exclusifs avec
std::optional
Flux de travail
- Au premier message : envoyez le profil de session,
configure.basis: "XZY"etconfigure.preset: arm_front. Commencez à envoyerset_cursor_forcepour le sol fixe. - À chaque cycle : lisez 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. - Au cycle suivant : construisez un bloc
configure.mountavec une transformation dontrotationest un quaternion unitaire (composition Z puis X du tangage / lacet courants). Omettezpreset— les deux sont mutuellement exclusifs au niveau du protocole. - La touche de réinitialisation (
R) efface le remplacement ; la configuration suivante revient àpreset.
Paramètres
| Nom | Défaut | Rôle |
|---|---|---|
BASIS | "XZY" | Permutation des axes — repère d'application orienté Y 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 sol fixe (Y de l'application) |
STIFFNESS | 1000 N/m | Constante de raideur du sol |
MOUNT_STEP_DEG | 10° | Rotation par pression de 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 +X de l'appareil (tangage) |
A / D | Faire pivoter le montage de ±10° autour de +Z de l'appareil (lacet) |
R | Réinitialiser le montage — revenir au préréglage |
H | Afficher les commandes |
Q | Quitter |
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. Appuyer sur R réactive preset lors de la configuration suivante et retire mount.
Les variantes C++ lisent la saisie ligne par ligne dans un fil d'exécution stdin en arrière-plan (appuyez sur ENTRÉE après chaque lettre). Python utilise le paquet keyboard pour la scrutation des touches en temps réel dans la boucle asynchrone principale — pas besoin d'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 la même pour toutes les variantes ; les différences notables concernent la façon dont chacune construit l'embranchement mutuellement exclusif mount / preset et la façon dont le fil d'entrée signale le fil WebSocket.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Boucle asynchrone unique avec scrutation 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 (un std::atomic<bool>) ; le fil 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 de 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 (transform) · Tutoriel 04 (Hello Floor)