إعداد Meta Quest 3 VR Teleop
أكمل المسار خطوة بخطوة بدءًا من المهمة 3 العارية حتى التشغيل عن بعد لذراع الروبوت. يغطي إعداد الشبكة، وتكوين Unity، وخادم Python UDP، وpiper_controller.py، ونمط المحول لأي ذراع.
الشبكة والمتطلبات الأساسية
قبل كتابة أي رمز، تأكد من أن 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 الخاصة بالشركات تعزل العملاء عن بعضهم البعض — استخدم نقطة وصول مخصصة لعمل الروبوتات.
تكوين تطبيق الوحدة
تم تصميم تطبيق 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 فوق سطح الطاولة لتجنب الاصطدام |
إعداد خادم بايثون 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)))
QUEUE_MAXSIZE 1 يعطي الحد الأدنى من الكمون ولكن يمكن أن يشعر بالتشنج. تعمل القيمة 3-5 على تسهيل الحركة عبر فقدان الحزمة ولكنها تضيف ما يصل إلى 100 مللي ثانية من زمن الوصول الإضافي عند 30 هرتز. ابدأ في الساعة 3 وقم بالضبط حسب الذوق.
تجول 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: يمر
piper.GetPiperFirmwareVersion() والتحقق من مستندات مطور AgileX قبل الأمر بأي إجراء.
التحقق من السلامة والجلسة الأولى
قائمة التحقق من التشغيل الجاف (إيقاف التشغيل):
- ابدأ تشغيل خادم 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()والخروج نظيفا.
valid تنخفض العلامة الموجودة في حزمة UDP إلى 0. ويحتفظ خادم Python بآخر موضع معروف بدلاً من وضع الذراع في الموضع صفر.
واجهة المحول — منفذ إلى أي ذراع
يتم تعميم نمط مبادلة وحدة التحكم على أي ذراع باستخدام Python SDK. الشرط الوحيد هو أن تقوم فئة وحدة التحكم الخاصة بك بتنفيذ هذه الطرق الخمس باستخدام هذا التوقيع بالضبط:
| طريقة | إمضاء | عقد |
|---|---|---|
| يتصل() | () → منطقي | يفتح قناة الاتصال. يعود صحيحا على النجاح |
| قطع الاتصال() | () → لا شيء | تعطيل طاقة المؤازرة وإغلاق المنفذ أو المقبس |
| set_pose(x، y، z، لفة، خطوة، ياو) | (تعويم × 6) → لا شيء | هدف المؤثر النهائي الديكارتي بالملليمتر + الدرجات؛ يجب المشبك داخليا |
| set_gripper (القيمة) | (تعويم) → لا شيء | الانفتاح الطبيعي 0.0-1.0؛ خريطة للوحدات الخاصة بالذراع داخليًا |
| Emergency_stop() | () → لا شيء | يجب أن تكون آمنة للاتصال من أي موضوع في أي وقت |
خطوات إضافة ذراع جديد:
- يكتب
myarm_controller.pyتنفيذ الطرق الخمس المذكورة أعلاه باستخدام SDK الخاص بذراعك. حدود مساحة عمل الكود الثابت وحدود السرعة كثوابت على مستوى الوحدة. - اختبار الوحدة في العزلة: call
connect()، ثمset_poseمع وضع آمن على بعد 5 سم من المنزل. تحقق من تحركات الذراع وإرجاع الموضع المتوقع. - مبادلة الاستيراد في
teleoperation_main.py: يستبدلfrom piper_controller import PiperControllerمع وحدة التحكم الجديدة الخاصة بك. لا حاجة إلى تغييرات أخرى. - معايرة معلمات Unity Inspector (
positionOffset,rotationOffset,scaleFactor) لمساحة عمل الذراع الجديدة. - التحقق من صحة سلوك الإيقاف الإلكتروني، وتعليق فقدان التتبع، ومشابك مساحة العمل قبل أي جلسة عمل للمشغل.
geometry_msgs/PoseStamped الموضوع داخل set_pose - تبقى بقية العمارة متطابقة. يمتص التصميم القائم على قائمة الانتظار بشكل طبيعي زمن الوصول لدورة ROS 2 pub/sub.