メインコンテンツまでスキップ

シミュレーションチャネル

概要​

シミュレーションチャネルは、デバイスの状態をやり取りし、セッションまたはデバイスのコマンドを送信するために使用される、高頻度・双方向のWebSocketチャネルです。

デフォルトの URL: ws://localhost:10001

ポートは設定で変更できます。

コアの取り決め(重要):

  • クライアントが接続すると、サービスは全デバイスの完全な一覧を含む 初期インベントリメッセージ を送信します。
  • その後、サーバーは、クライアントから受信した メッセージごとに 正確に1つ の State Update メッセージを送信します。
  • 各 State Update には、すべてのデバイスの状態(およびステータス)が含まれます。
モジュール

このページでは、シミュレーションチャネルのコアプロトコルとコマンドについて説明します。

追加のモジュールや機能は、独自のコマンドや状態フィールドを登録してシステムを拡張できます。それらについては モジュール セクションで個別に説明しています。

プロトコルのルール​

クライアントメッセージごとに1つの応答​

サービスは、クライアントから受信した各メッセージに対して、すべてのデバイスの状態を含む 1つ の State Update メッセージを送信します。

これは次の意味です:

  • デバイス状態の最新のスナップショットが必要な場合は、何らかの メッセージ(セッションコマンド、プローブコマンド、またはデバイスコマンド)を送信する必要があります。
  • このチャネルは、クライアントメッセージによって駆動される「ティック」ループのように動作します。

力を印加せずにポーリングする(プローブコマンド)​

力を印加したりシミュレーションパラメーターを変更したりせずに、状態の変化を監視したい場合は、プローブコマンドを使用してください:

  • inverse3 向けの probe_position
  • Verse Grip 向けの probe_orientation

プローブコマンドにはコマンドデータは含まれず、デバイス情報の問い合わせを強制するだけです。これにより、次の State Update で位置・向きのデータが最新の状態になります。

プローブコマンドが不要な場合があります

制御コマンド(set_cursor_force、set_cursor_position など)をすでに送信しているシミュレーションセッションでは、プローブコマンドを送信する必要はありません。デバイスの状態は、常にレスポンスフレームに含まれます。プローブは、デバイスを動作させることなくデバイスの状態をポーリングする必要がある、監視専用のセッション(例: Haply Hub)向けです。

設定と状態​

  • 初期インベントリ メッセージには、デバイスの config、state、status が含まれます。
  • 通常の State Update メッセージには、デバイスの state と status が含まれます。

設定を含むスナップショットを再度取得する必要がある場合は、session.force_render_full_state を使用してください。

configure と commands の違い​

デバイスメッセージは、configure と commands の 2 つの別々のマップをサポートします。両者は異なる目的で使用されます:

マップ目的送信頻度
configure一度だけ設定する永続的な構成: プリセット、基準(basis)、取り付け(mount)、フィルター、モジュール設定1回、または変更時に送信
commandsティックごとの非永続的な指令: 力、位置、トルク毎ティック送信が必要

commands 内のエントリ(set_cursor_force、set_cursor_position、set_angular_torques、set_angular_position)は、ティックごとに1回適用され、保存されません。送信を止めると、効果はただちになくなり、デバイスはアイドル状態に戻ります。再送信しなくてもティックをまたいで保持すべきものには、configure を使用してください。

注記
set_transform は特別なケースです

set_transform は commands の下に置かれますが、永続的です。サービスは、新しいトランスフォームを送信するまで、最後に送信されたトランスフォームを保持します。毎ティック送信する必要はありません。とはいえ、その目的はカメラ/シーンのナビゲーションであるため、ティックレートでストリーミングする(例: ユーザーがシーンをパンするときにフレームごとに1つのトランスフォームを送信する)ことが、連続的な動きのナビゲーションにおける想定される使用パターンです。

座標系​

Haply は、デフォルトで Z-Up の 右手系 座標系を使用します。

2 つの configure エントリが、座標の解釈と返却の方法に影響します:

  • session.configure.basis: Haply の座標とアプリケーションの座標系との対応関係を変更します(下記の Basis を参照)。
  • inverse3[*].configure.preset: ワークスペースの原点を配置する、名前付きの工場出荷時構成を選択します(例: arm_front_centered は (0, 0, 0) をワークスペースの中心に配置します)。デバイスごとの Configure セクションを参照してください。

メッセージフォーマット​

このセクションでは、メッセージの上位レベルのエンベロープとメッセージの種類について説明します。完全な例は、このドキュメントの後半に掲載しています。

デバイスグループ​

メッセージは、トップレベルでデバイスタイプごとにグループ化されます(例: inverse3、verse_grip、wireless_verse_grip)。各デバイスタイプのキーは、デバイスごとのオブジェクトの配列に対応します。

初期インベントリ(サーバー → クライアント)​

WebSocket への接続が確立された直後に、1 回送信されます。

各エントリには以下が含まれます:

  • device_id
  • config
  • state
  • status

参照: 例: 初期インベントリのペイロード

State Update(サーバー → クライアント)​

クライアントメッセージごとに 1 回送信されます。

各エントリには以下が含まれます:

  • device_id
  • state
  • status

参照: 例: State Update のペイロード

セッションコマンドのエンベロープ(クライアント → サーバー)​

セッションコマンドは、現在の接続/セッションに適用されるアクションであり、デバイス固有ではありません。

{
"session": {
"<command_name>": {
"...": "..."
}
}
}

デバイスコマンドのエンベロープ(クライアント → サーバー)​

デバイスコマンドは、デバイスタイプのキーの下に配列として送信され、1 つのメッセージで 1 台以上のデバイスにコマンドを送信できます。各デバイスのエントリは、configure マップと commands マップの両方をサポートします。

{
"<device_type>": [
{
"device_id": "<id>",
"configure": {
"<config_key>": { "...": "..." }
},
"commands": {
"<command_name>": { "...": "..." }
}
}
]
}

同じ種類の複数のデバイスにコマンドを送信するために、1 つのメッセージに複数のエントリを含めることができます。 commands はディクショナリであり、1 つのデバイスに対して複数のコマンドを含めることができます。ただし、コマンドの種類ごとに 1 つのみです。

execute フィールド​

すべての command または configure エントリは、省略可能な "execute" ブール値(デフォルトは true)を受け付けます。"execute": false を設定すると、エントリは解析されますが適用されません。これは、すべてのフィールドを常に出力するリフレクションベースのシリアライザー(例: Unity JsonUtility)で役立ちます。

"set_cursor_force": { "execute": false, "vector": { "x": 0.0, "y": 0.0, "z": 0.0 } }

コマンドリファレンス​

セッション​

完全な状態の強制レンダリング​

すべてのデバイスの状態と構成のスナップショットを要求します。

{
"session": {
"force_render_full_state": {}
}
}

セッションの構成​

セッションレベルの永続的な構成は session.configure を介して送信されます。これはデバイスレベルの configure マップのパターンを踏襲しています。

プロファイル​

セッションのプロファイル名を設定します。この名前は Haply Hub がシミュレーションを認識し、アプリごとのデバイス調整を保存するために使用されます。

{
"session": {
"configure": {
"profile": {
"name": "my_profile"
}
}
}
}
基底(セッションレベル)​

セッション全体の基底マッピングを設定します。基底マッピングは、 Haply の座標がアプリケーションの座標系へどのように変換されるかを表します。 設定後は、すべてのデバイス状態がその基底で返され、送信するすべての値もその基底で解釈されます。

順列文字列は、Haply の右手系・Z 軸上向きの座標系を基準として表されます。X、Y、Z の順列で、必要に応じて + または - を前置します。例:

  • XYZ、ZYX、+Y-Z+X、X-ZY
  • YZX は、アプリケーションの Y が Haply の右方向、Z が Haply の前方向、X が Haply の上方向であることを意味します。

左手系・Z 軸上向きの例(Unreal、X-YZ):

{
"session": {
"configure": {
"basis": { "permutation": "X-YZ" }
}
}
}
警告
session.set_basis からの移行

従来の session.set_basis と session.configure.basis の間で、軸の符号の規則が変更されました。旧コマンドで動作していた順列が、新しいコマンドでは逆向きの変換を生成する場合があります。必要に応じて軸の符号を反転してください(例:X-ZY は XZ-Y になります)。

全デバイス共通のコマンド​

プロービング(全デバイス)​

プロービングコマンドを使用すると、力の印加やその他のシミュレーションの変更を行わずに、最新の位置・姿勢情報を要求できます。

  • inverse3: probe_position
  • Verse Grip(verse_grip、wireless_verse_grip): probe_orientation
{
"inverse3": [
{
"device_id": "049D",
"commands": {
"probe_position": {}
}
}
],
"verse_grip": [
{
"device_id": "049D",
"commands": {
"probe_orientation": {}
}
}
],
"wireless_verse_grip": [
{
"device_id": "049D",
"commands": {
"probe_orientation": {}
}
}
]
}

変換の設定(全デバイス)​

デバイスのワークスペース変換(デバイス空間からアプリケーション空間への変換)を設定します。

ほかの commands エントリとは異なり、set_transform は永続的です。サービスは、次の値を送信するまで最後に送信された値を保持します。毎ティック送信する必要はありませんが、用途がカメラやシーンのナビゲーションであるため、連続的な動きに対しては、ティックレートでストリーミングすることが想定される使い方です(例:ユーザーがシーンをパンしている間、フレームごとに 1 つの変換を送信する)。

{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_transform": {
"transform": {
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0 },
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 }
}
}
}
}
]
}
Verse Grip / Wireless Verse Grip

Verse Grip および Wireless Verse Grip デバイスでは、変換のうち rotation 成分のみが効果を持ちます。グリップは姿勢のみを報告するため、position や scale で拡大縮小や平行移動の対象となるものがありません。position と scale のフィールドは、Inverse3 とのスキーマの一貫性を保つためだけに受け付けられ(スナップショットにもそのまま返されます)、恒等値(position = {0,0,0}、scale = {1,1,1})のまま使用するのが標準的な方法です。

Inverse3 デバイスの configure​

構成エントリは configure マップで送信します。これらは永続的です。一度送信するか、値が変更されたときに送信してください。

プリセット​

{
"inverse3": [
{
"device_id": "049D",
"configure": {
"preset": { "preset": "arm_front_centered" }
}
}
]
}

指定可能な値:device_defaults、arm_front、arm_front_centered、led_front、led_front_centered、custom。

基底(デバイスレベル)​

{
"inverse3": [
{
"device_id": "049D",
"configure": {
"basis": { "permutation": "ZXY" }
}
}
]
}

マウント​

デバイスのマウント変換を設定します。mount 内の transform ラッパーに注意してください。

{
"inverse3": [
{
"device_id": "049D",
"configure": {
"mount": {
"transform": {
"position": { "x": 0.0, "y": 0.0, "z": 0.0 },
"rotation": { "x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0 },
"scale": { "x": 1.0, "y": 1.0, "z": 1.0 }
}
}
}
}
]
}
mount スキーマの非対称性

コマンド側(クライアントからサーバーへ)では、変換がラッパーで包まれます:"mount": { "transform": { ... } }。 スナップショット側(サーバーからクライアントへ)では、フラットな構造になります:"mount": { "position": {...}, "rotation": {...}, "scale": {...} }。 これは意図的な設計です。コマンドは統一されたラッパーを使用し、スナップショットは変換を直接シリアライズします。スナップショットの形式をコマンドのペイロードにコピーしないよう注意してください。

減衰​

均一な減衰および/または方向別の減衰を制御します。少なくとも 1 つのフィールドを指定する必要があります。

{
"inverse3": [
{
"device_id": "049D",
"configure": {
"damping": { "scalar": 0.5 }
}
}
]
}

方向別の減衰を設定することも、両方を同時に設定することもできます:

"damping": { "scalar": 0.5, "vector": { "x": 0.0, "y": 1.0, "z": 0.0 } }

フォースゲート​

フォースゲートの減衰ゲイン(0.0~1.0)を設定します。

{
"inverse3": [
{
"device_id": "049D",
"configure": {
"force_gate": { "gain": 0.3 }
}
}
]
}

Inverse3 コマンド​

inverse3 デバイスにコマンドを送信するには、inverse3 キーの下に、一致する device_id を持つエントリを含めます。コマンドはティック単位で有効であり、有効な状態を維持するには毎ティック再送信する必要があります。

1 つのデバイスにコマンドを送信する場合:

{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}

1 つのメッセージで複数のデバイスにコマンドを送信する:

{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
},
{
"device_id": "049E",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}

カーソル位置の設定​

{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_position": {
"position": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}

カーソルの力の設定​

{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_cursor_force": {
"vector": {
"x": 1.0,
"y": 2.0,
"z": 3.0
}
}
}
}
]
}

角度位置の設定​

{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_angular_position": {
"angles": {
"a0": 1.0,
"a1": 2.0,
"a2": 3.0
}
}
}
}
]
}

角度トルクの設定​

{
"inverse3": [
{
"device_id": "049D",
"commands": {
"set_angular_torques": {
"torques": {
"a0": 1.0,
"a1": 2.0,
"a2": 3.0
}
}
}
}
]
}

Verse Grip の configure​

Verse Grip および Wireless Verse Grip デバイスは、preset、basis、mount について、Inverse3 と同じ configure キーをサポートします。

拡張 Verse Grip​

set_extension_data コマンドは、ボード拡張通信プロトコルを実装したグリップのバージョンで使用される、Verse Grip の拡張プロトコルの一部です。

グリップ拡張データの設定​

サポートされるデータ長:

  • アップストリーム(クライアント → デバイス):最大 20 バイト。
  • ダウンストリーム(デバイス → クライアント):最大 12 バイト。state.extension_data として State Update メッセージで返されます。

データ仕様:

  • 配列の長さ:20 バイト
  • 値の範囲:各値は 0~255
{
"wireless_verse_grip": [
{
"device_id": "049D",
"commands": {
"set_extension_data": {
"extension_data": [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
}
}
}
]
}

例​

例:初期インベントリのペイロード​

WebSocket が接続されると、サービスは完全なデバイス一覧を含むメッセージを送信します。初期メッセージは次の JSON 形式を持ちます:

{
"inverse3": [
{
"device_id": "04BA",
"config": {
"type": "inverse3",
"port": "COM13",
"device_info": {
"major_version": 7,
"minor_version": 1,
"id": "04BA",
"device_type": 4,
"uuid": "2D35F80DD9005F599B68F49944CB04BA"
},
"extended_device_id": "2D35F80DD9005F599B68F49944CB04BA",
"extended_firmware_version": "8C20FDC8010AA1E15AA133CDA2534874",
"gravity_compensation": {
"enabled": true,
"scaling_factor": 1
},
"handedness": "right",
"streaming_mode": "USB",
"torque_scaling": {
"enabled": true
},
"home_return": {
"enabled": false
},
"filters": {
"force_gate": { "gain": 0.3 },
"damping": { "scalar": 0 }
},
"preset": "defaults",
"basis": { "permutation": "XYZ" },
"mount": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
}
},
"state": {
"cursor_position": {
"x": 0.07842738,
"y": -0.14836666,
"z": 0.14297646
},
"cursor_velocity": {
"x": -0.011969013,
"y": 0.0012009288,
"z": -0.043197
},
"angular_position": {
"x": -69.31704,
"y": 137.62952,
"z": 19.832787
},
"angular_velocity": {
"x": 0,
"y": 0,
"z": 0
},
"body_orientation": {
"x": -0.01940918,
"y": 0.7026367,
"z": 0.00048828125,
"w": 0.7113037
},
"current_cursor_force": { "x": 0, "y": 0, "z": 0 },
"current_cursor_position": { "x": 0, "y": 0, "z": 0 },
"current_angular_torques": { "x": 0, "y": 0, "z": 0 },
"current_angular_position": { "x": 0, "y": 0, "z": 0 },
"control_domain": "cartesian",
"control_mode": "force",
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"calibrated": false,
"in_use": false,
"power_supply": true,
"ready": true,
"started": true
}
}
],
"verse_grip": [
{
"device_id": "61548",
"config": {
"type": "verse_grip",
"port": "COM3",
"preset": "device_defaults",
"basis": { "permutation": "XYZ" },
"mount": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
}
},
"state": {
"button": false,
"hall": 0,
"orientation": {
"x": -0.5019531,
"y": 0.8632202,
"z": -0.048095703,
"w": -0.022338867
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"error": 0,
"ready": true
}
}
],
"wireless_verse_grip": [
{
"device_id": "0",
"config": {
"type": "wireless_verse_grip",
"port": "COM6",
"major_version": 1,
"minor_version": 4,
"hardware_version": 1,
"streaming_mode": "Radio",
"preset": "device_defaults",
"basis": { "permutation": "XYZ" },
"mount": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
}
},
"state": {
"battery_level": 0.816,
"battery_voltage": 3.77,
"buttons": {
"a": false,
"b": false,
"c": false
},
"hall": 16,
"orientation": {
"x": -0.019866943,
"y": -0.017486572,
"z": 0.05508423,
"w": -0.9963989
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"connected": true,
"awake": true,
"ready": true
}
}
]
}

例:状態更新のペイロード​

サービスは、受信したメッセージごとに、すべてのデバイスの状態を含む状態更新メッセージを 1 つ送信します。

マシンの状態を知りたい場合は、事前にメッセージを送信しておく必要があります(例:値がゼロであっても、プローブコマンドや力の値など)。力を印加せずにデバイスを入力ソース(例:位置のトラッキング)として使用する場合に、これは特に重要です。

状態更新メッセージは次の JSON 形式を持ちます:

{
"inverse3": [
{
"device_id": "04BA",
"state": {
"cursor_position": {
"x": 0.07842738,
"y": -0.14836666,
"z": 0.14297646
},
"cursor_velocity": {
"x": -0.011969013,
"y": 0.0012009288,
"z": -0.043197
},
"angular_position": {
"x": -69.31704,
"y": 137.62952,
"z": 19.832787
},
"angular_velocity": {
"x": 0,
"y": 0,
"z": 0
},
"body_orientation": {
"x": -0.01940918,
"y": 0.7026367,
"z": 0.00048828125,
"w": 0.7113037
},
"current_cursor_force": { "x": 0, "y": 0, "z": 0 },
"current_cursor_position": { "x": 0, "y": 0, "z": 0 },
"current_angular_torques": { "x": 0, "y": 0, "z": 0 },
"current_angular_position": { "x": 0, "y": 0, "z": 0 },
"control_domain": "cartesian",
"control_mode": "force",
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"calibrated": false,
"in_use": false,
"power_supply": true,
"ready": true,
"started": true
}
}
],
"verse_grip": [
{
"device_id": "61548",
"state": {
"button": false,
"hall": 0,
"orientation": {
"x": -0.5019531,
"y": 0.8632202,
"z": -0.048095703,
"w": -0.022338867
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"error": 0,
"ready": true
}
}
],
"wireless_verse_grip": [
{
"device_id": "0",
"state": {
"battery_level": 0.816,
"battery_voltage": 3.77,
"buttons": {
"a": false,
"b": false,
"c": false
},
"hall": 16,
"orientation": {
"x": -0.019866943,
"y": -0.017486572,
"z": 0.05508423,
"w": -0.9963989
},
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"connected": true,
"awake": true,
"ready": true
}
}
],
"custom_verse_grip": [
{
"device_id": "0",
"state": {
"battery_level": 0.816,
"battery_voltage": 3.77,
"buttons": {
"a": false,
"b": false,
"c": false
},
"hall": 16,
"orientation": {
"x": -0.019866943,
"y": -0.017486572,
"z": 0.05508423,
"w": -0.9963989
},
"extension_data": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 1, "y": 1, "z": 1 }
},
"transform_velocity": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"scale": { "x": 0, "y": 0, "z": 0 }
}
},
"status": {
"ready": true
}
}
]
}