メタクエスト 3 VR テレオプのセットアップ

素の Quest 3 から実際のロボット アームの遠隔操作までの完全なステップバイステップ パス。 ネットワーク設定、Unity 構成、Python UDP サーバー、piper_controller.py、および任意のアームのアダプター パターンをカバーします。

1

ネットワークと前提条件

コードを記述する前に、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 ネットワークがクラ​​イアントを相互に分離していることです。ロボットの作業には専用のアクセス ポイントが使用されています。
2

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 分をテーブル表面の上に維持してください
最初のライブ実行の前に調整してください。 Quest 3 を目的のワークスペースの各端までゆっくりと移動し、ターミナル出力でコマンドの位置を確認します。 フルスピード ストリーミングを有効にする前に、ロボットが関節制限内に十分に収まっていることを確認してください。
3

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)))
キュー サイズは、遅延とスムーズさを制御します。 A QUEUE_MAXSIZE 1 に設定すると遅延は最小限になりますが、ぎくしゃくした感じがすることがあります。 値を 3 ~ 5 にすると、パケット損失による動きが滑らかになりますが、30 Hz で最大 100 ミリ秒の余分な遅延が追加されます。 3から始めて好みに合わせて調整してください。
4

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:
                合格
ファームウェアのバージョンについては SDK ユニットを確認してください。 Piper SDK は急速に進化しています。 一部のファームウェア バージョンでは、位置はマイクロメートル (整数) 単位です。 他の場合はミリメートル (フロート) です。 印刷する piper.GetPiperFirmwareVersion() モーションを命令する前に、AgileX 開発者ドキュメントと照合して確認してください。
5

安全性検証と最初のセッション

電源を入れる前にお読みください。 VR 遠隔操作は、手の動きに基づいてリアルタイムでロボットに命令します。 不適切に校正されたpositionOffsetまたはscaleFactorにより、アームが即座に位置に移動して、アーム自体に損傷を与えたり、近くの人に怪我をさせたりする可能性があります。 最初に必ずロボットの電源をオフにして予行運転を行ってください。

予行チェックリスト (電源オフ):

  • Python サーバーを起動し、Quest 3 に接続します。 transform_position 端末に出力されます。
  • 目的の作業スペースの中心に手を置きます。 印刷された XYZ 値がロボットのホーム ポジション (Piper の場合は約 0、0、300 mm) に近いことを確認します。
  • 手をワークスペースの各端に移動します。 値がクランプされた範囲内に留まり、Z が負にならないことを確認します。
  • Quest のメニュー ボタンを押して、ソフトウェア非常停止をトリガーします。 確認する emergency_stop() が呼び出され、ループが停止します。

最初のライブセッション:

  • から開始 SPEED_PERCENT = 20 — これはおよそ 40°/s の最大ジョイント速度です。
  • サーボ電源を有効にします。 最初の 1 分間は腕のホームポジションの近くに留まり、ゆっくりと移動します。
  • ハンドの動きとロボットの動きが同じ方向であることを確認します。 手首が逆回転する場合は調整してください rotationOffset Unity インスペクターの Y で ±90° ずつ調整します。
  • 方向マッピングが正しいことを確認したら、動作範囲を徐々に拡大します。
  • 物理的な緊急停止装置 (パワーリレー) を常に手の届くところに置いてください。

2 つのソフトウェア非常停止パスが実装されています。

  • クエスト 3 メニュー ボタン: 後続のすべての UDP パケットにフラグ バイトのビット 1 を設定します。 Python サーバーが呼び出す robot.emergency_stop() すぐに。
  • Ctrl-C (SIGINT): シグナル ハンドラーはシャットダウン イベントを設定します。これにより、制御ループは次の呼び出しを実行します。 emergency_stop() そしてきれいに抜けます。
追跡紛失は安全に処理されます。 Quest 3 がハンド トラッキングを失うと (手がカメラの FOV から離れ、指が重なり合う)、 valid UDP パケット内のフラグは 0 に下がります。Python サーバーは、アームを位置 0 にスナップするのではなく、最後にわかった位置を保持します。
6

アダプター インターフェイス - 任意のアームへのポート

コントローラー交換パターンは、Python SDK を使用するあらゆるアームに一般化されます。 唯一の要件は、コントローラー クラスが次の 5 つのメソッドをまさにこのシグネチャで実装することです。

方法 サイン 契約
接続する() () → ブール値 通信チャネルを開きます。 成功すると True を返します
切断() ()→なし サーボ電源を無効にし、ポートまたはソケットを閉じます。
set_pose(x、y、z、ロール、ピッチ、ヨー) (フロート×6) → なし デカルト エンドエフェクター ターゲット (mm + 度)。 内部でクランプする必要がある
set_gripper(値) (フロート) → なし 正規化された開放度 0.0 ~ 1.0。 アーム固有のユニットに内部的にマッピング
緊急停止() ()→なし いつでもどのスレッドからでも安全に呼び出すことができる必要があります

新しいアームを追加する手順:

  1. 書く myarm_controller.py arm の SDK を使用して上記の 5 つのメソッドを実装します。 ワークスペースの境界と速度制限をモジュールレベルの定数としてハードコードします。
  2. 単体テスト: 呼び出し connect()、 それから set_pose 家から5センチ離れた安全な場所で。 アームが動き、予想どおりの位置に戻ることを確認します。
  3. インポートを入れ替える teleoperation_main.py: 交換する from piper_controller import PiperController 新しいコントローラーで。 他に変更は必要ありません。
  4. Unity Inspector パラメータを調整します (positionOffset, rotationOffset, scaleFactor) 新しいアームのワークスペース用。
  5. オペレータセッションの前に、非常停止動作、トラッキングロスホールド、およびワークスペースクランプを検証します。
直接 SDK の代わりに ROS 2 を使用しますか? に公開する geometry_msgs/PoseStamped 内部のトピック set_pose — 残りのアーキテクチャは同一のままです。 キューベースの設計により、ROS 2 パブリッシュ/サブスクライブ サイクルのレイテンシーが自然に吸収されます。

セットアップは完了しましたか?

UDP パケットの完全な仕様を確認するか、開発者 Wiki を読んで詳細を確認してください。