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.

1

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))"
Même sous-réseau requis. Courir 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.
2

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
Calibrez avant votre première exécution en direct. Déplacez lentement le Quest 3 vers chaque bord de l'espace de travail prévu et observez la position commandée dans la sortie de votre terminal. Confirmez que le robot reste bien dans ses limites articulaires avant d'activer la diffusion à pleine vitesse.
3

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)))
La taille de la file d’attente contrôle la latence par rapport à la fluidité. A 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.
4

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
Vérifiez les unités SDK pour la version de votre micrologiciel. Le SDK Piper évolue rapidement. Dans certaines versions de firmware, la position est en micromètres (entier) ; dans d'autres, c'est en millimètres (flotteur). Imprimer piper.GetPiperFirmwareVersion() et vérifiez par rapport à la documentation du développeur AgileX avant de commander un mouvement.
5

Validation de sécurité et première session

A lire avant de mettre sous tension. La téléopération VR commande le robot en temps réel en fonction du mouvement de votre main. Un positionOffset ou un scaleFactor mal calibré peut amener le bras à se déplacer instantanément vers une position qui s'endommage ou blesse les personnes à proximité. Effectuez toujours un fonctionnement à sec avec le robot hors tension en premier.

Liste de contrôle du fonctionnement à sec (hors tension) :

  • Démarrez le serveur Python et connectez Quest 3. Regardez le transform_position sortie 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 rotationOffset de ±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.
La perte de suivi est gérée en toute sécurité. Lorsque le Quest 3 perd le suivi de la main (la main quitte le champ de vision de la caméra, les doigts se chevauchent), le 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.
6

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 :

  1. Écrire myarm_controller.py implé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.
  2. Test unitaire isolé : appel connect(), alors set_pose avec une position sécuritaire à 5 cm de la maison. Vérifiez que le bras bouge et revient à la position attendue.
  3. Échangez l'importation dans teleoperation_main.py: remplacer from piper_controller import PiperController avec votre nouveau contrôleur. Aucun autre changement n'est nécessaire.
  4. Calibrer les paramètres de Unity Inspector (positionOffset, rotationOffset, scaleFactor) pour l'espace de travail du nouveau bras.
  5. 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.
Vous utilisez ROS 2 au lieu d’un SDK direct ? Publier sur un 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.

Configuration terminée ?

Vérifiez les spécifications complètes des paquets UDP ou lisez le wiki complet du développeur pour une couverture plus approfondie.