本文へスキップ

08. リモートセッションコンフィギュレータ

別の場所(他のアプリ、Unityシーン、Haply Hubのデモなど)ですでに実行中のセッションを再構成します。このチュートリアルではWebSocketを開かず、デバイスに対してHTTP REST呼び出し(GET、POST、DELETE)のみを送信します。他のアプリがハプティックレンダリングを継続している間も、ベース、ワークスペースプリセット、マウントトランスフォームを変更することができます。

ユースケース​

  • 実行中のデモをリアルタイムで調整する。 Haply HubOrbデモを起動した後、別のターミナルでこのチュートリアルを実行して、基底の順列を変更したり、ワークスペースのプリセットを変更したり、マウント変換を微調整したりします。デモを停止しなくても、Orb の座標系が即座に切り替わります。
  • ユーザーごとのワークスペースのキャリブレーション。 メインマシンでハプティックシーンを継続して実行し、同じネットワーク上のオペレーターが mount オフセット/回転/拡大縮小を反映させます。そうすることで、仮想ワークスペースがユーザーの机に合わせて調整されます。
  • デバイス選択オプションメニュー。 同じHTTPヘルパーを使用して GET /devices(チュートリアル 00 (参考)をクエリすると、デバイス一覧を取得して対話型メニューを作成できます。デバイスを選択した後、再構成を行いますが、セッションのWebSocketには影響しません。このチュートリアルでは、 /sessionsについて問いかけ、 *inverse/0をハードコーディングしますが、 /devices 基盤セレクターへの切り替えは、ローカル変更で行うことができます。
  • スクリプトによる再構成。セッションの録画が開始される前に、事前処理(basis + preset + mountの設定)を自動化します。設定をすべてのクライアントに含める必要はありません。

前提条件​

チュートリアル 08 では、すでに実行中のセッションを再構成します。アクティブなハプティックセッションが必要です。他のチュートリアル、Unity シーン、Haply Hub のデモなど、どれでも構いません。

セッションを最も速く実行する方法

Haply Hubを開き、Orbのデモを実行してから、そのセッションを直接ターゲットとして指定します:

./08-haply-inverse-http-remote-config --session co.haply.hub::demo-orb
python 08-haply-inverse-http-remote-config.py --session "co.haply.hub::demo-orb"

「Orb」シーンは、デバイスワークスペースに球体(sphere)をレンダリングします。チュートリアル08の手順に従って、basisやプリセットを切り替えたり、マウントトランスフォームを微調整したりすると、Orbの座標系が画面上でリアルタイムに移動します。

使用方法​

# Pick a session interactively (lists every session the service knows)
./08-haply-inverse-http-remote-config
python 08-haply-inverse-http-remote-config.py

# Target the Haply Hub Orb demo directly
./08-haply-inverse-http-remote-config --session co.haply.hub::demo-orb
python 08-haply-inverse-http-remote-config.py --session "co.haply.hub::demo-orb"

# Target one directly by selector
./08-haply-inverse-http-remote-config --session :my_profile:0
python 08-haply-inverse-http-remote-config.py --session "#42"

# Or by a wildcard profile pattern (first match) — handy when the exact profile is unknown
./08-haply-inverse-http-remote-config --session "co.haply.hub::*:0"

# Pick a specific device on the session (default is *inverse/0)
./08-haply-inverse-http-remote-config --device 'inverse3/#A12' # by exact id
./08-haply-inverse-http-remote-config --device '*inverse:left/0' # first left-handed inverse
./08-haply-inverse-http-remote-config --device '*inverse:right/0' # first right-handed inverse
./08-haply-inverse-http-remote-config --device '*verse_grip/0' # first grip-family device

チュートリアルは、開始時に現在のセッションのbasis / preset / mountを出力した後、キー入力を待ちます。キーを1回押すごとに、REST呼び出しが正確に1つずつ送信されます。

シミュレーションでのプロファイル名の設定

プロファイル名のないセッションは、数値のIDのみで指定することができ、このIDは実行するたびに変わります。メインアプリが最初のメッセージで session.configure.profile.nameを呼び出すように設定すると、すべての実行において --session :my_profile:0のような固定セレクタを再利用できます。 セッション — プロフィール名を参照してください。

キーバインディング​

キー動作
B基底、順列、巡回
Pワークスペースプリセットの切り替え
W / E / Rマウント編集モードの選択 — 位置 (mm) / 回転 (°) / 倍率 (%)
← / →現在のモードで、−X/+X方向に1段階移動する
↑ / ↓現在のモードで、+Y/−Y方向に1段階移動する
Page Up / Page Down現在のモードで、+Z/−Z方向に1段階移動する
= / -3つの軸を同時に均一に±倍率調整(常時利用可能)
Deletebasis + preset + mount に DELETE 適用 — デバイスのデフォルト設定に戻す
Hヘルプを表示
Esc終了 (Ctrl+C(動作も)

HTTPメソッド — GET、POST、DELETE​

このチュートリアルでは、HTTPメソッドのうち3つだけを使用します。すべての呼び出しは標準の JSONエンベロープ(成功した場合 {"ok": true, "data": {...}}, 失敗した場合は {"ok": false, "error": "..."}) の形式で応答し、ステータスコードは次の3つのいずれかになります: 200 成功、 400 リクエスト形式が不正です、 404 セレクタに一致する項目はありません。

メソッド役割使用経路
GET現在の状態の読み取り — セッション一覧の照会、特定のセッションの照会、現在の設定値の照会/sessions, /sessions/<selector>, /<device_selector>/config/{basis,preset,mount}?session=...
POST設定値の置き換え — 本文はJSON/<device_selector>/config/{basis,preset,mount}?session=...
DELETE設定値をデバイスのデフォルト値に戻す/<device_selector>/config/{basis,preset,mount}?session=...

HTTPヘルパー​

3つのメソッドを包み込む薄いラッパーであり、これにより、以降のチュートリアルのコードをビジネスロジックを中心に読み解くことができるようになります:

Pythonでは、HTTPのキープアライブのために requests.Session()を使用します(リクエストあたりの遅延時間を約50 msから約5 msに短縮します):

http = requests.Session()

def api_get(path):
r = http.get(f"{BASE_URL}{path}", timeout=3)
return r.json() if r.status_code == 200 else None

def api_post(path, body):
r = http.post(f"{BASE_URL}{path}", json=body, timeout=3)
return r.json() if r.status_code == 200 else None

def api_delete(path):
r = http.delete(f"{BASE_URL}{path}", timeout=3)
return r.json() if r.status_code == 200 else None

def session_url(endpoint):
return f"{endpoint}?session={session_selector}"

セッションの探索 — GET /sessions​

--session オプションに応じて分岐します:

  • --session SELECTORが指定されている場合 → GET /sessions/<SELECTOR> 1回呼び出し。 200その結果を利用し、 404エラーが発生した場合は、エラーメッセージを出力して終了します。
  • フラグがない場合 → GET /sessions(一覧照会) → プロフィール名を含むセッションの表示 → インデックス入力リクエスト → 最終セレクタの生成 (可能であれば :profile:0を優先して使用し、なければ #idに置き換えます)。

SELECTORは セレクタ — セッション・セレクタで定義されているすべての形式が許可されます: :profile:instance, #id, :-1, :0、一般プロフィール名、または co.haply.hub::*:0のような プロフィール名のワイルドカード パターンです。チュートリアルではこの文字列をそのまま渡しており、解析はサービス側が担当します。

def discover_session(session_arg):
global session_selector

if session_arg:
# Direct lookup (e.g. ":my_profile:0", "#42", ":-1")
if api_get(f"/sessions/{session_arg}") is None:
return False
session_selector = session_arg
return True

# Otherwise: list and pick
data = api_get("/sessions")
sessions = data.get("data", {}).get("sessions", [])
for i, s in enumerate(sessions):
name = s.get("config", {}).get("profile", {}).get("name", "default")
print(f" [{i}] session #{s['session_id']} profile={name}")

picked = sessions[int(input("Pick session index: "))]
name = picked.get("config", {}).get("profile", {}).get("name", "")
# Prefer the profile selector — it survives restarts; id doesn't
session_selector = (f":{name}:0" if name and name != "default"
else f"#{picked['session_id']}")
return True

デバイスセレクター — *inverse/0​

すべての設定呼び出しは、デバイス単位で範囲が指定されます。このチュートリアルでは、製品ファミリーのワイルドカードとインデックスを組み合わせた形式のセレクタを使用しています:

/*inverse/0/config/<key>
  • *inverseInverse 製品群に属するすべてのデバイス(inverse3, inverse3x, minverse)と一致します — 特定のモデルに関係なく、チュートリアルは修正なしで動作します。
  • 0は、その製品群内で0から始まるインデックスです。チュートリアルでは、常に最初のInverseのみを使用します。

対象デバイスを変更するには、文字列を1つ変更するだけで済みます — --deviceを指定してください(パスはこの値に基づいて構成されます):

/verse_grip/0/config/basis?session=... # --device verse_grip/0
/*verse_grip/0/config/basis?session=... # --device '*verse_grip/0' — first grip, any kind
/inverse3/A14/config/mount?session=... # --device 'inverse3/#A14'
/*inverse:left/0/config/basis?session=... # --device '*inverse:left/0' — first left-handed inverse
/*inverse:right/0/config/basis?session=... # --device '*inverse:right/0' — first right-handed inverse

:left / :right クラスはインデックス付け 以前、 適用されます — *inverse:left/0は「0番目の 左利き 「逆」であり、「0番目の逆で、おそらく左側にあるもの」ではありません。詳細については セレクタ — インデックス作成前のクラスフィルターの解釈を参照してください。

対象のデバイスがない場合、チュートリアル08は終了します

チュートリアルは開始時に GET …/config/basisをリクエストします。セッションに --device一致するデバイスがない場合(例:右利き専用設定で --device '*inverse:left/0'を使用する場合)、本サービスは 404 no device matches selector '<sel>'と回答し、チュートリアルは Device '<sel>' not found in session '<sel>'.を出力した後、終了状態 1で終わります。古いデフォルト値のため、キーボードループには入りません。

文法全体は セレクタ — デバイスセレクタを参照してください。ハードコーディングする代わりに、デバイス選択メニューを作成するには GET /devices?session=<selector>(チュートリアル 00)でデバイスを一覧表示し、選択した device_id設定パスに接続してください。

POST設定 — 基本設定、プリセット、マウント​

3つのキーはリクエスト形式は同じですが、本文のスキーマがそれぞれ異なります。すべてのPOSTリクエストは、結果値を dataに入れて 200を返し、セッションまたはデバイスセレクタに一致する項目がない場合は 404を返します。

基礎​

POST /*inverse/0/config/basis?session=:my_profile:0
Content-Type: application/json

{"permutation": "XZY"}

回答: {"ok": true, "data": {"permutation": "XZY"}}

def post_basis():
perm, _ = BASIS_OPTIONS[basis_index]
api_post(session_url("/inverse3/0/config/basis"), {"permutation": perm})

プリセット​

POST /*inverse/0/config/preset?session=:my_profile:0
Content-Type: application/json

{"preset": "arm_front_centered"}

回答: {"ok": true, "data": {"preset": "arm_front_centered"}}

def post_preset():
preset = PRESET_OPTIONS[preset_index]
api_post(session_url("/inverse3/0/config/preset"), {"preset": preset})

マウント​

POST /*inverse/0/config/mount?session=:my_profile:0
Content-Type: application/json

{
"transform": {
"position": {"x": 0.02, "y": 0.0, "z": 0.0},
"rotation": {"w": 0.966, "x": 0.0, "y": 0.259, "z": 0.0},
"scale": {"x": 1.0, "y": 1.0, "z": 1.0}
}
}

回答: {"ok": true, "data": {"transform": { ... }}} — 正規化後の有効な変換をそのまま返します。

def post_mount():
body = {
"transform": {
"position": {"x": mount_pos[0], "y": mount_pos[1], "z": mount_pos[2]},
"rotation": quat_from_euler_deg(*mount_rot),
"scale": {"x": mount_scale[0], "y": mount_scale[1], "z": mount_scale[2]},
}
}
api_post(session_url("/inverse3/0/config/mount"), body)
mountわあ presetは互いに排他的です

一方にPOSTリクエストを送信すると、デバイス上のもう一方の設定が消去されます。このチュートリアルでは、この動作を明示的に追跡していません。各POSTリクエストは独立しており、競合はサーバー側で解決されます。WebSocketの観点から見た同様のルールについては、チュートリアル07を参照してください。

DELETE 初期化 — 3回の呼び出し​

resetは、設定キーごとに DELETE を 1 回ずつ呼び出します。各呼び出しは dataそこに、現在デフォルト値となっている値を格納して 200を返します。

def reset_all():
api_delete(session_url("/inverse3/0/config/basis"))
api_delete(session_url("/inverse3/0/config/preset"))
api_delete(session_url("/inverse3/0/config/mount"))

マウントの回転構成​

transform.rotationは、転送形式における単位クォータニアンです。このチュートリアルでは、回転を Z-Y-X の固有オイラー3項で保存します(X軸を基準としたピッチ、Z軸を基準としたヨー、Y軸を基準としたロール — いずれも度単位)。そして、すべての POST ごとにクォータニアンを再構成します。

def quat_from_euler_deg(pitch_x, yaw_z, roll_y):
"""Hamilton quaternion for q = q_z * q_y * q_x (apply X, then Y, then Z)."""
hx, hy, hz = (math.radians(a) * 0.5 for a in (pitch_x, roll_y, yaw_z))
cx, sx = math.cos(hx), math.sin(hx)
cy, sy = math.cos(hy), math.sin(hy)
cz, sz = math.cos(hz), math.sin(hz)
return {
"w": cz*cy*cx + sz*sy*sx,
"x": cz*cy*sx - sz*sy*cx,
"y": cz*sy*cx + sz*cy*sx,
"z": sz*cy*cx - cz*sy*sx,
}
クォータニオン規約

単位ハミルトン四元数、右利き座標系、スカラー優先(w) という方式です。これはサービスの他の部分と同じ規約であり、 quaternionを参照してください。合成の手順は Z-Y-X 内在(intrinsic) 方式(q = q_z * q_y * q_x)です。つまり、まずX軸を中心にピッチ(pitch)を適用し、次にY軸を中心にロール(roll)を、最後にZ軸を中心にヨー(yaw)を適用します。

チュートリアルでは、各ステータスバーに導出されたクォータニオンとオイラー角の値が表示されるため、デバイスが回転する前に合成結果を検証することができます。ローカルのオイラー状態は、セッションにすでに保存されている値とは関係なく、 (0, 0, 0)から始まります。最初の mount POSTが、既存の値を上書きします。

入力モデル(概要)​

HTTPの接続構造が核心であり、キーボードのUXは二次的なものです。ここでは、以下の2つの意図的な簡略化手法を採用しています。

  • Python 実装では keyboard パッケージを使用します。クロスプラットフォーム対応で、キーを押し続けている際の繰り返し入力をデフォルトで処理します。方向キー、 Page Up / Page Down, = / - キーを押している間、マウント軸が移動し、 B および P キーは、立ち上がりエッジ(rising edge)でベースとプリセットを切り替えます。
  • C++ 実装では std::getline(std::cin, ...)および簡潔なトークン構文(x+20, sx-5, u+10)を使用します。連続的な微調整にはあまり適していませんが、プラットフォームごとのコンソールAPIについては #ifdefを使用しなくても移植が可能です。

出典​

SDKインストーラーとともに配布

チュートリアル 08 も SDK とともにローカルにインストールされます。サービスインストールディレクトリ内の tutorials/08-haply-inverse-http-remote-config/ 経路をご確認ください。

関連ドキュメント: セッション — リモートコントロール·セレクタ·デバイス設定·基本順列·マウントとワークスペース·JSON 規約·チュートリアル 00 — デバイス一覧·チュートリアル 07 — 基本順列とマウント(WebSocket 版)