Meta Quest 3 VR Teleop-Setup
Vollständiger Schritt-für-Schritt-Pfad vom einfachen Quest 3 zur Live-Roboterarm-Teleoperation. Behandelt die Netzwerkeinrichtung, die Unity-Konfiguration, den Python-UDP-Server, piper_controller.py und das Adaptermuster für jeden Arm.
Netzwerk & Voraussetzungen
Bevor Sie Code schreiben, stellen Sie sicher, dass Meta Quest 3 und der Steuer-PC einander über dasselbe lokale Netzwerk erreichen können. Das System verwendet UDP – beide Geräte müssen dasselbe Subnetz nutzen und die UDP-Ports 8888 und 8889 müssen in der PC-Firewall geöffnet sein.
Checkliste für Anforderungen:
- Meta Quest 3-Headset mit aktivierter Handverfolgung (Einstellungen → Bewegungsverfolgung → Handverfolgung)
- Wi-Fi 6-Zugangspunkt – es wird dringend empfohlen, die UDP-Transit-Latenz unter 10 ms zu halten
- Steuern Sie einen PC mit Linux (Ubuntu 22.04+) oder macOS mit Python 3.10+
- Für AgileX Piper: USB-zu-CAN-Adapter (z. B. Kvaser oder PEAK), CAN-Kabel am Arm angeschlossen
- Die UDP-Ports 8888 und 8889 sind eingehend auf der PC-Firewall geöffnet
Konnektivität überprüfen:
# 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 "import 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 auf dem PC und überprüfen Sie, ob die ersten drei Oktette mit der IP des Quest 3 übereinstimmen (sichtbar unter Quest-Einstellungen → WLAN → Zahnradsymbol). Ein häufiges Problem besteht darin, dass Wi-Fi-Netzwerke in Unternehmen die Clients voneinander isolieren – verwenden Sie einen dedizierten Zugangspunkt für Roboterarbeiten.
Unity-App-Konfiguration
Die auf dem Quest 3 ausgeführte Unity-App wurde mit Unity 2022.3 LTS oder höher erstellt und verwendet das XR Hands-Paket für die Handverfolgung. Drei Skripte kümmern sich um die Teleoperation:
- VRHandPoseSender.cs – Liest die Handhaltung aus dem XR Hands-Subsystem, serialisiert sie in ein 45-Byte-Binärpaket und sendet es über UDP
- VRGripperController.cs – Ordnet die Einklemmstärke einem normalisierten Greifwert zu [0, 1]
- VRTeleoperationManager.cs – Lebenszyklusverwaltung, Verbindungsstatus-Benutzeroberfläche, automatische Wiederverbindung
Du tust nicht Sie müssen die Unity-App neu kompilieren, um zwischen den Roboterarmen zu wechseln. Machen Sie die folgenden Felder als serialisierte Inspektorfelder verfügbar und optimieren Sie sie über den Unity-Editor oder über eine Konfigurationsdatei:
| Inspektorfeld | AgileX Piper-Startwert | Notizen |
|---|---|---|
| Ziel-IP | Die IP-Adresse Ihres PCs | Laufen ip addr auf dem PC, um es zu finden |
| positionOffset (m) | (0, 0, 0.3) | Verschiebt den Ursprung des virtuellen Roboters; Piper-Reichweite ist kürzer als xArm6 |
| rotationOffset (Grad) | (0, 90, 0) | 90° Y-Korrektur für Piper CAN-Rahmen; je nach Montageausrichtung anpassen |
| Skalierungsfaktor | 0.75 | Reduziert den Handbewegungsbereich, um ihn an den Piper-Arbeitsplatz anzupassen (ca. 600 mm Reichweite). |
| Arbeitsbereich X (mm) | ±400 | Lassen Sie einen Rand von 50 mm innerhalb der physikalischen Grenzen |
| Arbeitsbereich Z (mm) | 50 – 700 | Halten Sie Z min. über der Tischoberfläche, um Kollisionen zu vermeiden |
Python-UDP-Server-Setup
Der Python-Server führt drei gleichzeitige Threads aus: a Empfängerthread das rohe UDP-Datagramme liest, a Sicherheitsfaden das Pakete validiert und in die Warteschlange einreiht, und a Robotersteuerungsthread Dadurch wird die Warteschlange geleert und die Robotersteuerung aufgerufen. Durch diese Trennung wird sichergestellt, dass langsame Roboter-SDK-Aufrufe niemals den UDP-Empfang blockieren.
Abhängigkeiten installieren:
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
Führen Sie den Teleoperationsserver aus:
python3 teleoperation_main.py
Der Server bindet an 0.0.0.0:8888 (rechte Hand) und optional 0.0.0.0:8889 (linke Hand). Wenn ein Paket mit dem ankommt valid Flag gesetzt, ruft der Kontrollthread auf robot.set_pose() Und robot.set_gripper(). Drücken Strg-C um einen Nothalt und eine saubere Abschaltung auszulösen.
Die vereinfachte Serverstruktur zeigt, wie alle drei Threads interagieren:
# teleoperation_main.py (simplified structure) Import socket, struct, queue, threading, signal, time aus piper_controller Import 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 udp_receiver(port: int): „Thread 1 – rohe UDP-Datagramme empfangen.“ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((HOST, port)) sock.settimeout(1.0) während nicht shutdown_event.is_set(): versuchen: data, _ = sock.recvfrom(256) pose = parse_packet(data) Wenn pose Und pose["gültig"]: versuchen: pose_queue.put_nowait(pose) außer queue.Full: pose_queue.get_nowait() # drop oldest frame pose_queue.put_nowait(pose) außer socket.timeout: weitermachen sock.close() def robot_control_loop(robot: PiperController): „Thread 3 – Warteschlange entleeren und Roboter bei CONTROL_HZ befehlen.““ period = 1.0 / CONTROL_HZ last_pose = None während nicht shutdown_event.is_set(): t0 = time.monotonic() versuchen: pose = pose_queue.get(timeout=0.1) Wenn pose[„Sicherungen“]: robot.emergency_stop() weitermachen Wenn pose["gültig"]: last_pose = pose x, y, z = transform_position(pose["Position"]) roll, pitch, yaw = quat_to_euler(pose["Drehung"]) robot.set_pose(x, y, z, roll, pitch, yaw) robot.set_gripper(pose[„Greifer“]) # tracking lost — hold last known position (do not send zero) außer queue.Empty: passieren time.sleep(max(0, period - (time.monotonic() - t0)))
QUEUE_MAXSIZE von 1 sorgt für minimale Latenz, kann sich aber ruckartig anfühlen. Ein Wert von 3–5 glättet die Bewegung über den Paketverlust hinaus, führt aber zu einer zusätzlichen Latenz von bis zu 100 ms bei 30 Hz. Beginnen Sie um 3 Uhr und stimmen Sie es nach Geschmack ab.
Komplettlösung für piper_controller.py
Der piper_controller.py Das Modul umschließt das AgileX piper_sdk Python-Bibliothek. Es implementiert die von erwartete Fünf-Methoden-Schnittstelle teleoperation_main.py. Wichtige Designentscheidungen:
- CAN über USB: Der Piper-Arm kommuniziert über CAN-Bus. Das SDK benötigt einen CAN-Schnittstellennamen (
can0) und erfordert, dass die Schnittstelle vor dem Herstellen einer Verbindung aktiviert wird. - Slave-Modus: Berufung
MasterSlaveConfig(0xFC, 0, 0, 0)Versetzt den Arm in den Slave-Modus, sodass er Streaming-Positionsbefehle akzeptiert. - Arbeitsraumspannung: Die Positionen werden vor jedem SDK-Aufruf fest an die Konstanten oben in der Datei gebunden – diese sind die letzte Verteidigungslinie der Software vor der Firmware.
- SDK-Einheiten: Die Position wird in Mikrometern (Ganzzahl) angegeben, die Ausrichtung in Milligrad – multiplizieren Sie die Float-mm/Grad-Werte mit 1000, bevor Sie an übergeben
EndEffectorCtrl.
# piper_controller.py aus piper_sdk Import C_PiperInterface Import 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 Klasse PiperController: „““Drop-in-Ersatz für XArmController – dieselbe Fünf-Methoden-Schnittstelle.“““ def __init__(self, can_interface: str = „can0“): self.can_interface = can_interface self._piper: C_PiperInterface | None = None self.connected = False def verbinden(self) -> bool: „““CAN-Port öffnen und Slave-Modus aktivieren.““ versuchen: 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 verbunden am %s“, self.can_interface) zurückkehren True außer Exception als e: logger.error(„Piper-Verbindung fehlgeschlagen: %s“, e) zurückkehren False def trennen(self): „Servo deaktivieren und Port schließen.“ Wenn self._piper: versuchen: self._piper.DisableArm(7) # disable all joints außer Exception: passieren self.connected = False def set_pose(self, x: float, y: float, z: float, roll: float, pitch: float, yaw: float): „“„Bewegen Sie den Endeffektor auf (x,y,z) mm mit Ausrichtung (Rollen,Nicken,Gieren) Grad.““ wenn nicht self.connected: zurückkehren x = max(X_MIN, min(X_MAX, x)) y = max(Y_MIN, min(Y_MAX, y)) z = max(Z_MIN, min(Z_MAX, z)) versuchen: self._piper.EndEffectorCtrl( int(x * 1000), int(y * 1000), int(z * 1000), int(roll * 1000), int(pitch * 1000), int(yaw * 1000), SPEED_PERCENT, ) außer Exception als e: logger.warning(„set_pose-Fehler: %s“, e) def set_greifer(self, value: float): „“„Greiferöffnung einstellen. 0,0 = vollständig geschlossen, 1,0 = vollständig geöffnet.“““ wenn nicht self.connected: zurückkehren GRIPPER_MAX_UM = 70_000 # 70 mm max opening in µm target_um = int(value * GRIPPER_MAX_UM) versuchen: self._piper.GripperCtrl(target_um, SPEED_PERCENT, 0x01, 0) außer Exception als e: logger.warning(„set_gripper-Fehler: %s“, e) def Emergency_Stop(self): „“„Deaktivieren Sie sofort alle Gelenke. Der Aufruf kann von jedem Thread aus sicher sein.“““ Wenn self._piper: versuchen: self._piper.DisableArm(7) außer Exception: passieren
piper.GetPiperFirmwareVersion() und prüfen Sie anhand der AgileX-Entwicklerdokumente, bevor Sie eine Aktion anordnen.
Sicherheitsvalidierung und erste Sitzung
Checkliste für den Trockenlauf (Strom AUS):
- Starten Sie den Python-Server und verbinden Sie Quest 3. Sehen Sie sich das an
transform_positionAusgabe auf dem Terminal gedruckt. - Halten Sie Ihre Hand in die Mitte des vorgesehenen Arbeitsbereichs. Stellen Sie sicher, dass die gedruckten XYZ-Werte in der Nähe der Ausgangsposition des Roboters liegen (ungefähr 0, 0, 300 mm für Piper).
- Bewegen Sie Ihre Hand zu jeder Kante des Arbeitsbereichs. Stellen Sie sicher, dass die Werte innerhalb der festgelegten Grenzen bleiben und auf Z niemals negativ werden.
- Drücken Sie die Menütaste auf dem Quest, um den Software-Notstopp auszulösen. Bestätigen
emergency_stop()aufgerufen und die Schleife angehalten.
Erste Live-Sitzung:
- Beginnen Sie bei
SPEED_PERCENT = 20— das entspricht etwa 40°/s maximaler Gelenkgeschwindigkeit. - Servoleistung aktivieren. Bewegen Sie sich langsam und bleiben Sie die erste Minute in der Nähe der Ausgangsposition des Arms.
- Stellen Sie sicher, dass die Handbewegung und die Roboterbewegung in die gleiche Richtung erfolgen. Wenn sich das Handgelenk nach hinten dreht, passen Sie es an
rotationOffsetum ±90° auf Y im Unity Inspector. - Erweitern Sie den Bewegungsbereich schrittweise, sobald die Richtungszuordnung korrekt ist.
- Halten Sie jederzeit einen physischen Not-Aus-Schalter (Leistungsrelais) in Reichweite.
Es sind zwei Software-Notstopppfade implementiert:
- Menüschaltfläche für Quest 3: Setzt Bit 1 des Flag-Bytes in jedem nachfolgenden UDP-Paket. Der Python-Server ruft auf
robot.emergency_stop()sofort. - Strg-C (SIGINT): Der Signalhandler setzt das Shutdown-Ereignis, das den Aufruf des Regelkreises bewirkt
emergency_stop()und sauber verlassen.
valid Flag im UDP-Paket fällt auf 0. Der Python-Server behält die letzte bekannte Position bei, anstatt den Arm auf Position Null zu bewegen.
Adapterschnittstelle – Anschluss an jeden Arm
Das Controller-Swap-Muster lässt sich auf jeden Arm mit einem Python-SDK verallgemeinern. Die einzige Voraussetzung ist, dass Ihre Controller-Klasse diese fünf Methoden mit genau dieser Signatur implementiert:
| Verfahren | Unterschrift | Vertrag |
|---|---|---|
| verbinden() | () → bool | Öffnet den Kommunikationskanal; gibt bei Erfolg „True“ zurück |
| trennen() | () → Keine | Deaktiviert die Servostromversorgung und schließt den Anschluss oder die Buchse |
| set_pose(x, y, z, roll, pitch, yaw) | (float×6) → Keine | Kartesisches Endeffektorziel in mm + Grad; muss innen klemmen |
| set_greifer(Wert) | (float) → Keine | Normalisierte Offenheit 0,0–1,0; Zuordnung zu waffenspezifischen Einheiten intern |
| Emergency_stop() | () → Keine | Der Aufruf muss jederzeit von jedem Thread aus sicher möglich sein |
Schritte zum Hinzufügen eines neuen Arms:
- Schreiben
myarm_controller.pyImplementieren Sie die fünf oben genannten Methoden mithilfe des SDK Ihres Arms. Codieren Sie Arbeitsbereichsgrenzen und Geschwindigkeitsbegrenzungen als Konstanten auf Modulebene fest. - Unit-Test isoliert: Aufruf
connect(), Dannset_posemit einer sicheren Position 5 cm von zu Hause entfernt. Überprüfen Sie, ob sich der Arm bewegt und in die erwartete Position zurückkehrt. - Tauschen Sie den Import ein
teleoperation_main.py: ersetzenfrom piper_controller import PiperControllermit Ihrem neuen Controller. Keine weiteren Änderungen erforderlich. - Kalibrieren Sie die Unity Inspector-Parameter (
positionOffset,rotationOffset,scaleFactor) für den Arbeitsbereich des neuen Arms. - Validieren Sie vor jeder Bedienersitzung das Not-Aus-Verhalten, die Funktion „Tracking-Loss Hold“ und die Arbeitsbereichsklemmen.
geometry_msgs/PoseStamped Thema im Inneren set_pose — Der Rest der Architektur bleibt identisch. Das warteschlangenbasierte Design absorbiert natürlich die Latenz eines ROS 2 Pub/Sub-Zyklus.