Haply Inverse SDK
Haply Inverse SDK는 언어에 구애받지 않는 WebSocket + HTTP 인터페이스로, Haply 햅틱 장치(Inverse3, Inverse3x, Minverse, VerseGrip, Wireless VerseGrip)에 접근할 수 있게 해 줍니다. 로컬 서비스로 실행되며 장치 탐색, 시리얼 통신, 안전 모니터링 및 상태 스트리밍을 처리하므로, 애플리케이션은 소켓을 통해 JSON만 사용하면 됩니다.
주요 기능은 다음과 같습니다:
- 장치 탐색 및 관리 — HTTP REST API를 통해 연결된 Haply 장치를 자동으로 나열하고 구성합니다.
- 실시간 상태 스트리밍 — WebSockets를 통해 햅틱 제어 주기(수 kHz 수준)로 장치 상태를 전달합니다.
- 명령 처리 — 정밀한 햅틱 피드백을 위해 힘 및 위치 명령을 높은 충실도로 실행합니다.
- 백그라운드 작동 — 로컬 서비스로 실행되며, 사용자 개입 없이 장치를 사용할 준비 상태로 유지합니다.
Haply Hub 설치하기
시작하는 가장 쉬운 방법은 **Haply Hub**입니다 — Haply 장치를 설치, 실행, 구성, 테스트 및 모니터링하는 데스크톱 애플리케이션입니다. 펌웨어를 최신 상태로 유지하며, Inverse Service 번들 및 데모가 포함되어 있어 코드를 작성하기 전에 하드웨어를 확인할 수 있습니다.

Haply Hub
Haply Hub의 최신 버전 다운로드
Hub를 다운로드하여 설치한 후 장치를 연결하면, Hub가 펌웨어 업데이트를 안내합니다. 설치가 완료되면 Hub가 열려 있을 때마다 백그라운드에서 Inverse Service가 자동으로 실행됩니다.
Hub 없이도 Inverse Service의 특정 버전을 시스템 서비스(Windows) 또는 데몬(Linux / macOS)으로 설치할 수 있습니다. 설치 프로그램 링크 및 설명은 서비스 실행 문서를 참조하십시오.
간단한 예제
서비스에 연결하여 첫 번째 Inverse3의 커서 위치를 읽고, 힘이 0인 킵얼라이브(keepalive)를 전송하여 서비스가 상태 프레임을 계속 스트리밍하도록 합니다:
- Python
- JavaScript (Node)
- C++ (nlohmann)
- C++ (최적화)
- 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();
}
Glaze는 세계에서 가장 빠른 JSON 라이브러리 중 하나로, 컴파일 타임 리플렉션에 사용합니다. 햅틱 주기(수 kHz)에서는 JSON 파싱에 소요되는 모든 마이크로초가 제어 루프에서 빼앗기는 마이크로초이므로 이 점이 중요합니다. 읽고 쓰는 데 필요한 최소한의 형태만 선언하십시오. 나머지는 무시됩니다.
#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(())
}
힘 값을 변경할 때는 주의하십시오. 갑작스럽게 높은 힘 값을 주면 장치가 손상되거나 예기치 않은 동작이 발생할 수 있습니다.
메시지 엔벨로프, 포트, content-type 규칙은 JSON 규약 페이지를 참조하십시오.
추가 예제
Python, C++ (nlohmann), C++ (Glaze)를 사용한 전체 기능 튜토리얼(힘 피드백, 위치 제어, 다중 장치 설정, 마운트/basis 구성, 이벤트 스트리밍 포함)은 튜토리얼 페이지를 참조하십시오.