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.

1

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))"
Gleiches Subnetz erforderlich. Laufen 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.
2

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
Kalibrieren Sie vor Ihrem ersten Live-Lauf. Bewegen Sie den Quest 3 langsam an jede Kante des vorgesehenen Arbeitsbereichs und beobachten Sie die befohlene Position in Ihrer Terminalausgabe. Stellen Sie sicher, dass der Roboter innerhalb seiner Gelenkgrenzen bleibt, bevor Sie das Streaming mit voller Geschwindigkeit aktivieren.
3

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)))
Die Warteschlangengröße steuert Latenz und Glätte. A 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.
4

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
Überprüfen Sie die SDK-Einheiten auf Ihre Firmware-Version. Das Piper SDK entwickelt sich schnell weiter. In einigen Firmware-Versionen wird die Position in Mikrometern (Ganzzahl) angegeben. in anderen sind es Millimeter (Float). Drucken piper.GetPiperFirmwareVersion() und prüfen Sie anhand der AgileX-Entwicklerdokumente, bevor Sie eine Aktion anordnen.
5

Sicherheitsvalidierung und erste Sitzung

Vor dem Einschalten lesen. VR-Teleoperation steuert den Roboter in Echtzeit basierend auf Ihrer Handbewegung. Ein falsch kalibrierter PositionOffset oder ScaleFactor kann dazu führen, dass sich der Arm sofort in eine Position bewegt, die ihn selbst beschädigt oder Personen in der Nähe verletzt. Führen Sie immer zuerst einen Trockenlauf mit ausgeschalteter Stromversorgung des Roboters durch.

Checkliste für den Trockenlauf (Strom AUS):

  • Starten Sie den Python-Server und verbinden Sie Quest 3. Sehen Sie sich das an transform_position Ausgabe 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 rotationOffset um ±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.
Tracking-Verluste werden sicher gehandhabt. Wenn das Quest 3 die Handverfolgung verliert (Hand verlässt das Kamera-Sichtfeld, Finger überlappen), wird die 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.
6

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:

  1. Schreiben myarm_controller.py Implementieren Sie die fünf oben genannten Methoden mithilfe des SDK Ihres Arms. Codieren Sie Arbeitsbereichsgrenzen und Geschwindigkeitsbegrenzungen als Konstanten auf Modulebene fest.
  2. Unit-Test isoliert: Aufruf connect(), Dann set_pose mit einer sicheren Position 5 cm von zu Hause entfernt. Überprüfen Sie, ob sich der Arm bewegt und in die erwartete Position zurückkehrt.
  3. Tauschen Sie den Import ein teleoperation_main.py: ersetzen from piper_controller import PiperController mit Ihrem neuen Controller. Keine weiteren Änderungen erforderlich.
  4. Kalibrieren Sie die Unity Inspector-Parameter (positionOffset, rotationOffset, scaleFactor) für den Arbeitsbereich des neuen Arms.
  5. Validieren Sie vor jeder Bedienersitzung das Not-Aus-Verhalten, die Funktion „Tracking-Loss Hold“ und die Arbeitsbereichsklemmen.
Verwenden Sie ROS 2 anstelle eines direkten SDK? Veröffentlichen Sie auf a 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.

Einrichtung abgeschlossen?

Sehen Sie sich die vollständigen UDP-Paketspezifikationen an oder lesen Sie das vollständige Entwickler-Wiki für eine ausführlichere Abdeckung.