Haply Inverse SDK
Die Haply Inverse SDK ist eine sprachunabhängige Schnittstelle aus WebSocket + HTTP zu „ Haply “-Haptikgeräten – darunter Inverse3Inverse3x, Minverse, VerseGrip und Wireless VerseGrip. Die Schnittstelle läuft als lokaler Dienst, der die Geräteerkennung, serielle Kommunikation, Sicherheitsüberwachung und das Streaming von Zustandsdaten übernimmt – sodass Ihre Anwendung lediglich über einen Socket mit JSON kommunizieren muss.
Zu den Funktionen gehören:
- Geräteerkennung und -verwaltung – listet verbundene „ Haply “-Geräte auf und konfiguriert sie automatisch über eine HTTP-REST-API.
- Echtzeit-Zustandsstreaming – übermittelt den Gerätestatus mit den für die haptische Steuerung erforderlichen Raten (im Multi-kHz-Bereich) über WebSockets.
- Befehlsverarbeitung – führt Kraft- und Positionsbefehle mit hoher Genauigkeit aus, um präzises haptisches Feedback zu gewährleisten.
- Hintergrundbetrieb – läuft als lokaler Dienst und hält die Geräte ohne Benutzereingriff betriebsbereit.
Installation mit einem „ Haply “-Hub
Der einfachste Einstieg ist der Haply Hub – eine Desktop-Anwendung zum Installieren, Ausführen, Konfigurieren, Testen und Überwachen Ihrer „ Haply “-Geräte. Die Anwendung hält die Firmware auf dem neuesten Stand, bündelt den Inverse Service und enthält Demos, mit denen Sie Ihre Hardware überprüfen können, bevor Sie auch nur eine Zeile Code schreiben.

Haply Hub
Laden Sie die neueste Version von „ Haply Hub“ herunter
Laden Sie den Hub herunter und installieren Sie ihn, schließen Sie Ihr Gerät an, und der Hub führt Sie durch eventuelle Firmware-Updates. Nach der Installation läuft Inverse Service automatisch im Hintergrund, sobald der Hub geöffnet ist.
Sie können auch eine bestimmte Version von Inverse Service ohne den Hub als Systemdienst (Windows) oder Daemon (Linux/macOS) installieren. Den Link zum Installationsprogramm und die Anleitung finden Sie unter „Ausführen des Dienstes“.
Kurzes Beispiel
Stellen Sie eine Verbindung zum Dienst her, lesen Sie die Cursorposition des ersten „ Inverse3 “ aus und senden Sie ein Keepalive mit der Stärke Null, damit der Dienst weiterhin Zustandsframes überträgt:
- Python
- JavaScript (Node)
- C++ (nlohmann)
- C++ (optimiert)
- Rost
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();
}
Mit Glaze – einer der schnellsten JSON-Bibliotheken der Welt – für Reflection zur Kompilierungszeit. Bei hohen Frequenzen (mehrere kHz) geht jede Mikrosekunde, die für das Parsen von JSON aufgewendet wird, der Regelungsschleife verloren; deshalb ist dies wichtig. Deklarieren Sie nur die Mindeststruktur, die Sie lesen und schreiben möchten; alles Weitere wird ignoriert.
#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();
}
Ein anschauliches Beispiel mit 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(())
}
Passen Sie die Kraftwerte mit Bedacht an. Plötzlich auftretende hohe Kraftwerte können das Gerät beschädigen oder zu unerwartetem Verhalten führen.
Siehe die Seite „JSON-Konventionen“ für den Nachrichten-Envelope, die Ports und die Content-Type-Regeln.
Weitere Beispiele
Umfassende Tutorials zu Python, C++ (nlohmann) und C++ (Glaze) – mit Kraftrückmeldung, Positionsregelung, Setups mit mehreren Geräten, Mount-/Basis-Konfiguration und Event-Streaming – finden Sie auf der Seite „Tutorials“.