Haply Inverse SDK
Le Haply Inverse SDK est une interface indépendante du langage WebSocket + HTTP pour les appareils haptiques Haply — les Inverse3, Inverse3x, Minverse, VerseGrip et Wireless VerseGrip. Il s'exécute comme service local qui gère la découverte des appareils, la communication série, la surveillance de la sécurité et la diffusion de l'état, de sorte que votre application n'a qu'à communiquer avec JSON au moyen d'un socket.
Ses fonctionnalités comprennent :
- Découverte et gestion des appareils — répertorie et configure automatiquement les appareils Haply connectés au moyen d'une API REST HTTP.
- Diffusion de l'état en temps réel — transmet l'état des appareils aux fréquences de contrôle haptique (plusieurs kHz) via WebSockets.
- Traitement des commandes — exécute les commandes de force et de position avec une grande fidélité pour une rétroaction haptique précise.
- Fonctionnement en arrière-plan — s'exécute comme service local et maintient les appareils prêts, sans intervention de l'utilisateur.
Installation avec le Haply Hub
La façon la plus simple de commencer est d'utiliser le Haply Hub, une application de bureau permettant d'installer, d'exécuter, de configurer, de tester et de surveiller vos appareils Haply. Elle maintient le micrologiciel à jour, regroupe le Inverse Service et inclut des démonstrations qui vous permettent de vérifier votre matériel avant d'écrire la moindre ligne de code.

Haply Hub
Télécharger la dernière version du Haply Hub
Téléchargez et installez le Hub, branchez votre appareil, et le Hub vous guidera tout au long des mises à jour du micrologiciel. Une fois installé, le Inverse Service s'exécute automatiquement en arrière-plan chaque fois que le Hub est ouvert.
Vous pouvez également installer une version précise du Inverse Service comme service système (Windows) ou démon (Linux / macOS), sans passer par le Hub. Consultez Exécution du service pour le lien vers le programme d'installation et les instructions.
Exemple rapide
Connectez-vous au service, lisez la position du curseur du premier Inverse3, et envoyez un message de maintien à force nulle pour que le service continue de diffuser les trames d'état :
- Python
- JavaScript (Node)
- C++ (nlohmann)
- C++ (optimisé)
- Rust
import asyncio, json, websockets
async def main():
# Connect to the Haply Inverse service WebSocket
async with websockets.connect("ws://localhost:10001") as ws:
# Read the first state frame to discover the device id
first_state = json.loads(await ws.recv())
device_id = first_state["inverse3"][0]["device_id"]
# Build a zero-force keepalive command targeting that device
keepalive = {"inverse3": [{
"device_id": device_id,
"commands": {"set_cursor_force": {"vector": {"x": 0, "y": 0, "z": 0}}}
}]}
# Realtime loop: one send per tick, then read the resulting state
while True:
await ws.send(json.dumps(keepalive))
state = json.loads(await ws.recv())
pos = state["inverse3"][0]["state"]["cursor_position"]
print(f"pos: {pos}")
asyncio.run(main())
import WebSocket from 'ws'
// Connect to the Haply Inverse service WebSocket
const ws = new WebSocket('ws://localhost:10001')
let keepalive
ws.on('message', (msg) => {
const state = JSON.parse(msg)
if (!keepalive) {
// Read the first state frame to discover the device id
const deviceId = state.inverse3[0].device_id
// Build a zero-force keepalive command targeting that device
keepalive = JSON.stringify({
inverse3: [
{
device_id: deviceId,
commands: { set_cursor_force: { vector: { x: 0, y: 0, z: 0 } } },
},
],
})
ws.send(keepalive)
return
}
// Realtime loop: one send per tick, then read the resulting state
ws.send(keepalive)
console.log('pos:', state.inverse3[0].state.cursor_position)
})
#include <external/libhv.h>
#include <nlohmann/json.hpp>
int main() {
hv::WebSocketClient ws;
nlohmann::json keepalive;
ws.onmessage = [&](const std::string& msg) {
auto state = nlohmann::json::parse(msg);
if (keepalive.is_null()) {
// Read the first state frame to discover the device id
auto device_id = state["inverse3"][0]["device_id"];
// Build a zero-force keepalive command targeting that device
keepalive = {{"inverse3", nlohmann::json::array({{
{"device_id", device_id},
{"commands", {{"set_cursor_force",
{{"vector", {{"x", 0}, {"y", 0}, {"z", 0}}}}}}}
}})}};
ws.send(keepalive.dump());
return;
}
// Realtime loop: one send per tick, then read the resulting state
ws.send(keepalive.dump());
auto pos = state["inverse3"][0]["state"]["cursor_position"];
std::cout << "pos: " << pos << "\n";
};
// Connect to the Haply Inverse service WebSocket
ws.open("ws://localhost:10001");
std::cin.get();
}
En utilisant Glaze — l'une des bibliothèques JSON les plus rapides au monde — pour la réflexion à la compilation. Aux fréquences haptiques (de l'ordre de plusieurs kHz), chaque microseconde consacrée à l'analyse de JSON est une microseconde volée à la boucle de contrôle, d'où l'importance de ce point. Déclarez la structure minimale que vous lisez et écrivez ; le reste est ignoré.
#include <external/libhv.h>
#include <glaze/glaze.hpp>
struct vec3 { float x{}, y{}, z{}; };
struct inverse_state { vec3 cursor_position; };
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 device_cmd { std::string device_id;
struct { std::optional<set_cursor_force_cmd> set_cursor_force; } commands; };
struct commands_message { std::vector<device_cmd> inverse3; };
int main() {
hv::WebSocketClient ws;
commands_message keepalive;
ws.onmessage = [&](const std::string& msg) {
devices_message state{};
if (glz::read_json(state, msg)) return;
if (keepalive.inverse3.empty()) {
// Read the first state frame to discover the device id
auto device_id = state.inverse3[0].device_id;
// Build a zero-force keepalive command targeting that device
keepalive.inverse3.push_back({device_id});
keepalive.inverse3[0].commands.set_cursor_force = set_cursor_force_cmd{};
std::string out; (void)glz::write_json(keepalive, out);
ws.send(out);
return;
}
// Realtime loop: one send per tick, then read the resulting state
std::string out; (void)glz::write_json(keepalive, out);
ws.send(out);
printf("pos: %f %f %f\n", state.inverse3[0].state.cursor_position.x,
state.inverse3[0].state.cursor_position.y,
state.inverse3[0].state.cursor_position.z);
};
// Connect to the Haply Inverse service WebSocket
ws.open("ws://localhost:10001");
std::cin.get();
}
Exemple illustratif utilisant tokio-tungstenite + serde_json.
use futures_util::{SinkExt, StreamExt};
use serde_json::json;
use tokio_tungstenite::{connect_async, tungstenite::Message};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Connect to the Haply Inverse service WebSocket
let (mut ws, _) = connect_async("ws://localhost:10001").await?;
// Read the first state frame to discover the device id
let first = match ws.next().await {
Some(Ok(Message::Text(m))) => m,
_ => anyhow::bail!("no initial state frame"),
};
let first_state: serde_json::Value = serde_json::from_str(&first)?;
let device_id = first_state["inverse3"][0]["device_id"].as_str().unwrap();
// Build a zero-force keepalive command targeting that device
let keepalive = json!({
"inverse3": [{
"device_id": device_id,
"commands": { "set_cursor_force": { "vector": { "x": 0, "y": 0, "z": 0 } } }
}]
}).to_string();
// Realtime loop: one send per tick, then read the resulting state
loop {
ws.send(Message::Text(keepalive.clone())).await?;
let msg = match ws.next().await {
Some(Ok(Message::Text(m))) => m,
_ => break,
};
let state: serde_json::Value = serde_json::from_str(&msg)?;
println!("pos: {}", state["inverse3"][0]["state"]["cursor_position"]);
}
Ok(())
}
Modifiez les valeurs de force avec prudence. Des valeurs de force élevées et soudaines peuvent endommager l'appareil ou provoquer un comportement inattendu.
Consultez la page Conventions JSON pour l'enveloppe des messages, les ports et les règles de type de contenu.
Plus d'exemples
Pour des tutoriels complets en Python, C++ (nlohmann) et C++ (Glaze) — couvrant le retour de force, le contrôle de position, les configurations multi-appareils, la configuration du montage et du repère de base, ainsi que le flux d'événements — consultez la page Tutoriels.