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

Haply Hub
최신 버전의 Haply Hub를 다운로드하세요.
허브를 다운로드하여 설치하고 기기를 연결하면, 허브가 펌웨어 업데이트 과정을 안내해 드립니다. 설치가 완료되면, 허브가 실행 중일 때마다 Inverse 서비스가 백그라운드에서 자동으로 실행됩니다.
Hub를 사용하지 않고도 Inverse Service의 특정 버전을 시스템 서비스(Windows) 또는 데몬(Linux/macOS)으로 설치할 수 있습니다. 설치 프로그램 링크 및 설치 방법은 ‘서비스 실행’ 섹션을 참조하십시오.
간단한 예시
서비스에 연결하고, Inverse3 커서 위치를 읽은 다음, 서비스가 상태 프레임을 계속 스트리밍하도록 0 힘의 키프얼라이브를 전송합니다:
- 파이썬
- 자바스크립트 (Node)
- C++ (nlohmann)
- C++ (Glaze)
- 러스트
import asyncio, json, websockets
async def main():
async with websockets.connect("ws://localhost:10001") as ws:
# Handshake: register profile and send a zero-force keepalive
first_state = json.loads(await ws.recv())
device_id = first_state["inverse3"][0]["device_id"]
keepalive = {"inverse3": [{
"device_id": device_id,
"commands": {"set_cursor_force": {"vector": {"x": 0, "y": 0, "z": 0}}}
}]}
while True:
state = json.loads(await ws.recv())
pos = state["inverse3"][0]["state"]["cursor_position"]
print(f"pos: {pos}")
await ws.send(json.dumps(keepalive))
asyncio.run(main())
import WebSocket from 'ws'
const ws = new WebSocket('ws://localhost:10001')
let keepalive
ws.on('message', (msg) => {
const state = JSON.parse(msg)
if (!keepalive) {
const deviceId = state.inverse3[0].device_id
keepalive = JSON.stringify({
inverse3: [
{
device_id: deviceId,
commands: { set_cursor_force: { vector: { x: 0, y: 0, z: 0 } } },
},
],
})
}
console.log('pos:', state.inverse3[0].state.cursor_position)
ws.send(keepalive)
})
#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()) {
keepalive = {{"inverse3", nlohmann::json::array({{
{"device_id", state["inverse3"][0]["device_id"]},
{"commands", {{"set_cursor_force",
{{"vector", {{"x", 0}, {"y", 0}, {"z", 0}}}}}}}
}})}};
}
auto pos = state["inverse3"][0]["state"]["cursor_position"];
std::cout << "pos: " << pos << "\n";
ws.send(keepalive.dump());
};
ws.open("ws://localhost:10001");
std::cin.get();
}
컴파일 시점 JSON 리플렉션을 위해 Glaze를 사용합니다. 읽고 쓰는 최소 형식을 선언하면 나머지는 무시됩니다.
#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()) {
keepalive.inverse3.push_back({state.inverse3[0].device_id});
keepalive.inverse3[0].commands.set_cursor_force = set_cursor_force_cmd{};
}
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);
std::string out; (void)glz::write_json(keepalive, out);
ws.send(out);
};
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<()> {
let (mut ws, _) = connect_async("ws://localhost:10001").await?;
let mut keepalive: Option<String> = None;
while let Some(Ok(Message::Text(msg))) = ws.next().await {
let state: serde_json::Value = serde_json::from_str(&msg)?;
let device_id = state["inverse3"][0]["device_id"].as_str().unwrap();
if keepalive.is_none() {
keepalive = Some(json!({
"inverse3": [{
"device_id": device_id,
"commands": { "set_cursor_force": { "vector": { "x": 0, "y": 0, "z": 0 } } }
}]
}).to_string());
}
println!("pos: {}", state["inverse3"][0]["state"]["cursor_position"]);
ws.send(Message::Text(keepalive.clone().unwrap())).await?;
}
Ok(())
}
힘 값을 변경할 때는 주의하십시오. 갑작스럽게 높은 힘 값을 설정하면 장치가 손상되거나 예기치 않은 동작이 발생할 수 있습니다.
메시지 엔벨로프, 포트 및 콘텐츠 유형에 대한 규칙은 JSON 규약 페이지를 참조하십시오.
더 많은 예시
포스 피드백, 위치 제어, 다중 장치 설정, 마운트/베이스 구성, 이벤트 스트리밍 등을 다루는 Python, C++(nlohmann), C++(Glaze)의 상세한 튜토리얼은 튜토리얼 페이지에서 확인하실 수 있습니다.