本文へスキップ
バージョン: 3.5.x

08. リモートセッション設定ツール

別のアプリ、Unityシーン、Haply デモなど、どこかで既に実行中のセッションに対して、そのデバイスへHTTP RESTリクエストを送信することで設定を変更します。このチュートリアルではWebSocketは使用しません。他のアプリがハプティクスのレンダリングを継続している間に、GET、POST、DELETEリクエストを送信して、ベース、ワークスペースプリセット、またはマウントトランスフォームを変更します。

ユースケース

  • 実行中のデモをリアルタイムで調整しましょう。 Haply Orbデモを起動し、別のターミナルでこのチュートリアルを実行して、基底の順列を入れ替えたり、ワークスペースのプリセットを変更したり、マウント変換を微調整したりしてみてください。デモを停止することなく、Orb の座標系が即座に変化します。
  • ユーザーごとのワークスペースのキャリブレーション。 メインマシンでハプティック・シーンを実行したままにし、同じネットワーク上のオペレーターが mount オフセット/回転/拡大縮小を行い、仮想ワークスペースがユーザーのデスクと重なるようにします。
  • デバイス選択機能付きのオプションメニュー。 同じHTTPヘルパーを使用してクエリを実行できます GET /devices (参照 チュートリアル 00) を使用してデバイスを列挙し、セッションのWebSocketに触れることなく、インタラクティブなメニュー(デバイスを選択して再設定する)を構築します。このチュートリアルでは、 /sessions そしてハードコーディングする *inverse/0、しかし、 /devices-driven ピッカーはローカルな変更です。
  • スクリプトによる再構成。セッションの録画開始前に、事前準備の手順(ベースの設定、プリセット、マウント)を自動化します。これにより、各クライアントに設定を個別に組み込む必要がなくなります。

前提条件

チュートリアル08では、すでに実行中のセッションの設定を変更します。これを行うには、アクティブなハプティックセッション(別のチュートリアル、Unityシーン、Haply デモなど)が必要です。

セッションを立ち上げる最も手っ取り早い方法

Haply を開き、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」シーンでは、デバイスのワークスペース内に球体がレンダリングされます。ベースやプリセットを切り替えたり、チュートリアル08の手順に従ってマウントのトランスフォームを微調整したりすると、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"

このチュートリアルでは、起動時にセッションの現在のベース/プリセット/マウントを表示し、その後キー入力を待機します。キーが押されるたびに、REST呼び出しが1回だけ送信されます。

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

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

キー割り当て

キーアクション
Bサイクルを基にした置換
Pワークスペースのプリセットをリセット
W / E / Rマウント編集モードを選択 — 位置 (mm) / 回転 (°) / 拡大率 (%)
/ 現在のモードで−X / +X ステップ
/ 現在のモードで +Y / −Y ステップ
Page Up / Page Down現在のモードで +Z / −Z ステップ
= / -3軸すべてで同時にスケールを統一(常に利用可能)
DeleteDELETE ベース + プリセット + マウント — デバイスのデフォルト設定に戻す
Hヘルプを表示
Esc終了 (Ctrl+C (これも有効です)

HTTPメソッド — GET、POST、DELETE

このチュートリアルでは、3つのHTTPメソッドのみを使用しています。すべてのリクエストは標準の 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の用途 requests.Session() HTTPのキープアライブ機能(リクエストごとの遅延を約50ミリ秒から約5ミリ秒に短縮):

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 与えられた → 1 GET /sessions/<SELECTOR>. 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>
  • *inverse Inverse シリーズのすべてのデバイスに適合します(inverse3, inverse3x, minverse) — チュートリアルは、具体的なモデルが何であれ、変更なしで動作します。
  • 0 は、そのファミリーに対する0を基点とするインデックスです。このチュートリアルでは、最初のInverseについてのみ扱っています。

リターゲティングは、文字列を1か所変更するだけです:

/verse_grip/0/config/basis?session=... # target first wired VerseGrip
/*verse_grip/*/config/basis?session=... # target every grip, wired + wireless
/inverse3/A14/config/mount?session=... # target Inverse3 with id A14

参照 セレクタ — デバイスセレクタ 文法の詳細については、こちらをご覧ください。ハードコーディングの代わりにデバイス選択メニューを作成するには、 GET /devices?session=<selector> (チュートリアル 00) そして、選択した device_id 設定パスに追加します。

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

3つのキー、リクエストの形式は同じだが、ボディの構成が異なる。すべてのPOSTリクエストに対して、 200 その結果得られた値を dataあるいは 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リクエストは独立しており、サーバー側で競合を解決します。WebSocket側における同様のルールについては、チュートリアル07を参照してください。

DELETE reset — 3回の呼び出し

reset 設定キーごとに1つのDELETEを実行します。それぞれが 200 現在のデフォルト値で data.

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 固有 (q = q_z * q_y * q_x): まずX軸を中心にピッチを適用し、次にY軸を中心にロールを適用し、最後にZ軸を中心にヨーを適用します。

このチュートリアルでは、各ステータス行に導出されたクォータニオンとオイラー3要素を併記して表示するため、デバイスが回転する前に合成を確認することができます。ローカルなオイラー状態は、 (0, 0, 0) セッションにすでに何があるかに関わらず――最初の mount POST そこにあったものを上書きします。

入力モデル(概要)

重要なのはHTTPの配線であり、キーボードのUXは二の次です。意図的に採用した2つの近道:

  • Pythonkeyboard パッケージ — クロスプラットフォーム対応、キー長押しによるリピート機能をネイティブにサポート。矢印キー、 Page Up / Page Downそして = / - 押したままマウント軸をステップさせる; B そして P サイクル単位で、立ち上がりエッジでプリセットされます。
  • C++ 用途 std::getline(std::cin, ...) およびコンパクトなトークン文法(x+20, sx-5, u+10) — 継続的な調整にはあまり適していないが、持ち運びには便利だ #ifdef各プラットフォーム向けのコンソールAPI。

出典

SDKインストーラーに同梱

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

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