04. Bonjour Floor
Voici le premier effet tactile. Lorsque l'on appuie sur le curseur, cela crée un sol virtuel horizontal qui offre une résistance. La force est modélisée par un simple ressort de pénalité (stiffness × penetration_depth), et,set_cursor_force est appliquée dans la direction Z par l'intermédiaire de.
Ce que vous apprendrez dans ce tutoriel :
set_cursor_forcepour appliquer la force du ressort de pénalitécursor_positionlire les données et calculer la force en temps réel- (Python)Préréglages d'espace de travail(
arm_front_centered) et faire en sorte que le point d'origine se trouve au centre de l'espace de travail - (Python)
keyboardÀ l'aide du package, régler de manière interactive, à l'aide du clavier, la hauteur du plancher et la constante de rappel
Flux de travail
ws://localhost:10001Ouvrez un WebSocket et attendez la première trame d'état.- Dans la première image,Profil de sessionenregistrer. La version Python, en outre,
configure.preset: arm_front_centeredEnvoyez-le de manière à ce que le point d'origine se trouve au centre de l'espace de travail. La version C++ utilise telles quelles les configurations déjà activées sur l'appareil. - À chaque image,
cursor_position.zlire,force_z = max(0, (floor_pos - z) * stiffness)en calculant,set_cursor_forceJe l'envoie sous forme de commande. - À partir du tick suivant, seules les commandes de force seront envoyées. Le profil de session correspond à une poignée de main unique.
- (Python) À chaque tick, on interroge également les touches fléchées du clavier, et
floor_posetstiffnessest mis à jour en temps réel.
Paramètres
| Nom | Par défaut | Utilisation |
|---|---|---|
floor_pos | 0.10 m | Coordonnée Z du plan du sol virtuel |
stiffness | 1000 N/m | Constante de ressort (1 N pour une pénétration de 1 mm) |
PRINT_EVERY_MS | 100–200 | Intervalle d'échantillonnage de la télémétrie |
| Nom du profil de session | co.haply.inverse.tutorials:hello-floor | Haply Identifiez cette simulation dans Hub |
La version Python est keyboard Utilisez le paquet (sous Linux, des droits d'administrateur sont nécessaires) :
↑/↓— Relever / abaisser le plancher←/→— Réduire / augmenter la constante de ressortR— Réinitialiser les paramètres par défaut
Dans le système « Service Tick »,les forces s'additionnent. Avant d'être transmises à l'appareil, les forces provenant de plusieurs sources sont additionnées. Ce type de tutoriel peut coexister avec d'autres sources de force sans que l'une ou l'autre ne soit bloquée.
Champ d'état de lecture
data.inverse3[i].state Extrait de :
cursor_position.z—vec3。Ceci sert à calculer la profondeur d'enfoncement.current_cursor_force— Ces données sont transmises à des fins de télémétrie
Envoyer / Recevoir
À chaque tick, lire la coordonnée Z du curseur, etforce_z = max(0, (floor_pos - z) * stiffness) en calculant,set_cursor_force est envoyé. Le premier message envoyé contient également (dans toutes les versions) le profil de session ; dans la version Python, configure.preset: arm_front_centered est également inclus.
- Python
- C++ (nlohmann)
- C++ (Glaze)
Il s'agit d'une boucle asynchrone unique. La négociation initiale, lors de la première trame, implique le profil et configure.preset comprend,floor_pos = 0.1 on s'assure qu'ils soient alignés sur les coordonnées centrales de l'espace de travail.
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 + preset (one-shot)
request_msg = {
"session": {"configure": {"profile": {"name": "co.haply.inverse.tutorials:hello-floor"}}},
"inverse3": [{
"device_id": device_id,
"configure": {"preset": {"preset": "arm_front_centered"}}
}]
}
else:
# Per tick: compute force along Z, send set_cursor_force
z = data["inverse3"][0]["state"]["cursor_position"]["z"]
force_z = 0.0 if z > floor_pos else (floor_pos - z) * stiffness
request_msg = {
"inverse3": [{
"device_id": device_id,
"commands": {"set_cursor_force":
{"vector": {"x": 0.0, "y": 0.0, "z": force_z}}}
}]
}
await websocket.send(json.dumps(request_msg))
Il s'agit du modèle de rappel de libhv. La fonction `onmessage` s'exécute sur le thread d'E/S de WebSocket, tandis que le thread principal attend (se bloque) la touche ENTRÉE. La variante C++ implémente une procédure de handshake minimale (profil de session uniquement, sans préréglage).
ws.onmessage = [&](const std::string &msg) {
const json data = json::parse(msg);
// If no Inverse3 yet, ask the service to re-send the full state
if (!data.contains("inverse3") || data["inverse3"].empty()) {
ws.send(R"({"session":{"force_render_full_state":{}}})");
return;
}
json request = {};
if (first_message) {
first_message = false;
request["session"] = {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:hello-floor"}}}}}};
}
request["inverse3"] = json::array();
for (auto &el : data["inverse3"].items()) {
const float z = el.value()["state"]["cursor_position"]["z"].get<float>();
const float force_z = z > floor_pos ? 0.0f : (floor_pos - z) * stiffness;
request["inverse3"].push_back({
{"device_id", el.value()["device_id"]},
{"commands", {{"set_cursor_force",
{{"vector", {{"x", 0.0}, {"y", 0.0}, {"z", force_z}}}}}}},
});
}
ws.send(request.dump());
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
Le modèle de rappel de libhv reste le même. Seule la partie principale change. Des structures typées sont utilisées à la fois pour l'état et pour les commandes.std::optional<session_cmd> Il conserve un profil unique ; s'il n'est pas défini, Glaze l'omettra du JSON sérialisé.
// Struct models
struct vec3 { float x{}, y{}, z{}; };
struct inverse_state { vec3 cursor_position{}, current_cursor_force{}; };
struct inverse_device { std::string device_id; inverse_state state; };
struct devices_message { std::vector<inverse_device> inverse3; };
struct set_cursor_force_cmd { vec3 vector; };
struct commands_message {
std::optional<session_cmd> session;
std::vector<device_commands> inverse3;
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
if (data.inverse3.empty()) {
ws.send(R"({"session":{"force_render_full_state":{}}})");
return;
}
commands_message out_cmds{};
if (first_message) {
first_message = false;
out_cmds.session = session_cmd{ /* profile = hello-floor */ };
}
for (const auto &dev : data.inverse3) {
const float z = dev.state.cursor_position.z;
const float force_z = z > floor_pos ? 0.0f : (floor_pos - z) * stiffness;
device_commands dc{ .device_id = dev.device_id };
dc.commands.set_cursor_force = set_cursor_force_cmd{{0.0f, 0.0f, force_z}};
out_cmds.inverse3.push_back(std::move(dc));
}
std::string out_json;
(void)glz::write_json(out_cmds, out_json);
ws.send(out_json);
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
Le tutoriel 04 est également installé localement avec le SDK. Dans le répertoire d'installation du service, sous tutorials/04-haply-inverse-hello-floor/ Veuillez consulter [...].
Source : Python · C++ · C++ Glaze
À lire également : Commandes de contrôle (set_cursor_force) · Support et espace de travail (préréglages) · Type (vec3) · Session · Tutoriel 07 (Base et support)