Meta Quest 3 VR Teleop 설정

기본 Quest 3에서 실시간 로봇 팔 원격 조작까지 단계별 경로를 완료하세요. 네트워크 설정, Unity 구성, Python UDP 서버, Piper_controller.py 및 모든 arm의 어댑터 패턴을 다룹니다.

1

네트워크 및 전제 조건

코드를 작성하기 전에 Meta Quest 3와 제어 PC가 동일한 로컬 네트워크를 통해 서로 연결할 수 있는지 확인하세요. 시스템은 UDP를 사용합니다. 두 장치 모두 동일한 서브넷을 공유해야 하며 UDP 포트 8888 및 8889가 PC 방화벽에 열려 있어야 합니다.

요구 사항 체크리스트:

  • 손 추적이 활성화된 Meta Quest 3 헤드셋(설정 → 움직임 추적 → 손 추적)
  • Wi-Fi 6 액세스 포인트 - UDP 전송 대기 시간을 10ms 미만으로 유지하는 것이 좋습니다.
  • Linux(Ubuntu 22.04+) 또는 Python 3.10+가 설치된 macOS를 실행하는 PC 제어
  • AgileX Piper의 경우: USB-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 이상으로 구축되었습니다. 세 가지 스크립트가 원격 조작 측면을 처리합니다.

  • VRHandPoseSender.cs — XR Hands 하위 시스템에서 손 자세를 읽고, 45바이트 바이너리 패킷으로 직렬화하고, UDP를 통해 보냅니다.
  • VRGripperController.cs — 핀치 강도를 정규화된 그리퍼 값 [0, 1]에 매핑합니다.
  • VRTeleOperationManager.cs — 수명주기 관리, 연결 상태 UI, 자동 재연결

당신은 ~ 아니다 로봇 팔 사이를 전환하려면 Unity 앱을 다시 컴파일해야 합니다. 다음 필드를 직렬화된 Inspector 필드로 노출하고 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 작업 공간에 맞게 손 동작 범위를 줄입니다(~600mm 도달 거리).
작업공간 X(mm) ±400 물리적 한계 내에 50mm의 여백을 두십시오.
작업공간 Z(mm) 50 – 700 충돌을 피하기 위해 테이블 ​​표면 위에서 Z 최소값을 유지하세요.
첫 번째 라이브 실행 전에 보정하십시오. Quest 3를 의도한 작업 공간의 각 가장자리로 천천히 이동하고 터미널 출력에서 ​​명령된 위치를 관찰합니다. 최고 속도 스트리밍을 활성화하기 전에 로봇이 관절 한계 내에 잘 있는지 확인하십시오.
3

Python UDP 서버 설정

Python 서버는 세 개의 동시 스레드를 실행합니다. 수신기 스레드 원시 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 비상 정지 및 완전한 종료를 실행합니다.

세 스레드가 모두 상호 작용하는 방식을 보여주는 단순화된 서버 구조:

# 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_수신기(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의 값은 패킷 손실에 대한 동작을 부드럽게 하지만 30Hz에서 최대 100ms의 추가 대기 시간을 추가합니다. 3부터 시작하여 취향에 맞게 조정하세요.
4

Piper_controller.py 연습

그만큼 piper_controller.py 모듈은 AgileX를 래핑합니다. piper_sdk 파이썬 라이브러리. 기대되는 5가지 메소드 인터페이스를 구현합니다. teleoperation_main.py. 주요 설계 결정:

  • USB를 통해 가능: 파이퍼 암은 CAN 버스를 통해 통신합니다. SDK는 CAN 인터페이스 이름(can0) 연결하기 전에 인터페이스를 활성화해야 합니다.
  • 슬레이브 모드: 부름 MasterSlaveConfig(0xFC, 0, 0, 0) 팔을 슬레이브 모드로 전환하여 스트리밍 위치 명령을 받아들입니다.
  • 작업 공간 클램핑: 위치는 모든 SDK 호출 전에 파일 상단에 있는 상수로 하드 고정됩니다. 이는 펌웨어 이전의 소프트웨어 방어의 마지막 라인입니다.
  • SDK 단위: 위치는 마이크로미터(정수) 단위이고 방향은 밀리도 단위입니다. 전달하기 전에 부동 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("파이퍼 연결 실패: %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, 300mm) 근처에 있는지 확인합니다.
  • 각 작업 공간 가장자리로 손을 이동합니다. 값이 고정된 범위 내에 있는지 확인하고 Z에서 음수가 되지 않도록 하세요.
  • 소프트웨어 비상 정지를 실행하려면 Quest의 메뉴 버튼을 누르세요. 확인하다 emergency_stop() 호출되고 루프가 중단됩니다.

첫 번째 라이브 세션:

  • 시작 시간 SPEED_PERCENT = 20 — 이는 약 40°/s의 최대 관절 속도입니다.
  • 서보 전원을 활성화합니다. 처음 1분 ​​동안 팔의 원래 위치 근처에 머물면서 천천히 움직입니다.
  • 손의 움직임과 로봇의 움직임이 같은 방향인지 확인합니다. 손목이 뒤로 회전하는 경우 조정합니다. rotationOffset Unity Inspector의 Y에서 ±90°만큼.
  • 방향 매핑이 올바른 것으로 확인되면 동작 범위를 점차적으로 확장합니다.
  • 물리적 비상 정지 장치(전원 릴레이)를 항상 손이 닿는 곳에 두십시오.

두 가지 소프트웨어 비상 정지 경로가 구현됩니다.

  • 퀘스트 3 메뉴 버튼: 모든 후속 UDP 패킷에서 플래그 바이트의 비트 1을 설정합니다. Python 서버 호출 robot.emergency_stop() 즉시.
  • Ctrl-C(SIGINT): 신호 처리기는 제어 루프가 호출되도록 하는 종료 이벤트를 설정합니다. emergency_stop() 그리고 깔끔하게 퇴장하세요.
추적손실은 안전하게 처리됩니다. Quest 3가 손 추적을 잃으면(손이 카메라 FOV를 벗어나고 손가락이 겹침) valid UDP 패킷의 플래그는 0으로 떨어집니다. Python 서버는 팔을 위치 0으로 맞추는 대신 마지막으로 알려진 위치를 유지합니다.
6

어댑터 인터페이스 — 모든 암에 대한 포트

컨트롤러 교체 패턴은 Python SDK를 사용하는 모든 암에 일반화됩니다. 유일한 요구 사항은 컨트롤러 클래스가 정확히 다음 서명을 사용하여 다음 다섯 가지 메서드를 구현한다는 것입니다.

방법 서명 계약
연결하다() () → 부울 커뮤니케이션 채널을 엽니다. 성공하면 True를 반환합니다.
연결 해제() () → 없음 서보 전원을 비활성화하고 포트나 소켓을 닫습니다.
set_pose(x, y, z, 롤, 피치, 요) (float×6) → 없음 mm + 도 단위의 데카르트 엔드 이펙터 목표; 내부적으로 고정해야 함
set_gripper(값) (부동) → 없음 정규화된 개방성 0.0–1.0; 내부적으로 팔별 단위에 매핑
비상 정지() () → 없음 언제든지 어떤 스레드에서든 안전하게 호출할 수 있어야 합니다.

새 팔을 추가하는 단계:

  1. 쓰다 myarm_controller.py 팔의 SDK를 사용하여 위의 다섯 가지 방법을 구현합니다. 작업 공간 경계와 속도 제한을 모듈 수준 상수로 하드코드합니다.
  2. 격리된 단위 테스트: 호출 connect(), 그 다음에 set_pose 집에서 5cm 떨어진 안전한 위치에 있습니다. 팔이 움직이고 예상 위치로 돌아가는지 확인합니다.
  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 pub/sub 주기의 대기 시간을 자연스럽게 흡수합니다.

설정이 완료되었나요?

더 자세한 내용은 전체 UDP 패킷 사양을 확인하거나 전체 개발자 위키를 읽어보세요.