إعداد Meta Quest 3 VR Teleop

أكمل المسار خطوة بخطوة بدءًا من المهمة 3 العارية حتى التشغيل عن بعد لذراع الروبوت. يغطي إعداد الشبكة، وتكوين Unity، وخادم Python UDP، وpiper_controller.py، ونمط المحول لأي ذراع.

1

الشبكة والمتطلبات الأساسية

قبل كتابة أي رمز، تأكد من أن Meta Quest 3 وجهاز الكمبيوتر الشخصي للتحكم يمكنهما الوصول إلى بعضهما البعض عبر نفس الشبكة المحلية. يستخدم النظام UDP — يجب أن يشترك كلا الجهازين في نفس الشبكة الفرعية، ويجب أن يكون منفذا UDP 8888 و8889 مفتوحين في جدار الحماية للكمبيوتر الشخصي.

قائمة التحقق من المتطلبات:

  • سماعة الرأس Meta Quest 3 مع تمكين تتبع اليد (الإعدادات → تتبع الحركة → تتبع اليد)
  • نقطة وصول Wi-Fi 6 - يوصى بشدة بالحفاظ على زمن انتقال عبور UDP أقل من 10 مللي ثانية
  • التحكم في جهاز الكمبيوتر الذي يعمل بنظام التشغيل Linux (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

تكوين تطبيق الوحدة

تم تصميم تطبيق Unity الذي يعمل على Quest 3 باستخدام Unity 2022.3 LTS أو إصدار أحدث، باستخدام حزمة XR Hands للتتبع اليدوي. ثلاثة نصوص تتعامل مع جانب التشغيل عن بعد:

  • VRHandPoseSender.cs - يقرأ وضع اليد من النظام الفرعي XR Hands، ويجري تسلسلاً إلى حزمة ثنائية بحجم 45 بايت، ويرسل عبر UDP
  • VRGripperController.cs - خرائط قرصة القوة إلى قيمة القابض الطبيعية [0، 1]
  • VRTeleoperationManager.cs - إدارة دورة الحياة، واجهة مستخدم حالة الاتصال، إعادة الاتصال التلقائي

أنت تفعل لا بحاجة إلى إعادة ترجمة تطبيق Unity للتبديل بين أذرع الروبوت. اعرض الحقول التالية كحقول Inspector متسلسلة وقم بضبطها من Unity Editor أو عبر ملف التكوين:

ميدان المفتش AgileX Piper القيمة المبدئية ملحوظات
عنوان IP المستهدف عنوان IP لجهاز الكمبيوتر الخاص بك يجري ip addr على جهاز الكمبيوتر للعثور عليه
إزاحة الموضع (م) (0, 0, 0.3) يغير أصل الروبوت الافتراضي؛ مدى وصول بايبر أقصر من xArm6
إزاحة الدوران (درجة) (0, 90, 0) تصحيح 90 درجة Y لإطار Piper CAN؛ ضبط لكل اتجاه جبل
عامل القياس 0.75 يقلل من نطاق حركة اليد ليناسب مساحة عمل بايبر (~600 مم)
مساحة العمل X (مم) ±400 ترك هامش 50 ملم داخل الحدود المادية
مساحة العمل Z (مم) 50 – 700 احتفظ بـ Z min فوق سطح الطاولة لتجنب الاصطدام
قم بالمعايرة قبل أول تشغيل مباشر لك. انقل المهمة 3 ببطء إلى كل حافة من مساحة العمل المقصودة وشاهد الموضع المطلوب في مخرجاتك الطرفية. تأكد من بقاء الروبوت جيدًا داخل حدود مفاصله قبل تمكين البث بأقصى سرعة.
3

إعداد خادم بايثون UDP

يقوم خادم بايثون بتشغيل ثلاثة سلاسل رسائل متزامنة: أ موضوع المتلقي الذي يقرأ مخططات بيانات 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

تشغيل خادم التشغيل عن بعد:

python3 teleoperation_main.py

يرتبط الخادم بـ 0.0.0.0:8888 (اليد اليمنى) واختياريا 0.0.0.0:8889 (اليد اليسرى). عندما تصل الحزمة مع valid مجموعة العلم، يستدعي مؤشر ترابط التحكم robot.set_pose() و robot.set_gripper(). يضعط السيطرة-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()

مواطنه 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()

مواطنه robot_control_loop(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. قرارات التصميم الرئيسية:

  • يمكن عبر 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:
    """استبدال XArmController - نفس الواجهة ذات الخمس طرق."""

    مواطنه __الحرف الأولي__(self, can_interface: str = "يمكن0"):
        self.can_interface = can_interface
        self._piper: C_PiperInterface | None = None
        self.connected = False

    مواطنه يتصل(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("فشل اتصال Piper: %s", e)
            يعود False

    مواطنه قطع الاتصال(self):
        """تعطيل المؤازرة وإغلاق المنفذ."""
        لو self._piper:
            يحاول:
                self._piper.DisableArm(7)   # disable all joints
            يستثني Exception:
                يمر
        self.connected = False

    مواطنه 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(
                كثافة العمليات(x * 1000), كثافة العمليات(y * 1000), كثافة العمليات(z * 1000),
                كثافة العمليات(roll  * 1000),
                كثافة العمليات(pitch * 1000),
                كثافة العمليات(yaw   * 1000),
                SPEED_PERCENT,
            )
        يستثني Exception مثل e:
            logger.warning("خطأ set_pose: %s", e)

    مواطنه set_gripper(self, value: float):
        """ضبط فتحة المقبض. 0.0 = مغلق بالكامل، 1.0 = مفتوح بالكامل."""
        إذا لم يكن كذلك self.connected:
            يعود
        GRIPPER_MAX_UM = 70_000   # 70 mm max opening in µm
        target_um = كثافة العمليات(value * GRIPPER_MAX_UM)
        يحاول:
            self._piper.GripperCtrl(target_um, SPEED_PERCENT, 0x01, 0)
        يستثني Exception مثل e:
            logger.warning("خطأ set_gripper: %s", e)

    مواطنه Emergency_stop(self):
        """قم بتعطيل كافة الوصلات فورًا. الاتصال آمن من أي مؤشر ترابط."""
        لو self._piper:
            يحاول:
                self._piper.DisableArm(7)
            يستثني Exception:
                يمر
تحقق من وحدات SDK لمعرفة إصدار البرنامج الثابت الخاص بك. يتطور Piper SDK بسرعة. في بعض إصدارات البرامج الثابتة، يكون الموضع بالميكرومتر (عدد صحيح)؛ وفي حالات أخرى يكون مليمترات (تعويم). مطبعة piper.GetPiperFirmwareVersion() والتحقق من مستندات مطور AgileX قبل الأمر بأي إجراء.
5

التحقق من السلامة والجلسة الأولى

اقرأ قبل التشغيل. تعمل عملية التشغيل عن بعد بتقنية الواقع الافتراضي على توجيه الروبوت في الوقت الفعلي بناءً على حركة يدك. يمكن أن يؤدي إزاحة الموضع أو عامل القياس بشكل غير صحيح إلى تحرك الذراع على الفور إلى موضع يؤدي إلى إتلاف نفسه أو إصابة الأشخاص القريبين. قم دائمًا بالتشغيل الجاف مع إيقاف تشغيل طاقة الروبوت أولاً.

قائمة التحقق من التشغيل الجاف (إيقاف التشغيل):

  • ابدأ تشغيل خادم Python وقم بتوصيل Quest 3. شاهد transform_position الإخراج المطبوع إلى المحطة.
  • ضع يدك في وسط مساحة العمل المقصودة. تأكد من أن قيم XYZ المطبوعة قريبة من الموضع الرئيسي للروبوت (حوالي 0، 0، 300 مم لـ Piper).
  • حرك يدك إلى كل حافة مساحة العمل. تأكد من بقاء القيم ضمن الحدود المثبتة وألا تصبح سالبة أبدًا على Z.
  • اضغط على زر القائمة في المهمة لتشغيل الإيقاف الإلكتروني للبرنامج. يتأكد emergency_stop() يتم استدعاؤه وتتوقف الحلقة.

الجلسة المباشرة الأولى:

  • ابدأ في SPEED_PERCENT = 20 - هذا هو الحد الأقصى لسرعة المفصل بحوالي 40 درجة/ثانية.
  • تمكين قوة المؤازرة. التحرك ببطء، والبقاء بالقرب من موضع الذراع الأصلي للدقيقة الأولى.
  • تأكد من أن حركة اليد وحركة الروبوت في نفس الاتجاه. إذا كان المعصم يدور للخلف، فاضبطه rotationOffset بمقدار ±90° على Y في Unity Inspector.
  • قم بتوسيع نطاق الحركة تدريجيًا بمجرد التأكد من صحة تعيين الاتجاه.
  • احتفظ بنقطة توقف الطوارئ (مرحل الطاقة) في متناول اليد في جميع الأوقات.

يتم تنفيذ مسارين للتوقف الإلكتروني للبرنامج:

  • زر قائمة المهمة 3: يضبط البت 1 من بايت الإشارات في كل حزمة UDP لاحقة. يدعو خادم بايثون robot.emergency_stop() في الحال.
  • Ctrl-C (إشارة): يقوم معالج الإشارة بتعيين حدث إيقاف التشغيل، مما يؤدي إلى استدعاء حلقة التحكم emergency_stop() والخروج نظيفا.
يتم التعامل مع فقدان التتبع بأمان. عندما يفقد Quest 3 تتبع اليد (تترك اليد مجال رؤية الكاميرا، وتتداخل الأصابع)، فإن valid تنخفض العلامة الموجودة في حزمة UDP إلى 0. ويحتفظ خادم Python بآخر موضع معروف بدلاً من وضع الذراع في الموضع صفر.
6

واجهة المحول — منفذ إلى أي ذراع

يتم تعميم نمط مبادلة وحدة التحكم على أي ذراع باستخدام Python SDK. الشرط الوحيد هو أن تقوم فئة وحدة التحكم الخاصة بك بتنفيذ هذه الطرق الخمس باستخدام هذا التوقيع بالضبط:

طريقة إمضاء عقد
يتصل() () → منطقي يفتح قناة الاتصال. يعود صحيحا على النجاح
قطع الاتصال() () → لا شيء تعطيل طاقة المؤازرة وإغلاق المنفذ أو المقبس
set_pose(x، y، z، لفة، خطوة، ياو) (تعويم × 6) → لا شيء هدف المؤثر النهائي الديكارتي بالملليمتر + الدرجات؛ يجب المشبك داخليا
set_gripper (القيمة) (تعويم) → لا شيء الانفتاح الطبيعي 0.0-1.0؛ خريطة للوحدات الخاصة بالذراع داخليًا
Emergency_stop() () → لا شيء يجب أن تكون آمنة للاتصال من أي موضوع في أي وقت

خطوات إضافة ذراع جديد:

  1. يكتب myarm_controller.py تنفيذ الطرق الخمس المذكورة أعلاه باستخدام SDK الخاص بذراعك. حدود مساحة عمل الكود الثابت وحدود السرعة كثوابت على مستوى الوحدة.
  2. اختبار الوحدة في العزلة: call 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 pub/sub.

هل اكتمل الإعداد؟

تحقق من مواصفات حزمة UDP الكاملة أو اقرأ موقع wiki الكامل للمطور للحصول على تغطية أعمق.