Meta Quest 3 VR Teleop 설정
기본 Quest 3에서 실시간 로봇 팔 원격 조작까지 단계별 경로를 완료하세요. 네트워크 설정, Unity 구성, Python UDP 서버, Piper_controller.py 및 모든 arm의 어댑터 패턴을 다룹니다.
네트워크 및 전제 조건
코드를 작성하기 전에 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 네트워크가 클라이언트를 서로 격리한다는 것입니다. 로봇 작업에는 전용 액세스 포인트를 사용합니다.
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 최소값을 유지하세요. |
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)))
QUEUE_MAXSIZE 1이면 최소 대기 시간을 제공하지만 불안정하게 느껴질 수 있습니다. 3~5의 값은 패킷 손실에 대한 동작을 부드럽게 하지만 30Hz에서 최대 100ms의 추가 대기 시간을 추가합니다. 3부터 시작하여 취향에 맞게 조정하세요.
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: 통과하다
piper.GetPiperFirmwareVersion() 동작을 명령하기 전에 AgileX 개발자 문서를 확인하세요.
안전 검증 및 첫 번째 세션
테스트 실행 체크리스트(전원 끄기):
- Python 서버를 시작하고 Quest 3을 연결합니다.
transform_position터미널에 출력됩니다. - 의도한 작업 공간의 중앙에 손을 잡으세요. 인쇄된 XYZ 값이 로봇의 홈 위치(Piper의 경우 약 0, 0, 300mm) 근처에 있는지 확인합니다.
- 각 작업 공간 가장자리로 손을 이동합니다. 값이 고정된 범위 내에 있는지 확인하고 Z에서 음수가 되지 않도록 하세요.
- 소프트웨어 비상 정지를 실행하려면 Quest의 메뉴 버튼을 누르세요. 확인하다
emergency_stop()호출되고 루프가 중단됩니다.
첫 번째 라이브 세션:
- 시작 시간
SPEED_PERCENT = 20— 이는 약 40°/s의 최대 관절 속도입니다. - 서보 전원을 활성화합니다. 처음 1분 동안 팔의 원래 위치 근처에 머물면서 천천히 움직입니다.
- 손의 움직임과 로봇의 움직임이 같은 방향인지 확인합니다. 손목이 뒤로 회전하는 경우 조정합니다.
rotationOffsetUnity Inspector의 Y에서 ±90°만큼. - 방향 매핑이 올바른 것으로 확인되면 동작 범위를 점차적으로 확장합니다.
- 물리적 비상 정지 장치(전원 릴레이)를 항상 손이 닿는 곳에 두십시오.
두 가지 소프트웨어 비상 정지 경로가 구현됩니다.
- 퀘스트 3 메뉴 버튼: 모든 후속 UDP 패킷에서 플래그 바이트의 비트 1을 설정합니다. Python 서버 호출
robot.emergency_stop()즉시. - Ctrl-C(SIGINT): 신호 처리기는 제어 루프가 호출되도록 하는 종료 이벤트를 설정합니다.
emergency_stop()그리고 깔끔하게 퇴장하세요.
valid UDP 패킷의 플래그는 0으로 떨어집니다. Python 서버는 팔을 위치 0으로 맞추는 대신 마지막으로 알려진 위치를 유지합니다.
어댑터 인터페이스 — 모든 암에 대한 포트
컨트롤러 교체 패턴은 Python SDK를 사용하는 모든 암에 일반화됩니다. 유일한 요구 사항은 컨트롤러 클래스가 정확히 다음 서명을 사용하여 다음 다섯 가지 메서드를 구현한다는 것입니다.
| 방법 | 서명 | 계약 |
|---|---|---|
| 연결하다() | () → 부울 | 커뮤니케이션 채널을 엽니다. 성공하면 True를 반환합니다. |
| 연결 해제() | () → 없음 | 서보 전원을 비활성화하고 포트나 소켓을 닫습니다. |
| set_pose(x, y, z, 롤, 피치, 요) | (float×6) → 없음 | mm + 도 단위의 데카르트 엔드 이펙터 목표; 내부적으로 고정해야 함 |
| set_gripper(값) | (부동) → 없음 | 정규화된 개방성 0.0–1.0; 내부적으로 팔별 단위에 매핑 |
| 비상 정지() | () → 없음 | 언제든지 어떤 스레드에서든 안전하게 호출할 수 있어야 합니다. |
새 팔을 추가하는 단계:
- 쓰다
myarm_controller.py팔의 SDK를 사용하여 위의 다섯 가지 방법을 구현합니다. 작업 공간 경계와 속도 제한을 모듈 수준 상수로 하드코드합니다. - 격리된 단위 테스트: 호출
connect(), 그 다음에set_pose집에서 5cm 떨어진 안전한 위치에 있습니다. 팔이 움직이고 예상 위치로 돌아가는지 확인합니다. - 가져오기를 다음으로 교체하세요.
teleoperation_main.py: 바꾸다from piper_controller import PiperController새 컨트롤러로. 다른 변경은 필요하지 않습니다. - Unity Inspector 매개변수를 보정합니다(
positionOffset,rotationOffset,scaleFactor) 새 팔의 작업 공간입니다. - 운영자 세션 전에 비상 정지 동작, 추적 손실 유지 및 작업 공간 클램프를 검증하십시오.
geometry_msgs/PoseStamped 내부 주제 set_pose — 나머지 아키텍처는 동일하게 유지됩니다. 큐 기반 설계는 ROS 2 pub/sub 주기의 대기 시간을 자연스럽게 흡수합니다.