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

Haply Hub
Haply Hub의 최신 버전 다운로드
Hub를 다운로드하여 설치하고 기기를 연결하면, Hub가 펌웨어 업데이트 과정을 단계별로 안내해 드립니다. 설치가 완료되면 Hub의 전원이 켜져 있는 한Inverse Service가 백그라운드에서 자동으로 실행됩니다.
Hub를 사용하지 않고도 특정 버전의 Inverse Service를 시스템 서비스(Windows) 또는 백그라운드 프로세스(Linux/macOS) 형태로 설치할 수 있습니다. 설치 프로그램 링크 및설치방법은‘서비스 실행’ 섹션을참조하십시오.
간단한 예시
서비스에 연결하고, 첫 번째 Inverse3의 커서 위치를 읽은 다음, 제로 포스(zero force) 활성 유지 신호를 전송하여 서비스가 상태 프레임을 지속적으로 푸시하도록 합니다:
- 파이썬
- 자바스크립트 (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();
}
컴파일 시점 리플렉션을 위해 세계에서 가장 빠른 JSON 라이브러리 중 하나인 Glaze를사용하세요. 촉각 재생률(수천 헤르츠)에서는 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(())
}
힘의 값을 조정할 때는 주의하십시오. 갑자기 높은 힘의 값을 적용하면 장비가 손상되거나 예기치 않은 동작이 발생할 수 있습니다.
메시지 캡슐화, 포트 및 콘텐츠 유형(content-type) 규칙에 대해서는 JSON 규약 페이지를 참조하십시오.
더 많은 예시
힘 피드백, 위치 제어, 다중 장치 구성, 마운트/베이스 구성 및 이벤트 스트림을 다루는 Python, C++(nlohmann) 및 C++(Glaze)의 모든 기능을 다룬 튜토리얼을 보려면튜토리얼 페이지를참조하십시오.