הגדרת Meta Quest 3 VR Teleop

השלם את הנתיב צעד אחר צעד מ-Quest 3 חשוף להפעלה חיה של זרועות הרובוטים. מכסה את הגדרת הרשת, תצורת Unity, שרת Python UDP, piper_controller.py ודפוס המתאם לכל זרוע.

1

רשת ודרישות מוקדמות

לפני כתיבת קוד כלשהו, ​​אשר כי Meta Quest 3 ומחשב הבקרה יכולים להגיע זה לזה דרך אותה רשת מקומית. המערכת משתמשת ב-UDP - שני המכשירים חייבים לשתף את אותה רשת משנה, ויציאות UDP 8888 ו-8889 חייבות להיות פתוחות בחומת האש של המחשב.

רשימת דרישות:

  • אוזניות Meta Quest 3 עם מעקב ידני מופעל (הגדרות ← מעקב אחר תנועה ← מעקב יד)
  • נקודת גישה ל-Wi-Fi 6 - מומלץ בחום כדי לשמור על זמן אחזור תחבורה של UDP מתחת ל-10 אלפיות השנייה
  • שליטה במחשב עם לינוקס (Ubuntu 22.04+) או macOS עם Python 3.10+
  • עבור AgileX Piper: מתאם USB ל-CAN (למשל Kvaser או PEAK), כבל CAN מחובר לזרוע
  • יציאות UDP 8888 ו-8889 נפתחות נכנסות בחומת האש של המחשב

בדוק קישוריות:

# 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 "ייבואשקע; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.bind(('0.0.0.0',8888)); print('Listening...'); print(s.recvfrom(256))"
נדרשת אותה רשת משנה. לָרוּץ ip addr במחשב האישי וודא ששלושת האוקטטים הראשונים תואמים את ה-IP של Quest 3 (גלוי בהגדרות Quest → Wi-Fi → סמל גלגל השיניים). בעיה נפוצה היא שרשתות Wi-Fi ארגוניות מבודדות לקוחות זו מזו - השתמשו בנקודת גישה ייעודית לעבודת רובוטיקה.
2

תצורת App Unity

אפליקציית Unity הפועלת ב-Quest 3 בנויה עם Unity 2022.3 LTS ואילך, תוך שימוש בחבילת XR Hands למעקב ידני. שלושה סקריפטים מטפלים בצד הטלאופרציה:

  • VRHandPoseSender.cs - קורא תנוחת יד מתת-המערכת XR Hands, מסדרת לחבילה בינארית של 45 בתים, שולח באמצעות UDP
  • VRGripperController.cs - ממפה את עוצמת הצביטה לערך תפס מנורמל [0, 1]
  • VRTeleoperationManager.cs - ניהול מחזור חיים, ממשק משתמש סטטוס חיבור, חיבור אוטומטי מחדש

אתה כן לֹא צריך להרכיב מחדש את אפליקציית Unity כדי לעבור בין זרועות הרובוט. חשפו את השדות הבאים כשדות מפקח בסידרה וכוונו אותם מעורך Unity או דרך קובץ תצורה:

שדה המפקח ערך התחלתי של AgileX Piper הערות
IP יעד כתובת ה-IP של המחשב האישי שלך לָרוּץ ip addr במחשב כדי למצוא אותו
מיקום היסט (מ') (0, 0, 0.3) משנה את מקור הרובוט הווירטואלי; טווח הפייפר קצר מ-xArm6
סיבוב היסט (מעלה) (0, 90, 0) תיקון Y של 90° עבור מסגרת CAN של Piper; התאם לפי כיוון הרכבה
scaleFactor 0.75 מקטין את טווח תנועת היד כדי להתאים לסביבת העבודה של פייפר (כ-600 מ"מ טווח)
סביבת עבודה X (מ"מ) ±400 השאר שוליים של 50 מ"מ בתוך גבולות פיזיים
סביבת עבודה Z (מ"מ) 50 – 700 שמור על Z min מעל פני השולחן כדי למנוע התנגשות
כייל לפני הריצה החיה הראשונה שלך. הזז את ה-Quest 3 באיטיות לכל קצה של סביבת העבודה המיועדת וצפה במיקום הפקודה בפלט הטרמינל שלך. ודא שהרובוט נשאר היטב בתוך גבולות המפרקים שלו לפני הפעלת סטרימינג במהירות מלאה.
3

הגדרת שרת Python UDP

שרת Python מריץ שלושה שרשורים במקביל: א חוט מקלט שקורא נתונים גולמיים של UDP, א חוט בטיחות שמאמתת מנות ומעמידה אותן בתור, וכן א חוט בקרת רובוט שמנקז את התור וקורא לבקר הרובוט. הפרדה זו מבטיחה ששיחות SDK לרובוט איטיות לעולם לא יחסמו קליטת UDP.

תלות בהתקנה:

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

הפעל את שרת ה-Teleoperation:

python3 teleoperation_main.py

השרת מתחבר ל 0.0.0.0:8888 (יד ימין) ואופציונלי 0.0.0.0:8889 (יד שמאל). כאשר מגיעה חבילה עם ה valid הדגל מוגדר, שרשור הבקרה קורא robot.set_pose() ו robot.set_gripper(). לִלְחוֹץ Ctrl-C כדי להפעיל עצירת חירום וכיבוי נקי.

מבנה השרת הפשוט המראה כיצד כל שלושת השרשורים מתקשרים:

# teleoperation_main.py (simplified structure)
יְבוּא socket, struct, queue, threading, signal, time
מִן piper_controller יְבוּא 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):
    """שרשור 1 - קבל נתונים גולמיים של UDP."""
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind((HOST, port))
    sock.settimeout(1.0)
    בזמן שלא shutdown_event.is_set():
        לְנַסוֹת:
            data, _ = sock.recvfrom(256)
            pose = parse_packet(data)
            אִם pose ו pose["תָקֵף"]:
                לְנַסוֹת:
                    pose_queue.put_nowait(pose)
                אֶלָא queue.Full:
                    pose_queue.get_nowait()   # drop oldest frame
                    pose_queue.put_nowait(pose)
        אֶלָא socket.timeout:
            לְהַמשִׁיך
    sock.close()

def רובוט_שליטה_לולאת(robot: PiperController):
    """שרשור 3 - ניקוז תור ורובוט פקודה ב-CONTROL_HZ."""
    period = 1.0 / CONTROL_HZ
    last_pose = None
    בזמן שלא shutdown_event.is_set():
        t0 = time.monotonic()
        לְנַסוֹת:
            pose = pose_queue.get(timeout=0.1)
            אִם pose["לְהַפְסִיק"]:
                robot.emergency_stop()
                לְהַמשִׁיך
            אִם pose["תָקֵף"]:
                last_pose = pose
                x, y, z = transform_position(pose["מַצָב"])
                roll, pitch, yaw = quat_to_euler(pose["רוֹטַציָה"])
                robot.set_pose(x, y, z, roll, pitch, yaw)
                robot.set_gripper(pose["תפסן"])
            # tracking lost — hold last known position (do not send zero)
        אֶלָא queue.Empty:
            לַעֲבוֹר
        time.sleep(max(0, period - (time.monotonic() - t0)))
גודל התור שולט בזמן האחזור לעומת החלקות. A QUEUE_MAXSIZE של 1 נותן זמן אחזור מינימלי אבל יכול להרגיש קופצני. ערך של 3-5 מחליק את התנועה על פני אובדן מנות, אך מוסיף עד 100 אלפיות השנייה חביון נוסף ב-30 הרץ. מתחילים ב-3 ומכוונים לפי הטעם.
4

piper_controller.py הדרכה

ה piper_controller.py מודול עוטף את ה-AgileX piper_sdk ספריית פייתון. הוא מיישם את ממשק חמש השיטות המצופה על ידי teleoperation_main.py. החלטות עיצוב מרכזיות:

  • CAN דרך USB: זרוע הפייפר מתקשרת דרך אוטובוס CAN. ה-SDK מקבל שם ממשק CAN (can0) ומחייב את הפעלת הממשק לפני החיבור.
  • מצב עבדים: יִעוּד MasterSlaveConfig(0xFC, 0, 0, 0) מכניס את הזרוע למצב עבד כך שהיא מקבלת פקודות מיקום סטרימינג.
  • הידוק סביבת עבודה: מיקומים מוצמדים לקבועים בחלק העליון של הקובץ לפני כל קריאת SDK - אלה הם השורה האחרונה של ההגנה על התוכנה לפני הקושחה.
  • יחידות SDK: המיקום הוא במיקרומטרים (מספר שלם), כיוון במלי מעלות - הכפל את ערכי הצפה מ"מ/מעלות ב-1000 לפני מעבר ל EndEffectorCtrl.
# piper_controller.py
מִן piper_sdk יְבוּא C_PiperInterface
יְבוּא 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


מַחלָקָה PiperController:
    """תחליף ל-Drop-in עבור XArmController - אותו ממשק של חמש שיטות."""

    def __init__(self, can_interface: str = "יכול 0"):
        self.can_interface = can_interface
        self._piper: C_PiperInterface | None = None
        self.connected = False

    def לְחַבֵּר(self) -> bool:
        """פתח את יציאת CAN והפעל מצב עבדים."""
        לְנַסוֹת:
            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("פייפר מחובר ב-%s", self.can_interface)
            לַחֲזוֹר True
        אֶלָא Exception כְּמוֹ e:
            logger.error("חיבור הצינור נכשל: %s", e)
            לַחֲזוֹר False

    def לְנַתֵק(self):
        """השבת סרוו וסגור יציאה."""
        אִם self._piper:
            לְנַסוֹת:
                self._piper.DisableArm(7)   # disable all joints
            אֶלָא Exception:
                לַעֲבוֹר
        self.connected = False

    def set_pose(self, x: float, y: float, z: float,
                 roll: float, pitch: float, yaw: float):
        """הזז את גורם הקצה ל-(x,y,z) מ"מ במעלות כיוון (גלגול, גובה, סיבוב)."""
        אִם לֹא self.connected:
            לַחֲזוֹר
        x = max(X_MIN, min(X_MAX, x))
        y = max(Y_MIN, min(Y_MAX, y))
        z = max(Z_MIN, min(Z_MAX, z))
        לְנַסוֹת:
            self._piper.EndEffectorCtrl(
                int(x * 1000), int(y * 1000), int(z * 1000),
                int(roll  * 1000),
                int(pitch * 1000),
                int(yaw   * 1000),
                SPEED_PERCENT,
            )
        אֶלָא Exception כְּמוֹ e:
            logger.warning("שגיאת set_pose: %s", e)

    def set_gripper(self, value: float):
        """הגדר את פתיחות האוחז. 0.0 = סגור לחלוטין, 1.0 = פתוח לחלוטין."""
        אִם לֹא self.connected:
            לַחֲזוֹר
        GRIPPER_MAX_UM = 70_000   # 70 mm max opening in µm
        target_um = int(value * GRIPPER_MAX_UM)
        לְנַסוֹת:
            self._piper.GripperCtrl(target_um, SPEED_PERCENT, 0x01, 0)
        אֶלָא Exception כְּמוֹ e:
            logger.warning("שגיאת set_gripper: %s", e)

    def עצירת חירום(self):
        """השבת מיד את כל המפרקים. בטוח להתקשר מכל שרשור."""
        אִם self._piper:
            לְנַסוֹת:
                self._piper.DisableArm(7)
            אֶלָא Exception:
                לַעֲבוֹר
בדוק יחידות SDK עבור גרסת הקושחה שלך. ה- Piper SDK מתפתח במהירות. בחלק מגרסאות הקושחה המיקום הוא במיקרומטר (מספר שלם); באחרים זה מילימטרים (צף). הֶדפֵּס piper.GetPiperFirmwareVersion() ואמת מול מסמכי המפתחים של AgileX לפני פקודה על תנועה כלשהי.
5

אימות בטיחות ופגישה ראשונה

קרא לפני ההפעלה. תפעול VR מפעיל את הרובוט בזמן אמת על סמך תנועת היד שלך. מיקום שגוי מכויל Offset או scaleFactor יכול לגרום לזרוע לנוע באופן מיידי למצב שפוגע בעצמו או פוגע באנשים קרובים. הפעל תמיד יבש עם כיבוי רובוט ראשון.

רשימת בדיקה יבשה (כיבוי):

  • הפעל את שרת Python וחבר את Quest 3. צפה ב transform_position פלט מודפס למסוף.
  • החזק את ידך במרכז סביבת העבודה המיועדת. ודא שערכי ה-XYZ המודפסים נמצאים ליד מיקום הבית של הרובוט (כ-0, 0, 300 מ"מ עבור פייפר).
  • הזז את היד שלך לכל קצה סביבת עבודה. אשר שהערכים נשארים בגבולות המהודקים ולעולם לא ישתלו ב-Z.
  • לחץ על לחצן התפריט ב-Quest כדי להפעיל את עצירת התוכנה האלקטרונית. לְאַשֵׁר emergency_stop() נקרא והלולאה נעצרת.

מפגש חי ראשון:

  • התחל ב SPEED_PERCENT = 20 - זה בערך 40°/s מהירות המפרק המקסימלית.
  • אפשר כוח סרוו. זז לאט, הישאר ליד עמדת הבית של הזרוע במשך הדקה הראשונה.
  • ודא שתנועת היד ותנועת הרובוט נמצאים באותו כיוון. אם פרק כף היד מסתובב לאחור, התאם rotationOffset ב-90° ± על Y במפקח האחדות.
  • הרחב בהדרגה את טווח התנועה לאחר אישור מיפוי הכיוון הנכון.
  • שמור על עצירת חירום פיזית (ממסר מתח) בהישג יד בכל עת.

מיושמים שני נתיבי עצירה אלקטרונית של תוכנה:

  • כפתור תפריט קווסט 3: מגדיר סיביות 1 של בייט הדגלים בכל מנות UDP עוקבות. שרת Python קורא robot.emergency_stop() מִיָד.
  • Ctrl-C (SIGINT): מטפל האותות מגדיר את אירוע הכיבוי, שגורם ללולאת הבקרה להתקשר emergency_stop() ולצאת בצורה נקייה.
אובדן מעקב מטופל בבטחה. כאשר ה-Quest 3 מאבד את מעקב הידיים (היד עוזבת FOV של המצלמה, האצבעות חופפות), ה- valid הדגל בחבילת UDP יורד ל-0. שרת Python מחזיק במיקום האחרון הידוע במקום להצמיד את הזרוע למצב אפס.
6

ממשק מתאם - יציאה לכל זרוע

דפוס החלפת הבקר מתכלל לכל זרוע עם Python SDK. הדרישה היחידה היא שמחלקת הבקר שלך תטמיע את חמש השיטות האלה בדיוק עם החתימה הזו:

שִׁיטָה חֲתִימָה חוֹזֶה
לְחַבֵּר() () → bool פותח ערוץ תקשורת; מחזירה True על הצלחה
לְנַתֵק() () → אין משבית את כוח הסרוו וסוגר יציאה או שקע
set_pose(x, y, z, roll, pitch, yaw) (צף×6) → אין יעד אפקטור קצה קרטזי במ"מ + מעלות; חייב להדק פנימי
set_gripper(value) (צף) → אין פתיחות מנורמלת 0.0-1.0; מפה ליחידות ספציפיות לזרוע באופן פנימי
emergency_stop() () → אין חייב להיות בטוח להתקשר מכל שרשור בכל עת

שלבים להוספת זרוע חדשה:

  1. לִכתוֹב myarm_controller.py יישום חמש השיטות שלמעלה באמצעות SDK של הזרוע שלך. גבולות סביבת עבודה של קוד קשיח ומגבלות מהירות כקבועים ברמת המודול.
  2. בדיקת יחידה בבידוד: התקשר connect(), אז set_pose עם מיקום בטוח 5 ס"מ מהבית. ודא שהזרוע זזה ומחזירה את המיקום הצפוי.
  3. החלף את הייבוא ​​פנימה teleoperation_main.py: להחליף from piper_controller import PiperController עם הבקר החדש שלך. אין צורך בשינויים אחרים.
  4. כייל את הפרמטרים של Unity Inspector (positionOffset, rotationOffset, scaleFactor) עבור סביבת העבודה של הזרוע החדשה.
  5. אמת את התנהגות עצירה אלקטרונית, מעקב אחר אובדן, ומהדקי סביבת עבודה לפני כל הפעלת מפעיל.
משתמש ב-ROS 2 במקום ב-SDK ישיר? פרסם בא geometry_msgs/PoseStamped נושא בפנים set_pose - שאר הארכיטקטורה נשארת זהה. העיצוב מבוסס התור סופג באופן טבעי את ההשהיה של מחזור פאב/משנה ROS 2.

ההגדרה הושלמה?

בדוק את מפרט מנות UDP המלא או קרא את הוויקי המלא למפתחים לסיקור מעמיק יותר.