Haply Inverse SDK
Le Haply Inverse SDK は、Haply — いわゆる Inverse3、Inverse3x、 Minverse、VerseGrip、Wireless VerseGripといったハプティックデバイス向けの、WebSocket + HTTPに基づく言語非依存のインターフェースです。[email protected] — これはローカルサービスとして動作し、デバイスの検出、シリアル通信、セキュリティ監視、ステータスの配信を管理するため、アプリケーション側はソケットを介してJSONで通信するだけで済みます。
その機能には以下が含まれます:
- デバイスの検出と管理— REST HTTP API を通じて、接続されたHaply デバイスを自動的に検出・設定します。
- リアルタイムの状態配信— WebSocketsを介して、デバイスの状態をハプティック制御周波数(数kHz)で送信します。
- コマンド処理— 力および位置のコマンドを高い精度で実行し、正確なハプティックフィードバックを実現します。
- バックグラウンドでの動作— ローカルサービスとして実行され、ユーザーの操作を必要とせずにデバイスを常時利用可能な状態に保ちます。
Haply ハブを使用した設置
始めるのに最も簡単な方法は、 Haply Hubを使用することです。これは、デバイスのインストール、実行、設定、テスト、監視を行うためのデスクトップアプリケーションです(Haply )。このアプリケーションはファームウェアを最新の状態に保ち、Inverse Serviceを統合しており、コードを1行も書く前にハードウェアを確認できるデモも含まれています。

Haply
Haply Hubの最新バージョンをダウンロードする
Hubをダウンロードしてインストールし、デバイスを接続すると、Hubがファームウェアの更新手順を順を追って案内します。インストールが完了すると、Hubを開くたびにInverse Serviceがバックグラウンドで自動的に実行されます。
また、Hubを経由せずに、特定のバージョンのInverse Serviceをシステムサービス(Windows)またはデーモン(Linux / macOS)としてインストールすることも可能です。インストーラーへのリンクや手順については、「サービスの実行」をご覧ください。
簡単な例
サービスに接続し、最初のInverse3 のカーソル位置を読み取り、サービスがステータスフレームの送信を継続するように、強制値ゼロの維持メッセージを送信してください:
- 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();
}
コンパイル時のリフレクションには、世界最速クラスのJSONライブラリの一つであるGlazeを使用しています。ハプティック周波数(数kHzオーダー)では、JSONの解析に費やされる1マイクロ秒ごとに、制御ループから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)に関する包括的なチュートリアル(フォースフィードバック、位置制御、マルチデバイス構成、マウントおよびベースマークの設定、イベントフローなどを網羅)については、「チュートリアル」ページをご覧ください。