メインコンテンツへスキップ

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

別の場所(たとえば、別のアプリ、Unityシーン、Haply Hubのデモなど)で実行中のセッションを再配置するには、そのデバイスに対してHTTP REST呼び出しを実行します。このチュートリアルではWebSocketを開くことはありません。GET、POST、DELETEリクエストを使用するだけで、別のアプリで触覚フィードバックのレンダリングを継続しながら、basis、ワークスペースプリセット、またはマウント変換を変更することができます。

利用シーン​

  • 実行中のデモをリアルタイムで微調整します。「Haply 」HubOrbデモを起動し、別のターミナルでこのチュートリアルを実行すると、basis 軸の置換を切り替えたり、ワークスペースのプリセットを変更したり、mount 変換を微調整したりできます。Orb の座標系は即座に変化しますが、デモを停止する必要はありません。
  • ユーザーごとにワークスペースのキャリブレーションを行います。 タッチスクリーンシーンをホスト上で実行し続け、同一ネットワーク内のオペレーターがプッシュできるようにする mount のオフセット/回転/拡大縮小を行い、仮想作業スペースをユーザーのデスクに合わせます。
  • デバイス選択機能付きのオプションメニュー。 同じ HTTP ヘルパー関数を呼び出すことができます GET /devices(参照 チュートリアル 00)デバイス一覧を表示し、インタラクティブなメニューを構築します。デバイスを1つ選択すると、そのデバイスの設定を変更できます。セッション内のWebSocketにアクセスする必要はありません。このチュートリアルでは、 /sessions さらに、ハードコーディングされており、 *inverse/0,しかし、代わりに /devices ドライバのセレクタは、単なるローカルな変更に過ぎません。
  • スクリプト化による再構成。セッションの録画開始前に、事前チェック手順(basisの設定+プリセット+マウント)を自動的に実行し、各クライアントにこれらの設定を個別に組み込む必要をなくします。

前提条件​

チュートリアル 08:実行中のセッションを再構成するためのものです。アクティブなHapticセッション(別のチュートリアル、Unityシーン、またはHaply Hubのデモなど)が1つ必要です。

セッションを最も早く開始する方法

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」シーンは、デバイスのワークスペース内に球体をレンダリングします。チュートリアル 08 に従って、「basis」やプリセットを切り替えたり、「mount」の変形を微調整したりすると、「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 を出力し、その後キー入力を待機します。キーが押されるたびに、REST 呼び出しが1 回送信されます。

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

profile 名が設定されていないセッションは、数値の ID でのみ指定できます。ただし、この ID は実行のたびに変化します。メインアプリでは、最初のメッセージ内で以下を呼び出してください。 session.configure.profile.name,その後、実行のたびに安定したセレクタを再利用できるようになります。例えば、 --session :my_profile:0。参照 会話 — プロフィール名。

キーの割り当て​

ボタン操作
B循環切り替え、basis置換
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​

このチュートリアルでは、HTTP の 3 種類の動詞を使用しており、これら 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 の使用 requests.Session() HTTP 長接続の再利用を実現(1回のリクエストの遅延を約 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 → 1回送信する GET /sessions/<SELECTOR>。200 → そのまま使用;404 → エラーが発生し、終了しました。
  • 指定されていない引数 → GET /sessions(リスト)→ プロファイル名付きのセッションを表示 → インデックスの入力を促す → 最終的なセレクタを構築(利用可能な場合は優先して使用) :profile:0,そうでない場合は、以下にフォールバックします。 #id)。

SELECTOR 支持 セレクタ — セッションセレクタ ここで定義されたすべての形式::profile:instance、#id、:-1、:0、純粋なプロファイル名、または次のような形式の co.haply.hub::*:0 の profile 名称ワイルドカード パターン。このチュートリアルでは、この文字列をそのまま転送し、解析はサーバー側で行います。

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つの文字列を変更するだけで済みます — 渡す --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 個」と表示左手 「inverse」であり、「0番目の inverse(できれば左側)」ではない。参照: セレクタ――クラスフィルターはインデックス作成前に解析される。

チュートリアル 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 配置 — basis、preset、mount​

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 排他

そのうちの1つに対してPOSTを実行すると、デバイス上でもう1つがクリアされます。このチュートリアルでは、この点については明示的に追跡していません。各POSTは独立しており、競合の解決はサーバー側が行います。同じルールについてWebSocket側の観点から説明した内容については、チュートリアル07を参照してください。

DELETE リセット — 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の接続ロジックであり、キーボード操作はあくまで副次的なものです。ここには、意図的に簡略化された点が2つあります:

  • Python 使用 keyboard 包——クロスプラットフォーム対応、長押しによる繰り返し操作をネイティブで処理。方向キー、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 版)