メタクエスト 3 VR テレオプのセットアップ
素の Quest 3 から実際のロボット アームの遠隔操作までの完全なステップバイステップ パス。 ネットワーク設定、Unity 構成、Python UDP サーバー、piper_controller.py、および任意のアームのアダプター パターンをカバーします。
ネットワークと前提条件
コードを記述する前に、Meta Quest 3 と制御 PC が同じローカル ネットワーク上で相互に接続できることを確認してください。 システムは UDP を使用します。両方のデバイスが同じサブネットを共有し、PC ファイアウォールで UDP ポート 8888 と 8889 が開いている必要があります。
要件チェックリスト:
- ハンドトラッキングが有効になっている Meta Quest 3 ヘッドセット (設定 → 動きのトラッキング → ハンド トラッキング)
- Wi-Fi 6 アクセス ポイント - UDP 転送遅延を 10 ミリ秒未満に抑えることを強く推奨します
- Linux (Ubuntu 22.04+) または Python 3.10+ を搭載した macOS を実行している PC を制御する
- AgileX Piper の場合: USB-to-CAN アダプター (Kvaser または PEAK など)、アームに接続された CAN ケーブル
- UDP ポート 8888 および 8889 が PC ファイアウォールで受信側を開きます
接続を確認します。
# On the control PC — find your IP address ip addr show # Linux ipconfig getifaddr en0 # macOS (Wi-Fi) # Quick UDP listener test (run on PC, send any packet from Quest) python3 -c "インポートソケット; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.bind(('0.0.0.0',8888)); print('Listening...'); print(s.recvfrom(256))"
ip addr PC 上で最初の 3 オクテットが Quest 3 の IP と一致していることを確認します (Quest の設定 → Wi-Fi → 歯車アイコンで表示されます)。 一般的な問題は、企業の Wi-Fi ネットワークがクライアントを相互に分離していることです。ロボットの作業には専用のアクセス ポイントが使用されています。
Unity アプリの構成
Quest 3 で実行される Unity アプリは、ハンド トラッキングに XR Hands パッケージを使用して、Unity 2022.3 LTS 以降で構築されています。 遠隔操作側は 3 つのスクリプトで処理されます。
- VRHandPoseSender.cs — XR Hands サブシステムから手のポーズを読み取り、45 バイトのバイナリ パケットにシリアル化し、UDP 経由で送信します
- VRGripperController.cs — ピンチの強さを正規化されたグリッパー値 [0, 1] にマッピングします。
- VRTeleoperationManager.cs — ライフサイクル管理、接続ステータス UI、自動再接続
あなたがやる ない ロボット アームを切り替えるには Unity アプリを再コンパイルする必要があります。 次のフィールドをシリアル化されたインスペクター フィールドとして公開し、Unity エディターまたは構成ファイル経由で調整します。
| インスペクターフィールド | AgileX Piper の開始値 | 注意事項 |
|---|---|---|
| ターゲットIP | PCのIPアドレス | 走る ip addr PCで見つけてください |
| 位置オフセット (m) | (0, 0, 0.3) | 仮想ロボットの原点を移動します。 パイパーのリーチはxArm6より短い |
| 回転オフセット (度) | (0, 90, 0) | Piper CAN フレームの 90° Y 補正。 マウントの向きごとに調整する |
| スケールファクター | 0.75 | Piper の作業スペースに合わせて手の動作範囲を縮小します (到達距離約 600 mm) |
| 作業スペース X (mm) | ±400 | 物理的制限内に 50 mm のマージンを残してください |
| 作業スペース Z (mm) | 50 – 700 | 衝突を避けるために、Z 分をテーブル表面の上に維持してください |
Python UDP サーバーのセットアップ
Python サーバーは 3 つの同時スレッドを実行します。 レシーバースレッド 生の UDP データグラムを読み取ります。 安全糸 パケットを検証してキューに入れます。 ロボット制御スレッド これによりキューが空になり、ロボット コントローラーが呼び出されます。 この分離により、遅いロボット SDK 呼び出しによって UDP 受信がブロックされることがなくなります。
依存関係をインストールします。
pip install python-can piper_sdk # Activate the CAN interface (run once per boot, requires root or dialout group) bash can_activate.sh can0 1000000 # Verify the interface is UP ifconfig can0
遠隔操作サーバーを実行します。
python3 teleoperation_main.py
サーバーがバインドするのは、 0.0.0.0:8888 (右手) およびオプションで 0.0.0.0:8889 (左手)。 パケットが到着すると、 valid フラグが設定されると、制御スレッドが呼び出します robot.set_pose() そして robot.set_gripper()。 プレス Ctrl+C 緊急停止とクリーンシャットダウンをトリガーします。
3 つのスレッドすべてがどのように相互作用するかを示す簡略化されたサーバー構造:
# teleoperation_main.py (simplified structure) 輸入 socket, struct, queue, threading, signal, time から piper_controller 輸入 PiperController # swap this to change arms HOST = "0.0.0.0" RIGHT_PORT = 8888 LEFT_PORT = 8889 QUEUE_MAXSIZE = 3 # drop stale frames if robot is slow CONTROL_HZ = 30 # robot command rate pose_queue = queue.Queue(maxsize=QUEUE_MAXSIZE) shutdown_event = threading.Event() 確かに udp_receiver(port: int): """スレッド 1 — 生の UDP データグラムを受信します。""" sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((HOST, port)) sock.settimeout(1.0) そうでない間 shutdown_event.is_set(): 試す: data, _ = sock.recvfrom(256) pose = parse_packet(data) もし pose そして pose["有効"]: 試す: pose_queue.put_nowait(pose) を除外する queue.Full: pose_queue.get_nowait() # drop oldest frame pose_queue.put_nowait(pose) を除外する socket.timeout: 続く sock.close() 確かに ロボット制御ループ(robot: PiperController): """スレッド 3 — CONTROL_HZ のキューとコマンド ロボットを排出します。""" period = 1.0 / CONTROL_HZ last_pose = None そうでない間 shutdown_event.is_set(): t0 = time.monotonic() 試す: pose = pose_queue.get(timeout=0.1) もし pose["停止"]: robot.emergency_stop() 続く もし pose["有効"]: last_pose = pose x, y, z = transform_position(pose["位置"]) roll, pitch, yaw = quat_to_euler(pose["回転"]) robot.set_pose(x, y, z, roll, pitch, yaw) robot.set_gripper(pose[「グリッパー」]) # tracking lost — hold last known position (do not send zero) を除外する queue.Empty: 合格 time.sleep(max(0, period - (time.monotonic() - t0)))
QUEUE_MAXSIZE 1 に設定すると遅延は最小限になりますが、ぎくしゃくした感じがすることがあります。 値を 3 ~ 5 にすると、パケット損失による動きが滑らかになりますが、30 Hz で最大 100 ミリ秒の余分な遅延が追加されます。 3から始めて好みに合わせて調整してください。
Piper_controller.py チュートリアル
の piper_controller.py モジュールは AgileX をラップします piper_sdk Python ライブラリ。 が期待する 5 つのメソッド インターフェイスを実装します。 teleoperation_main.py。 主要な設計上の決定事項:
- USB経由のCAN: Piper アームは CAN バス経由で通信します。 SDK は CAN インターフェイス名 (
can0) 接続する前にインターフェイスをアクティブにする必要があります。 - スレーブモード: 電話をかける
MasterSlaveConfig(0xFC, 0, 0, 0)アームをスレーブ モードにして、ストリーミング位置コマンドを受け入れるようにします。 - ワークスペースのクランプ: 位置は、すべての SDK 呼び出しの前にファイルの先頭にある定数にハードクランプされます。これは、ファームウェアの前にあるソフトウェア防御の最後の線です。
- SDK ユニット: 位置はマイクロメートル (整数) 単位、方向はミリ度単位です — に渡す前に、float mm/deg 値に 1000 を乗算します。
EndEffectorCtrl.
# piper_controller.py から piper_sdk 輸入 C_PiperInterface 輸入 math, logging logger = logging.getLogger(__name__) # Piper workspace limits (millimetres) — keep 50 mm inside physical limits X_MIN, X_MAX = -400, 400 Y_MIN, Y_MAX = -400, 400 Z_MIN, Z_MAX = 50, 700 # Maximum joint speed (0–100 %; keep conservative for teleop) SPEED_PERCENT = 25 クラス パイパーコントローラー: """XArmController のドロップイン置換 — 同じ 5 つのメソッド インターフェイス。""" 確かに __初期化__(self, can_interface: str = 「できる0」): self.can_interface = can_interface self._piper: C_PiperInterface | None = None self.connected = False 確かに 接続する(self) -> bool: """CAN ポートを開いてスレーブ モードを有効にしてください。""" 試す: self._piper = C_PiperInterface( can_name=self.can_interface, judge_flag=False, # allow 3rd-party CAN adapters can_auto_init=True, dh_is_offset=1, # firmware >= V1.6-3 ) self._piper.ConnectPort() self._piper.MasterSlaveConfig(0xFC, 0, 0, 0) # enter slave mode self.connected = True logger.info(「パイパーは %s に接続しました」, self.can_interface) 戻る True を除外する Exception として e: logger.error(「Piper 接続に失敗しました: %s」, e) 戻る False 確かに 切断する(self): """サーボを無効にしてポートを閉じます。""" もし self._piper: 試す: self._piper.DisableArm(7) # disable all joints を除外する Exception: 合格 self.connected = False 確かに セットポーズ(self, x: float, y: float, z: float, roll: float, pitch: float, yaw: float): """向き (ロール、ピッチ、ヨー) 度でエンドエフェクタを (x,y,z) mm に移動します。""" そうでない場合 self.connected: 戻る x = max(X_MIN, min(X_MAX, x)) y = max(Y_MIN, min(Y_MAX, y)) z = max(Z_MIN, min(Z_MAX, z)) 試す: self._piper.EndEffectorCtrl( 整数(x * 1000), 整数(y * 1000), 整数(z * 1000), 整数(roll * 1000), 整数(pitch * 1000), 整数(yaw * 1000), SPEED_PERCENT, ) を除外する Exception として e: logger.warning(「set_pose エラー: %s」, e) 確かに セットグリッパー(self, value: float): """グリッパーの開度を設定します。0.0 = 完全に閉じ、1.0 = 完全に開きます。""" そうでない場合 self.connected: 戻る GRIPPER_MAX_UM = 70_000 # 70 mm max opening in µm target_um = 整数(value * GRIPPER_MAX_UM) 試す: self._piper.GripperCtrl(target_um, SPEED_PERCENT, 0x01, 0) を除外する Exception として e: logger.warning(「set_gripper エラー: %s」, e) 確かに 緊急停止(self): """すべてのジョイントを直ちに無効にします。どのスレッドからでも安全に呼び出すことができます。""" もし self._piper: 試す: self._piper.DisableArm(7) を除外する Exception: 合格
piper.GetPiperFirmwareVersion() モーションを命令する前に、AgileX 開発者ドキュメントと照合して確認してください。
安全性検証と最初のセッション
予行チェックリスト (電源オフ):
- Python サーバーを起動し、Quest 3 に接続します。
transform_position端末に出力されます。 - 目的の作業スペースの中心に手を置きます。 印刷された XYZ 値がロボットのホーム ポジション (Piper の場合は約 0、0、300 mm) に近いことを確認します。
- 手をワークスペースの各端に移動します。 値がクランプされた範囲内に留まり、Z が負にならないことを確認します。
- Quest のメニュー ボタンを押して、ソフトウェア非常停止をトリガーします。 確認する
emergency_stop()が呼び出され、ループが停止します。
最初のライブセッション:
- から開始
SPEED_PERCENT = 20— これはおよそ 40°/s の最大ジョイント速度です。 - サーボ電源を有効にします。 最初の 1 分間は腕のホームポジションの近くに留まり、ゆっくりと移動します。
- ハンドの動きとロボットの動きが同じ方向であることを確認します。 手首が逆回転する場合は調整してください
rotationOffsetUnity インスペクターの Y で ±90° ずつ調整します。 - 方向マッピングが正しいことを確認したら、動作範囲を徐々に拡大します。
- 物理的な緊急停止装置 (パワーリレー) を常に手の届くところに置いてください。
2 つのソフトウェア非常停止パスが実装されています。
- クエスト 3 メニュー ボタン: 後続のすべての UDP パケットにフラグ バイトのビット 1 を設定します。 Python サーバーが呼び出す
robot.emergency_stop()すぐに。 - Ctrl-C (SIGINT): シグナル ハンドラーはシャットダウン イベントを設定します。これにより、制御ループは次の呼び出しを実行します。
emergency_stop()そしてきれいに抜けます。
valid UDP パケット内のフラグは 0 に下がります。Python サーバーは、アームを位置 0 にスナップするのではなく、最後にわかった位置を保持します。
アダプター インターフェイス - 任意のアームへのポート
コントローラー交換パターンは、Python SDK を使用するあらゆるアームに一般化されます。 唯一の要件は、コントローラー クラスが次の 5 つのメソッドをまさにこのシグネチャで実装することです。
| 方法 | サイン | 契約 |
|---|---|---|
| 接続する() | () → ブール値 | 通信チャネルを開きます。 成功すると True を返します |
| 切断() | ()→なし | サーボ電源を無効にし、ポートまたはソケットを閉じます。 |
| set_pose(x、y、z、ロール、ピッチ、ヨー) | (フロート×6) → なし | デカルト エンドエフェクター ターゲット (mm + 度)。 内部でクランプする必要がある |
| set_gripper(値) | (フロート) → なし | 正規化された開放度 0.0 ~ 1.0。 アーム固有のユニットに内部的にマッピング |
| 緊急停止() | ()→なし | いつでもどのスレッドからでも安全に呼び出すことができる必要があります |
新しいアームを追加する手順:
- 書く
myarm_controller.pyarm の SDK を使用して上記の 5 つのメソッドを実装します。 ワークスペースの境界と速度制限をモジュールレベルの定数としてハードコードします。 - 単体テスト: 呼び出し
connect()、 それからset_pose家から5センチ離れた安全な場所で。 アームが動き、予想どおりの位置に戻ることを確認します。 - インポートを入れ替える
teleoperation_main.py: 交換するfrom piper_controller import PiperController新しいコントローラーで。 他に変更は必要ありません。 - Unity Inspector パラメータを調整します (
positionOffset,rotationOffset,scaleFactor) 新しいアームのワークスペース用。 - オペレータセッションの前に、非常停止動作、トラッキングロスホールド、およびワークスペースクランプを検証します。
geometry_msgs/PoseStamped 内部のトピック set_pose — 残りのアーキテクチャは同一のままです。 キューベースの設計により、ROS 2 パブリッシュ/サブスクライブ サイクルのレイテンシーが自然に吸収されます。