02. VerseGrip の出力
最初に有線で接続されたVerseGripから、姿勢(クォータニオン+Z-X-Yオイラー角)、ホールセンサーのレベル、ボタンの状態を取得して出力します。
このチュートリアルで学べる内容:
- 状態フレームから
quaternionの姿勢を読み取る - クォータニオンを、度単位の Z-X-Y オイラー角(+X が右、+Y が前、+Z が上)に変換する
- スタンドアロンのオブザーバーのキープアライブとして
probe_orientationを使用する - 最初のメッセージのみを使用したハンドシェイクパターン(チュートリアル01と同じ)
ワークフロー
ws://localhost:10001WebSocketを開き、最初のステートフレームを待ちます。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)