04. Bonjour Floor
Ton premier effet tactile : un sol virtuel horizontal qui génère une force de répulsion lorsque le curseur s'y enfonce. Cette force correspond à un modèle simple de ressort de pénalité —stiffness × penetration_depth——Approuvé par set_cursor_force Appliquer le long de l'axe Z.
Vous apprendrez :
- Utilisation
set_cursor_forceAppliquer une force de rappel - Lire
cursor_positionet calcule la force en temps réel - (Python) Configuration Préréglages d'espace de travail(
arm_front_centered), de manière à ce que l'origine se trouve au centre de l'espace de travail - (Python) via
keyboardRéglage interactif de la hauteur et de la rigidité du sol
Processus de travail
- Ouvrir à la page
ws://localhost:10001WebSocket, puis attendre la première trame d'état. - À la première image : inscription Fichier de configuration de session. La version Python enverra également
configure.preset: arm_front_centered, de manière à ce que le point d'origine se trouve au centre de l'espace de travail ; la version C++ utilise quant à elle la configuration actuellement active sur l'appareil. - À chaque image : lecture
cursor_position.z, calculforce_z = max(0, (floor_pos - z) * stiffness), et viaset_cursor_forceLa commande le renvoie. - À chaque cycle suivant, seules des commandes de force sont envoyées — le profil de session ne sert qu'à établir une poignée de main initiale.
- (Python) À chaque cycle, le programme interroge également les touches de direction du clavier et met à jour les données en temps réel.
floor_pos/stiffness。
Paramètres
| Nom | Valeur par défaut | Utilisation |
|---|---|---|
floor_pos | 0.10 m | Coordonnée Z du plan virtuel du sol |
stiffness | 1000 N/m | Constante de ressort (pénétration de 1 mm → 1 N) |
PRINT_EVERY_MS | 100–200 | Régulation à distance du débit |
| Nom du profil de session | co.haply.inverse.tutorials:hello-floor | Identifier cette simulation dans le Hub « Haply » |
Version de Python utilisée keyboard Paquet (nécessite des droits d'administrateur sous Linux) :
↑/↓——Rehausser / abaisser le niveau du sol←/→——Réduire / Augmenter la rigiditéR——Rétablir les valeurs par défaut
Au cours du cycle de service, les forces sontcumulables: avant d'être transmises à l'appareil, les forces provenant de différentes sources sont d'abord additionnées. Des exemples tels que celui présenté dans ce tutoriel peuvent coexister avec d'autres sources de génération de forces sans se bloquer mutuellement.
Champs d'état lus
Source : data.inverse3[i].state:
cursor_position.z—vec3, servant à calculer la profondeur de pénétrationcurrent_cursor_force— Destiné à la transmission de données de télémétrie
Envoyer / Recevoir
À chaque cycle : lire la coordonnée Z du curseur, calculer force_z = max(0, (floor_pos - z) * stiffness), puis envoyer un set_cursor_force Commande. Le message envoyé par l'article 1er contient également un profil de session (toutes versions) ; pour les versions Python, il contient en outre configure.preset: arm_front_centered。
- Python
- C++ (nlohmann)
- C++ (Glaze)
Cycle asynchrone unique. La poignée de main de la première trame contient le profil et configure.preset, ce qui fait que floor_pos = 0.1 Aligner avec les coordonnées du centre 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))
Modèle de rappel libhv — la fonction `onmessage` s'exécute sur le thread d'E/S WebSocket ; le thread principal est bloqué en attente de la touche ENTER. La variante C++ ne propose que la procédure de poignée de main la plus simple (comprenant uniquement le profil de session, 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
Comme pour le modèle de rappel de libhv — seule la partie principale diffère. Les états et les commandes utilisent tous deux des structures à typage fort.std::optional<session_cmd> Enregistrer les fichiers de configuration à usage unique — s'ils ne sont pas définis, Glaze les 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 — veuillez vous rendre dans le répertoire d'installation du service, sous tutorials/04-haply-inverse-hello-floor/ Rechercher dans.
Code source : Python · C++ · C++ Glaze
Contenu associé : Commandes de contrôle (set_cursor_force) · Montage et espace de travail (paramètres par défaut) · Type (vec3) · Conversation · Tutoriel 07 (Socle et fixation)