Haply Inverse SDK
이 Haply Inverse SDK 는 WebSocket과 HTTP를 기반으로 한, Haply 의 햅틱 기기(여기에는 Inverse3Inverse3x, Minverse, VerseGrip 및 Wireless VerseGrip 등이 포함됩니다. 이 인터페이스는 로컬 서비스로 실행되며, 장치 인식, 직렬 통신, 보안 모니터링 및 상태 스트리밍을 처리하므로, 애플리케이션은 소켓을 통해 JSON으로만 통신하면 됩니다.
주요 기능은 다음과 같습니다:
- 기기 감지 및 관리 – 연결된 Haply 기기를 나열하고 HTTP REST API를 통해 자동으로 구성합니다.
- 실시간 상태 스트리밍 – 웹소켓을 통해 햅틱 제어에 필요한 속도(수 kHz 범위)로 장치 상태를 전송합니다.
- 명령 처리 – 정밀한 햅틱 피드백을 제공하기 위해 힘 및 위치 명령을 높은 정확도로 실행합니다.
- 백그라운드 실행 – 로컬 서비스로 실행되며, 사용자의 개입 없이 장치를 항상 사용 가능한 상태로 유지합니다.
Haply 허브 설치
가장 쉽게 시작할 수 있는 방법은 Haply Hub 입니다. 이는 Haply 기기를 설치, 실행, 구성, 테스트 및 모니터링할 수 있는 데스크톱 애플리케이션입니다. 이 애플리케이션은 펌웨어를 최신 상태로 유지하고, Inverse Service를 통합하며, 코드를 한 줄도 작성하기 전에 하드웨어를 확인할 수 있는 데모 기능을 제공합니다.

Haply Hub
Haply Hub의 최신 버전을 다운로드하세요
허브를 다운로드하여 설치하고, 기기를 연결하면 허브가 필요한 펌웨어 업데이트 절차를 안내해 드립니다. 설치가 완료되면 허브를 실행하는 즉시 Inverse Service가 백그라운드에서 자동으로 실행됩니다.
또한 허브 없이 Inverse Service의 특정 버전을 시스템 서비스(Windows) 또는 데몬(Linux/macOS)으로 설치할 수도 있습니다. 설치 프로그램 링크와 설치 안내서는 ‘서비스 실행’ 섹션에서 확인하실 수 있습니다.
간단한 예시
서비스에 연결하고, 첫 번째 Inverse3 의 커서 위치를 읽어온 다음, 서비스가 상태 프레임을 계속 스트리밍하도록 강도가 0인 Keepalive를 전송하십시오:
- 파이썬
- 자바스크립트 (Node)
- C++ (nlohmann)
- C++ (최적화됨)
- 러스트
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();
}
Glaze (세계에서 가장 빠른 JSON 라이브러리 중 하나)를 사용하여 컴파일 시점에 리플렉션을 구현하세요. 햅틱 속도(수 kHz)에서는 JSON 구문 분석에 소요되는 1마이크로초마다 제어 루프에서 소중한 시간이 손실되므로, 이는 매우 중요합니다. 읽고 쓰는 데 필요한 최소한의 구조만 선언하면 되며, 그 외의 모든 내용은 무시됩니다.
#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();
}
다음과 같은 구체적인 예시 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(())
}
힘 값을 변경할 때는 주의하십시오. 갑자기 높은 힘 값이 설정되면 장치가 손상되거나 예기치 않은 동작이 발생할 수 있습니다.
메시지 엔벨로프, 포트 및 콘텐츠 유형 규칙에 대한 JSON 규약 페이지를 참조하십시오.
기타 예시
Python, C++ (nlohmann) 및 C++ (Glaze)에 대한 전체 튜토리얼(힘 피드백, 위치 제어, 다중 장치 설정, 마운트/베이스 구성 및 이벤트 스트리밍 포함)은 ‘튜토리얼’ 페이지에서 확인하실 수 있습니다.