02. VerseGrip の出力
最初に有線接続されたVerseGripから、姿勢(クォータニオン + Z-X-Y オイラー角)、ホールセンサーのレベル、ボタンの状態を取得して出力します。
このチュートリアルで学ぶこと:
- 状態フレームから
quaternionの姿勢を読み取る - クォータニオンを、度単位の Z-X-Y オイラー角(+X 右、+Y 前、+Z 上)に変換する
- スタンドアロンのオブザーバーのキープアライブとして
probe_orientationを使用する - 最初のメッセージのみを使ったハンドシェイクパターン(チュートリアル01 と同じ)
ワークフロー
ws://localhost:10001にWebSocketを開き、最初の状態フレームを待ちます。verse_grip配列から、最初に有線接続されたVerseGripのdevice_idを取得します。- セッションプロファイル と、デバイスごとの
probe_orientationキープアライブ(空のオブジェクトのコマンドで、状態フレームにグリップの姿勢が流れ続けるようにします)を含むリクエストを作成します。 - リクエストを送信した後、
sessionフィールドを削除します。これは1回限りのハンドシェイクであるためです。 - 以降のフレームごとに、クォータニオンをオイラー角に変換し、出力頻度を制限したテレメトリを出力します。毎ティック、キープアライブを再送信します。
パラメーター
| 名前 | デフォルト | 用途 |
|---|---|---|
URI | ws://localhost:10001 | シミュレーションチャネル WebSocket のURL |
PRINT_EVERY_MS | 100 | コンソール出力の頻度制限 |
| セッションプロファイル名 | co.haply.inverse.tutorials:print-verse-grip | Haply Hub 内でこのシミュレーションを識別します |
変換は、アプリケーション座標系 +X right, +Y forward, +Z up における固有の Z-X-Y(ヨー → ピッチ → ロール)です。glm::eulerAngles は使用しないでください。 この関数は別の規約に従うため、ここでは誤った値が読み取られます。3つの言語版はいずれも同じ計算を実装しています。計算式はソースを参照してください。
probe_orientation が実際に必要な場合probe_orientation が有用なのは、セッションがInverse3にコマンドを一切送信しない場合のみです。Inverse3(力、位置、トルクなど)にコマンドを送信するとすぐに、サービスは対になっているVerseGripの姿勢を、すべての状態フレームで自動的に配信します。そのためプローブは不要です。probe_orientation は、このチュートリアルのようなスタンドアロンのグリップ監視ツールでのみ使用してください。
読み取る状態フィールド
data.verse_grip[0].state から以下を読み取ります:
orientation—quaternion(w, x, y, z)hall— ホールセンサーの整数値button— 真偽値transform.rotation— ワークスペースの回転(quaternion)。位置とスケールは、姿勢のみを扱うデバイスには該当しないため、送信されることはありません
transform.rotation は、単位クォータニオン {w:1,x:0,y:0,z:0} と等しい場合は省略されます。読み取り時にはデフォルト値を指定してください。常に受信するには、serialization/explicit_fields を有効にしてください。
送受信
WebSocket ループ: 状態フレームを受信し、ハンドシェイクと probe_orientation キープアライブを構築して返送します。最初の送信メッセージにはセッションプロファイルが含まれ、以降のフレームにはキープアライブのみが含まれます。
- Python
- C++ (nlohmann)
- C++ (Glaze)
単一の非同期ループ — recv() → コマンドの構築 → send() → 繰り返し。
async with websockets.connect(URI) as websocket:
while True:
msg = await websocket.recv()
data = json.loads(msg)
if first_message:
first_message = False
device_id = data["verse_grip"][0]["device_id"]
request_msg = {
"session": {"configure": {"profile": {
"name": "co.haply.inverse.tutorials:print-verse-grip"}}},
"verse_grip": [{
"device_id": device_id,
"commands": {"probe_orientation": {}} # empty — keepalive
}]
}
await websocket.send(json.dumps(request_msg))
request_msg.pop("session", None) # one-shot handshake
libhv は I/O スレッド上で WebSocket を駆動します。フレームごとの処理は ws.onmessage に記述されています。メインスレッドは ENTER キーの入力を待機します。
ws.onmessage = [&](const std::string &msg) {
const json data = json::parse(msg);
if (first_message) {
first_message = false;
device_id = data["verse_grip"][0].at("device_id").get<std::string>();
request_msg = {
{"session", {{"configure", {{"profile",
{{"name", "co.haply.inverse.tutorials:print-versegrip"}}}}}}},
{"verse_grip", json::array({
{{"device_id", device_id},
{"commands", {{"probe_orientation", json::object()}}}},
})},
};
}
ws.send(request_msg.dump());
request_msg.erase("session"); // one-shot handshake
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
libhv のコールバックモデルは同じです。型付き構造体が nlohmann::json の代わりに使用されます。probe_orientation_cmd は空の構造体です(Glaze はこれを {} として書き出します)。std::optional<session_cmd> が1回限りのハンドシェイクを扱い、最初の送信後に .reset() を呼び出すと、以降の JSON 出力からそれが省略されます。
// Struct models
struct quat { float w{1.0f}, x{}, y{}, z{}; };
struct grip_state { quat orientation{}; bool button{}; uint8_t hall{}; };
struct grip_device { std::string device_id; grip_state state; };
struct devices_message { std::vector<grip_device> verse_grip; };
struct probe_orientation_cmd {}; // empty object on the wire
struct commands_message {
std::optional<session_cmd> session; // one-shot — omitted when unset
std::vector<device_commands> verse_grip;
};
// Send / receive
ws.onmessage = [&](const std::string &msg) {
devices_message data{};
if (glz::read<glz_settings>(data, msg)) return;
if (first_message) {
first_message = false;
out_cmds.session = session_cmd{ /* profile = print-versegrip */ };
device_commands dc{ .device_id = data.verse_grip[0].device_id };
dc.commands.probe_orientation = probe_orientation_cmd{};
out_cmds.verse_grip.push_back(std::move(dc));
}
std::string out_json;
(void)glz::write_json(out_cmds, out_json);
ws.send(out_json);
out_cmds.session.reset(); // one-shot handshake
};
ws.open("ws://localhost:10001");
while (std::cin.get() != '\n') {} // block main thread
コマンドラインフラグ(Python)
Python バリアントは、チュートリアル01 と同じ2つのフラグを受け付けます:
--full— 1行の要約ではなく、生のJSONペイロードを整形して出力します。--query-config— 送信する各ティックでsession.force_render_full_stateを再送信し、毎フレームにconfigブロック(デバイスタイプ、ファームウェア、マウントの変換など)を含めます。状態の差分だけでなく、毎回完全な情報が届きます。設定の変更をリアルタイムでデバッグする際に便利です。
両方のフラグは組み合わせて使用できます。C++ バリアントでは、これらのフラグは提供されていません。
チュートリアル02も、SDKと一緒にローカルにインストールされています。サービスのインストールディレクトリ配下の tutorials/02-haply-inverse-print-verse-grip/ を確認してください。
関連: 型(クォータニオン) · 制御コマンド(probe_orientation) · WebSocket プロトコル · チュートリアル03(Wireless VG)