Configuration de Meta Quest 3 VR Teleop
Suivez le cheminement étape par étape depuis un Quest 3 nu jusqu'à la téléopération du bras robotique en direct. Couvre la configuration du réseau, la configuration Unity, le serveur Python UDP, piper_controller.py et le modèle d'adaptateur pour n'importe quel bras.
Réseau et prérequis
Avant d'écrire un code, vérifiez que le Meta Quest 3 et le PC de contrôle peuvent se joindre sur le même réseau local. Le système utilise UDP : les deux appareils doivent partager le même sous-réseau et les ports UDP 8888 et 8889 doivent être ouverts dans le pare-feu du PC.
Liste de contrôle des exigences :
- Casque Meta Quest 3 avec suivi manuel activé (Paramètres → Suivi des mouvements → Suivi manuel)
- Point d'accès Wi-Fi 6 — fortement recommandé pour maintenir la latence de transit UDP inférieure à 10 ms
- Contrôler un PC exécutant Linux (Ubuntu 22.04+) ou macOS avec Python 3.10+
- Pour AgileX Piper : adaptateur USB vers CAN (par exemple Kvaser ou PEAK), câble CAN connecté au bras
- Les ports UDP 8888 et 8889 s'ouvrent en entrant sur le pare-feu du PC
Vérifiez la connectivité :
# 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 "importer socket; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.bind(('0.0.0.0',8888)); print('Listening...'); print(s.recvfrom(256))"
ip addr sur le PC et vérifiez que les trois premiers octets correspondent à l'adresse IP du Quest 3 (visible dans Paramètres du Quest → Wi-Fi → icône d'engrenage). Un problème courant est que les réseaux Wi-Fi d'entreprise isolent les clients les uns des autres : utilisez un point d'accès dédié pour le travail robotique.
Configuration de l'application Unity
L'application Unity exécutée sur Quest 3 est construite avec Unity 2022.3 LTS ou version ultérieure, en utilisant le package XR Hands pour le suivi manuel. Trois scripts gèrent le côté téléopération :
- VRHandPoseSender.cs - lit la pose de la main à partir du sous-système XR Hands, sérialise en un paquet binaire de 45 octets, envoie via UDP
- VRGripperController.cs — mappe la force de pincement à une valeur de préhension normalisée [0, 1]
- VRTeleoperationManager.cs — gestion du cycle de vie, interface utilisateur de l'état de la connexion, reconnexion automatique
Tu fais pas Vous devez recompiler l'application Unity pour basculer entre les bras du robot. Exposez les champs suivants en tant que champs d'inspecteur sérialisés et ajustez-les à partir de l'éditeur Unity ou via un fichier de configuration :
| Champ d'inspecteur | Valeur de départ AgileX Piper | Remarques |
|---|---|---|
| Câble IP | L'adresse IP de votre PC | Courir ip addr sur le PC pour le trouver |
| positionDécalage (m) | (0, 0, 0.3) | Déplace l'origine du robot virtuel ; La portée du Piper est plus courte que celle du xArm6 |
| rotationDécalage (degrés) | (0, 90, 0) | Correction Y de 90° pour la trame Piper CAN ; ajuster selon l'orientation du support |
| facteur d'échelle | 0.75 | Réduit la plage de mouvement de la main pour s'adapter à l'espace de travail Piper (portée d'environ 600 mm) |
| Espace de travail X (mm) | ±400 | Laisser une marge de 50 mm à l’intérieur des limites physiques |
| Espace de travail Z (mm) | 50 – 700 | Gardez Z min au-dessus de la surface de la table pour éviter les collisions |
Configuration du serveur Python UDP
Le serveur Python exécute trois threads simultanés : un fil de réception qui lit les datagrammes UDP bruts, un fil de sécurité qui valide les paquets et les met en file d'attente, et un fil de commande du robot qui vide la file d'attente et appelle le contrôleur du robot. Cette séparation garantit que les appels lents du SDK du robot ne bloquent jamais la réception UDP.
Installer les dépendances :
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
Exécutez le serveur de téléopération :
python3 teleoperation_main.py
Le serveur se lie à 0.0.0.0:8888 (main droite) et en option 0.0.0.0:8889 (main gauche). Lorsqu'un paquet arrive avec le valid indicateur défini, le thread de contrôle appelle robot.set_pose() et robot.set_gripper(). Presse Ctrl-C pour déclencher un arrêt d'urgence et un arrêt propre.
La structure de serveur simplifiée montrant comment les trois threads interagissent :
# teleoperation_main.py (simplified structure) importateur socket, struct, queue, threading, signal, time depuis piper_controller importateur 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() déf udp_receiver(port: int): """Thread 1 — recevoir des datagrammes UDP bruts.""" sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((HOST, port)) sock.settimeout(1.0) alors que non shutdown_event.is_set(): essayer: data, _ = sock.recvfrom(256) pose = parse_packet(data) et pose et pose["valide"]: essayer: pose_queue.put_nowait(pose) sauf queue.Full: pose_queue.get_nowait() # drop oldest frame pose_queue.put_nowait(pose) sauf socket.timeout: continuer sock.close() déf robot_control_loop(robot: PiperController): """Thread 3 : vidange de la file d'attente et robot de commande à CONTROL_HZ.""" period = 1.0 / CONTROL_HZ last_pose = None alors que non shutdown_event.is_set(): t0 = time.monotonic() essayer: pose = pose_queue.get(timeout=0.1) et pose["arrêt"]: robot.emergency_stop() continuer et pose["valide"]: last_pose = pose x, y, z = transform_position(pose["position"]) roll, pitch, yaw = quat_to_euler(pose["rotation"]) robot.set_pose(x, y, z, roll, pitch, yaw) robot.set_gripper(pose["pince"]) # tracking lost — hold last known position (do not send zero) sauf queue.Empty: passeuse time.sleep(max(0, period - (time.monotonic() - t0)))
QUEUE_MAXSIZE de 1 donne une latence minimale mais peut sembler saccadé. Une valeur de 3 à 5 adoucit le mouvement en cas de perte de paquets, mais ajoute jusqu'à 100 ms de latence supplémentaire à 30 Hz. Commencez à 3 heures et réglez selon vos goûts.
piper_controller.py Procédure pas à pas
Le piper_controller.py le module enveloppe l'AgileX piper_sdk Bibliothèque Python. Il implémente l'interface à cinq méthodes attendue par teleoperation_main.py. Décisions de conception clés :
- PEUT via USB : Le bras Piper communique via le bus CAN. Le SDK prend un nom d'interface CAN (
can0) et nécessite que l'interface soit activée avant de se connecter. - Mode esclave : Appel
MasterSlaveConfig(0xFC, 0, 0, 0)met le bras en mode esclave afin qu'il accepte les commandes de position en streaming. - Serrage de l'espace de travail : Les positions sont strictement liées aux constantes en haut du fichier avant chaque appel du SDK : il s'agit de la dernière ligne de défense logicielle avant le micrologiciel.
- Unités du SDK : La position est en micromètres (entier), l'orientation en millimètres — multipliez les valeurs flottantes mm/deg par 1 000 avant de passer à
EndEffectorCtrl.
# piper_controller.py depuis piper_sdk importateur C_PiperInterface importateur 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 classe Contrôleur de joueur de cornemuse: """Remplacement immédiat pour XArmController — même interface à cinq méthodes.""" déf __init__(self, can_interface: str = "peut0"): self.can_interface = can_interface self._piper: C_PiperInterface | None = None self.connected = False déf connecter(self) -> bool: """Ouvrez le port CAN et activez le mode esclave.""" essayer: 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 connecté sur %s", self.can_interface) retour True sauf Exception comme e: logger.error("Échec de la connexion Piper : %s", e) retour False déf déconnecter(self): """Désactiver le servo et fermer le port.""" et self._piper: essayer: self._piper.DisableArm(7) # disable all joints sauf Exception: passeuse self.connected = False déf set_pose(self, x: float, y: float, z: float, roll: float, pitch: float, yaw: float): """Déplacez l'effecteur final à (x,y,z) mm avec des degrés d'orientation (roulis, tangage, lacet).""" sinon self.connected: retour x = max(X_MIN, min(X_MAX, x)) y = max(Y_MIN, min(Y_MAX, y)) z = max(Z_MIN, min(Z_MAX, z)) essayer: self._piper.EndEffectorCtrl( int(x * 1000), int(y * 1000), int(z * 1000), int(roll * 1000), int(pitch * 1000), int(yaw * 1000), SPEED_PERCENT, ) sauf Exception comme e: logger.warning("Erreur set_pose : %s", e) déf set_gripper(self, value: float): """Définir l'ouverture de la pince. 0,0 = complètement fermé, 1,0 = complètement ouvert.""" sinon self.connected: retour GRIPPER_MAX_UM = 70_000 # 70 mm max opening in µm target_um = int(value * GRIPPER_MAX_UM) essayer: self._piper.GripperCtrl(target_um, SPEED_PERCENT, 0x01, 0) sauf Exception comme e: logger.warning("Erreur set_gripper : %s", e) déf arrêt_d'urgence(self): """Désactivez immédiatement toutes les articulations. Appelez en toute sécurité depuis n'importe quel fil de discussion.""" et self._piper: essayer: self._piper.DisableArm(7) sauf Exception: passeuse
piper.GetPiperFirmwareVersion() et vérifiez par rapport à la documentation du développeur AgileX avant de commander un mouvement.
Validation de sécurité et première session
Liste de contrôle du fonctionnement à sec (hors tension) :
- Démarrez le serveur Python et connectez Quest 3. Regardez le
transform_positionsortie imprimée sur le terminal. - Tenez votre main au centre de l'espace de travail prévu. Confirmez que les valeurs XYZ imprimées sont proches de la position d'origine du robot (environ 0, 0, 300 mm pour Piper).
- Déplacez votre main vers chaque bord de l'espace de travail. Confirmez que les valeurs restent dans les limites serrées et ne deviennent jamais négatives sur Z.
- Appuyez sur le bouton Menu du Quest pour déclencher le logiciel d'arrêt d'urgence. Confirmer
emergency_stop()est appelé et la boucle s'arrête.
Première séance en direct :
- Commencez à
SPEED_PERCENT = 20— il s'agit d'une vitesse articulaire maximale d'environ 40°/s. - Activer la puissance du servo. Déplacez-vous lentement, en restant près de la position initiale du bras pendant la première minute.
- Vérifiez que le mouvement de la main et le mouvement du robot sont dans la même direction. Si le poignet tourne vers l'arrière, ajustez
rotationOffsetde ±90° sur Y dans l’inspecteur Unity. - Étendez progressivement la plage de mouvement une fois que la cartographie de direction est confirmée correcte.
- Gardez un arrêt d'urgence physique (relais de puissance) à portée de main à tout moment.
Deux parcours logiciels d'arrêt d'urgence sont mis en œuvre :
- Bouton Menu Quête 3 : Définit le bit 1 de l’octet d’indicateurs dans chaque paquet UDP suivant. Le serveur Python appelle
robot.emergency_stop()immédiatement. - Ctrl-C (SIGINT) : Le gestionnaire de signal définit l'événement d'arrêt, ce qui provoque l'appel de la boucle de contrôle
emergency_stop()et ressortez proprement.
valid L'indicateur dans le paquet UDP tombe à 0. Le serveur Python conserve la dernière position connue plutôt que de mettre le bras en position zéro.
Interface d'adaptateur — Port vers n'importe quel bras
Le modèle d’échange de contrôleur se généralise à n’importe quelle branche disposant d’un SDK Python. La seule exigence est que votre classe de contrôleur implémente ces cinq méthodes avec exactement cette signature :
| Méthode | Signature | Contracter |
|---|---|---|
| connecter() | () → booléen | Ouvre un canal de communication ; renvoie True en cas de succès |
| déconnecter() | () → Aucun | Désactive l'alimentation du servo et ferme le port ou la prise |
| set_pose(x, y, z, roue, attelage, lacet) | (float×6) → Aucun | Cible de l'effecteur cartésien en mm + degrés ; doit serrer à l'intérieur |
| set_gripper (valeur) | (flotteur) → Aucun | Ouverture normalisée 0,0–1,0 ; mapper vers des unités spécifiques à un bras en interne |
| arrêt_d'urgence() | () → Aucun | Il doit être possible d'appeler en toute sécurité depuis n'importe quel fil de discussion à tout moment |
Étapes pour ajouter un nouveau bras :
- Écrire
myarm_controller.pyimplémentant les cinq méthodes ci-dessus à l'aide du SDK de votre bras. Coder en dur les limites de l'espace de travail et les limites de vitesse en tant que constantes au niveau du module. - Test unitaire isolé : appel
connect(), alorsset_poseavec une position sécuritaire à 5 cm de la maison. Vérifiez que le bras bouge et revient à la position attendue. - Échangez l'importation dans
teleoperation_main.py: remplacerfrom piper_controller import PiperControlleravec votre nouveau contrôleur. Aucun autre changement n'est nécessaire. - Calibrer les paramètres de Unity Inspector (
positionOffset,rotationOffset,scaleFactor) pour l'espace de travail du nouveau bras. - Validez le comportement de l'arrêt d'urgence, le maintien de la perte de suivi et les pinces de l'espace de travail avant toute session de l'opérateur.
geometry_msgs/PoseStamped sujet à l'intérieur set_pose — le reste de l'architecture reste identique. La conception basée sur la file d’attente absorbe naturellement la latence d’un cycle de pub/sub ROS 2.