Configuración de teleoperación VR del Meta Quest 3
Ruta completa paso a paso desde un Quest 3 básico hasta la teleoperación en vivo de un brazo robótico. Cubre la configuración de red, la configuración de Unity, el servidor UDP de Python, piper_controller.py y el patrón de adaptador para cualquier brazo.
Red y requisitos previos
Antes de escribir cualquier código, confirma que el Meta Quest 3 y la PC de control pueden comunicarse entre sí a través de la misma red local. El sistema utiliza UDP — ambos dispositivos deben compartir la misma subred, y los puertos UDP 8888 y 8889 deben estar abiertos en el firewall de la PC.
Lista de verificación de requisitos:
- Auriculares Meta Quest 3 con seguimiento de manos habilitado (Configuración → Seguimiento de movimiento → Seguimiento de manos)
- Punto de acceso Wi-Fi 6 — se recomienda encarecidamente para mantener la latencia de tránsito UDP por debajo de 10 ms
- PC de control que ejecute Linux (Ubuntu 22.04+) o macOS con Python 3.10+
- Para AgileX Piper: adaptador USB a CAN (por ejemplo, Kvaser o PEAK), cable CAN conectado al brazo
- Puertos UDP 8888 y 8889 abiertos para entrada en el firewall de la PC
Verifica la conectividad:
# 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 "importar socket; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.bind(('0.0.0.0',8888)); print('Escuchando...'); print(s.recvfrom(256))"
ip addr en la PC y verifica que los primeros tres octetos coincidan con la IP del Quest 3 (visible en Configuración del Quest → Wi-Fi → icono de engranaje). Un problema común es que las redes Wi-Fi corporativas aíslan a los clientes entre sí; utiliza un punto de acceso dedicado para trabajos de robótica.
Configuración de la aplicación Unity
La aplicación de Unity que se ejecuta en el Quest 3 está construida con Unity 2022.3 LTS o posterior, utilizando el paquete XR Hands para el seguimiento de manos. Tres scripts manejan el lado de la teleoperación:
- VRHandPoseSender.cs — lee la pose de la mano del subsistema XR Hands, serializa a un paquete binario de 45 bytes, envía a través de UDP
- VRGripperController.cs — mapea la fuerza de pellizco a un valor de agarre normalizado [0, 1]
- VRTeleoperationManager.cs — gestión del ciclo de vida, UI de estado de conexión, reconexión automática
Tú haces No necesitas recompilar la aplicación de Unity para cambiar entre brazos robóticos. Expón los siguientes campos como campos de Inspector serializados y ajústalos desde el Editor de Unity o a través de un archivo de configuración:
| Campo del Inspector | Valor inicial de AgileX Piper | Notas |
|---|---|---|
| IP objetivo | La dirección IP de tu PC | Ejecutar ip addr en la PC para encontrarla |
| positionOffset (m) | (0, 0, 0.3) | Desplaza el origen del robot virtual; el alcance de Piper es más corto que el xArm6 |
| rotationOffset (deg) | (0, 90, 0) | Corrección de 90° Y para el marco Piper CAN; ajustar según la orientación del soporte |
| factorDeEscala | 0.75 | Reduce el rango de movimiento de la mano para ajustarse al espacio de trabajo de Piper (~600 mm de alcance) |
| Espacio de trabajo X (mm) | ±400 | Dejar un margen de 50 mm dentro de los límites físicos |
| Espacio de trabajo Z (mm) | 50 – 700 | Mantener Z min por encima de la superficie de la mesa para evitar colisiones |
Configuración del servidor UDP de Python
El servidor de Python ejecuta tres hilos concurrentes: un hilo receptor que lee datagramas UDP en bruto, un hilo de seguridad que valida paquetes y los encola, y un hilo de control del robot que drena la cola y llama al controlador del robot. Esta separación asegura que las llamadas lentas al SDK del robot nunca bloqueen la recepción de UDP.
Instalar dependencias:
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
Ejecutar el servidor de teleoperación:
python3 teleoperation_main.py
El servidor se vincula a 0.0.0.0:8888 (mano derecha) y opcionalmente 0.0.0.0:8889 (mano izquierda). Cuando llega un paquete con el valid indicador establecido, el hilo de control llama robot.set_pose() y robot.set_gripper(). Presione Ctrl-C para activar una parada de emergencia y un apagado limpio.
La estructura simplificada del servidor que muestra cómo interactúan los tres hilos:
# teleoperation_main.py (simplified structure) importar socket, struct, queue, threading, signal, time de piper_controller importar 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() def receptor_udp(port: int): """Hilo 1 — recibir datagramas UDP en bruto.""" sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((HOST, port)) sock.settimeout(1.0) mientras no shutdown_event.is_set(): intentar: data, _ = sock.recvfrom(256) pose = parse_packet(data) y pose y pose["válido"]: intentar: pose_queue.put_nowait(pose) excepto queue.Full: pose_queue.get_nowait() # drop oldest frame pose_queue.put_nowait(pose) excepto socket.timeout: continuar sock.close() def bucle_control_robot(robot: PiperController): """Hilo 3 — drenar la cola y comandar el robot a CONTROL_HZ.""" period = 1.0 / CONTROL_HZ last_pose = None mientras no shutdown_event.is_set(): t0 = time.monotonic() intentar: pose = pose_queue.get(timeout=0.1) y pose["estop"]: robot.emergency_stop() continuar y pose["válido"]: last_pose = pose x, y, z = transform_position(pose["posición"]) roll, pitch, yaw = quat_to_euler(pose["rotación"]) robot.set_pose(x, y, z, roll, pitch, yaw) robot.set_gripper(pose["garra"]) # tracking lost — hold last known position (do not send zero) excepto queue.Empty: mercado time.sleep(max(0, period - (time.monotonic() - t0)))
QUEUE_MAXSIZE de 1 da la latencia mínima pero puede sentirse entrecortado. Un valor de 3-5 suaviza el movimiento ante la pérdida de paquetes pero añade hasta 100 ms de latencia extra a 30 Hz. Comienza en 3 y ajusta al gusto.
Guía de piper_controller.py
La piper_controller.py el módulo envuelve el AgileX piper_sdk biblioteca de Python. Implementa la interfaz de cinco métodos esperada por teleoperation_main.py. Decisiones de diseño clave:
- CAN a través de USB: El brazo Piper se comunica a través del bus CAN. El SDK toma un nombre de interfaz CAN (
can0) y requiere que la interfaz esté activada antes de conectar. - Modo esclavo: Llamando
MasterSlaveConfig(0xFC, 0, 0, 0)pone el brazo en modo esclavo para que acepte comandos de posición en streaming. - Fijación del espacio de trabajo: Las posiciones están fijadas a las constantes en la parte superior del archivo antes de cada llamada al SDK — estas son la última línea de defensa de software antes del firmware.
- unidades del SDK: La posición está en micrómetros (entero), la orientación en miligrados — multiplica los valores de mm/deg en punto flotante por 1000 antes de pasarlos a
EndEffectorCtrl.
# piper_controller.py de piper_sdk importar C_PiperInterface importar 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 clase ControladorPiper: """Reemplazo directo para ControladorXArm — misma interfaz de cinco métodos.""" def __init__(self, can_interface: str = "can0"): self.can_interface = can_interface self._piper: C_PiperInterface | None = None self.connected = False def conectar(self) -> bool: """Abrir puerto CAN y habilitar modo esclavo.""" intentar: 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("Piper conectado en %s", self.can_interface) regresar True excepto Exception como e: logger.error("Error al conectar Piper: %s", e) regresar False def desconectar(self): """Deshabilitar servo y cerrar puerto.""" y self._piper: intentar: self._piper.DisableArm(7) # disable all joints excepto Exception: mercado self.connected = False def set_pose(self, x: float, y: float, z: float, roll: float, pitch: float, yaw: float): """Mover efector final a (x,y,z) mm con orientación (rollo, inclinación, guiñada) grados.""" si no self.connected: regresar x = max(X_MIN, min(X_MAX, x)) y = max(Y_MIN, min(Y_MAX, y)) z = max(Z_MIN, min(Z_MAX, z)) intentar: self._piper.EndEffectorCtrl( int(x * 1000), int(y * 1000), int(z * 1000), int(roll * 1000), int(pitch * 1000), int(yaw * 1000), SPEED_PERCENT, ) excepto Exception como e: logger.warning("error en set_pose: %s", e) def establecer_pinza(self, value: float): """Establecer apertura de la pinza. 0.0 = completamente cerrada, 1.0 = completamente abierta.""" si no self.connected: regresar GRIPPER_MAX_UM = 70_000 # 70 mm max opening in µm target_um = int(value * GRIPPER_MAX_UM) intentar: self._piper.GripperCtrl(target_um, SPEED_PERCENT, 0x01, 0) excepto Exception como e: logger.warning("error en set_gripper: %s", e) def parada_de_emergencia(self): """Deshabilitar inmediatamente todas las articulaciones. Seguro para llamar desde cualquier hilo.""" y self._piper: intentar: self._piper.DisableArm(7) excepto Exception: mercado
piper.GetPiperFirmwareVersion() y verificar contra la documentación del desarrollador de AgileX antes de comandar cualquier movimiento.
Validación de seguridad y primera sesión
Lista de verificación de prueba en seco (energía APAGADA):
- Inicia el servidor de Python y conecta Quest 3. Observa la
transform_positionsalida impresa en la terminal. - Mantén tu mano en el centro del espacio de trabajo previsto. Confirma que los valores XYZ impresos están cerca de la posición de inicio del robot (aproximadamente 0, 0, 300 mm para Piper).
- Mueve tu mano a cada borde del espacio de trabajo. Confirma que los valores se mantengan dentro de los límites fijados y nunca sean negativos en Z.
- Presiona el botón de Menú en el Quest para activar el e-stop del software. Confirma
emergency_stop()que se llama y el bucle se detiene.
Primera sesión en vivo:
- Comienza en
SPEED_PERCENT = 20— esta es aproximadamente la velocidad máxima de la articulación de 40°/s. - Habilita la energía del servo. Mueve lentamente, manteniéndote cerca de la posición de inicio del brazo durante el primer minuto.
- Verifica que el movimiento de la mano y el movimiento del robot estén en la misma dirección. Si la muñeca rota hacia atrás, ajusta
rotationOffseten ±90° en Y en el Inspector de Unity. - Expande gradualmente el rango de movimiento una vez que se confirme que la asignación de dirección es correcta.
- Mantén un botón de parada de emergencia físico (relé de potencia) al alcance en todo momento.
Se implementan dos rutas de parada de emergencia de software:
- Botón de menú Quest 3: Establece el Bit 1 del byte de banderas en cada paquete UDP subsiguiente. El servidor de Python llama
robot.emergency_stop()inmediatamente. - Ctrl-C (SIGINT): El controlador de señales establece el evento de apagado, lo que provoca que el bucle de control llame
emergency_stop()y salga de manera limpia.
valid indicador en el paquete UDP cae a 0. El servidor de Python mantiene la última posición conocida en lugar de llevar el brazo a la posición cero.
Interfaz de adaptador — Puerto para cualquier brazo
El patrón de intercambio de controladores se generaliza a cualquier brazo con un SDK de Python. El único requisito es que tu clase de controlador implemente estos cinco métodos con exactamente esta firma:
| Método | Negocio | Contrato |
|---|---|---|
| connect() | () → bool | Abre el canal de comunicación; devuelve True en caso de éxito |
| disconnect() | () → None | Desactiva la alimentación del servo y cierra el puerto o socket |
| set_pose(x, y, z, roll, pitch, yaw) | (float×6) → Ninguno | Objetivo del efector final cartesiano en mm + grados; debe fijarse internamente |
| set_gripper(valor) | (float) → Ninguno | Apertura normalizada 0.0–1.0; mapear a unidades específicas del brazo internamente |
| emergency_stop() | () → None | Debe ser seguro llamarlo desde cualquier hilo en cualquier momento |
Pasos para agregar un nuevo brazo:
- Escribir
myarm_controller.pyimplementando los cinco métodos anteriores utilizando el SDK de su brazo. Codifique en duro los límites del espacio de trabajo y los límites de velocidad como constantes a nivel de módulo. - Prueba unitaria en aislamiento: llama
connect(), luegoset_posecon una posición segura a 5 cm de casa. Verifica que el brazo se mueva y regrese a la posición esperada. - Cambia la importación en
teleoperation_main.py: reemplazafrom piper_controller import PiperControllercon tu nuevo controlador. No se necesitan otros cambios. - Calibra los parámetros del Inspector de Unity (
positionOffset,rotationOffset,scaleFactor) para el espacio de trabajo del nuevo brazo. - Valide el comportamiento de parada de emergencia, la retención de pérdida de seguimiento y las pinzas de espacio de trabajo antes de cualquier sesión de operador.
geometry_msgs/PoseStamped tema dentro set_pose — el resto de la arquitectura permanece idéntico. El diseño basado en colas absorbe naturalmente la latencia de un ciclo de publicación/suscripción de ROS 2.