
سلسلة OmArm Zero
الجزء 1: البناء · الجزء 2: URDF + RViz + Gazebo · الجزء 3: MoveIt 2 (هذا المقال) · الجزء 4: الالتقاط والوضع بالرؤية الحاسوبية
نظرة سريعة
ما ستبنيه: إعداد MoveIt 2 للذراع OmArm Zero. يغطي الحركيات العكسية، والتخطيط الخالي من التصادمات في RViz، والتنفيذ في Gazebo، وجسرًا بلغة Python يقود محركات السيرفو الحقيقية عبر المنفذ التسلسلي أو واي فاي (WiFi).
المتطلبات المسبقة: إتمام الجزء 2 (مع عمل URDF وRViz وGazebo). يجب أن تكون قادرًا على تشغيل محاكاة Gazebo ورؤية الذراع تستجيب لأوامر المسار الحركي للمفاصل.
البرمجيات: Ubuntu 24.04، وROS 2 Jazzy، وGazebo Harmonic، وMoveIt 2.
الوقت: 3-5 ساعات (الإعداد، والمعايرة، والاختبار على العتاد الحقيقي).
مستوى الصعوبة: من متوسط إلى متقدم.
الملفات: مساحة عمل ROS 2 الكاملة متوفرة في المتجر (حزمة مساحة عمل ROS 2 للذراع OmArm Zero، بسعر 29 €؛ الرابط في النهاية).
1. من أشرطة تمرير المفاصل إلى تخطيط الحركة
في الجزء 2 بنيت التوأم الرقمي للذراع OmArm Zero بصيغة URDF، وهي صيغة XML لوصف الوصلات (Links) والمفاصل، وشغّلته في RViz وGazebo. كان بإمكانك تحريك كل مفصل بشريط تمرير وإرسال أوامر المسار الحركي للمفاصل. هذا يكفي لاختبار كل مفصل على حدة. أما المهام الحقيقية فتبدأ من الطرف الآخر: تعرف أين يجب أن يصل القابض وتحتاج إلى زوايا المفاصل.
كل حركة جديدة جرّبتها بأشرطة التمرير في الجزء 2 إما جعلت الذراع تصطدم بنفسها أو أخطأت الهدف بنحو 3 cm. كنت أخمّن زوايا المفاصل.
باستخدام أشرطة التمرير، كنت تختار زوايا المفاصل وتراقب أين ينتهي القابض. هذه هي الحركيات الأمامية (Forward Kinematics, FK): زوايا المفاصل هي المدخلات، وموضع القابض هو المخرجات. لالتقاط كوب، تحدد أين يجب أن يصل القابض: موضع الكوب في X وY وZ واتجاه القابض. أما حساب زوايا المفاصل فهو مهمة الروبوت.
عمل الجزء 2 في فضاء المفاصل (Joint Space): قائمة من زوايا المفاصل. أما الجزء 3 فيعمل في فضاء المهمة (Task Space): موضع القابض واتجاهه في الغرفة. ويترجم MoveIt 2 بين الفضاءين.

2. الحركيات العكسية
الحركيات الأمامية هي الاتجاه السهل. لديك زوايا المفاصل وملف URDF (أطوال الوصلات، ومحاور المفاصل، والإزاحات). طبّق دوران كل مفصل على طول السلسلة فتحصل على وضعية القابض (Pose)، أي موضعه مع اتجاهه. مدخل واحد، ومخرج واحد، والحل ممكن دائمًا. وقد ظهر ذلك في الجزء 2 عندما حسب robot_state_publisher شجرة TF (شجرة الأطر الإحداثية) من حالات المفاصل.
الحركيات العكسية (Inverse Kinematics, IK) هي العكس: إذا عُلمت وضعية مستهدفة للقابض، فأوجد زوايا المفاصل التي توصله إليها.

الحركيات العكسية (IK) أصعب من الحركيات الأمامية (FK) لثلاثة أسباب.
حلول متعددة. لمعظم وضعيات القابض التي يمكن الوصول إليها توجد عدة تهيئات صالحة للمفاصل. تخيّل أنك تلمس أنفك. يمكن أن يشير مرفقك إلى اليسار أو اليمين أو الأعلى أو الأسفل: الموضع نفسه لطرف الإصبع، لكن بتهيئات مختلفة للذراع. وقد يكون لذراع صناعية عامة بستة مفاصل ما يصل إلى 16 حلًا للحركيات العكسية لوضعية واحدة للقابض. في هذه السلسلة تُعدّ OmArm Zero ذراعًا بست درجات حرية (6-DOF؛ ودرجة الحرية DOF اتجاه حركة مستقل): خمسة مفاصل للذراع بالإضافة إلى قابض، تقودها ستة محركات سيرفو. ومع ذلك تستطيع مفاصل الذراع الخمسة الوصول إلى أهداف كثيرة بأكثر من طريقة، مثل وضع المرفق للأعلى أو للأسفل.
لا يوجد حل. إذا كان الهدف خارج نطاق العمل (Workspace، أي الحيّز الذي يستطيع القابض الوصول إليه)، أو كانت حدود المفاصل تمنع الاتجاه المطلوب، فلا توجد إجابة. وعلى أداة الحل (Solver) أن تكتشف ذلك وتبلغ بالفشل بدلًا من إرجاع نتيجة غير صالحة.
نقاط التفرّد (Singularities). في بعض التهيئات يصطف محورا مفصلين على استقامة واحدة فتفقد الذراع درجة حرية. وبالقرب من نقطة التفرّد تسبب تغييرات صغيرة جدًا في الهدف قفزات كبيرة في زوايا المفاصل، وتصبح أداة الحل غير مستقرة عدديًا.
في الأذرع البسيطة يمكنك حل الحركيات العكسية تحليليًا، بإجراء حسابات المثلثات يدويًا. أما في ذراع بستة مفاصل بأطوال وصلات وإزاحات اعتباطية، فتصبح المعادلات ذات الصيغة المغلقة (Closed-form) طويلة وصعبة الاشتقاق. ولهذا توجد أدوات الحل العددية.
يستخدم MoveIt 2 أداة الحل KDL (Kinematics and Dynamics Library) مع OmArm Zero. تقرأ KDL ملف URDF وتبني السلسلة الحركية، أي تتابع الوصلات والمفاصل من القاعدة إلى القابض. ثم تستخدم طريقة نيوتن-رافسون (Newton-Raphson): تبدأ من تخمين، وتقيس بُعد القابض عن الهدف، وتصحح زوايا المفاصل خطوة بخطوة حتى يصبح الخطأ صغيرًا بما يكفي. لا تبرمج الحركيات العكسية بنفسك؛ فـKDL تحسب زوايا المفاصل من ملف URDF.
# kinematics.yaml -- the entire IK configuration
arm:
kinematics_solver: kdl_kinematics_plugin/KDLKinematicsPlugin
position_only_ik: true
kinematics_solver_search_resolution: 0.005
kinematics_solver_timeout: 0.2
kinematics_solver_attempts: 10
يعني position_only_ik: true أن أداة الحل تطابق موضع القابض في X وY وZ فقط وتتجاهل اتجاهه. تضم مجموعة الذراع arm خمسة مفاصل دوّارة (Revolute)، منها دوران الساعد؛ أما القابض فمجموعة مستقلة. لا تستطيع خمسة مفاصل تحقيق قيم الوضعية الست كلها (X وY وZ وثلاث دورانات) في آن واحد، ولذلك كانت أداة الحل ستفشل مع معظم الأهداف. ويحدد kinematics_solver_timeout مدة كل محاولة بـ 0.2 s.
3. تخطيط المسار الحركي: الطريق إلى الهدف
تعطي الحركيات العكسية زوايا المفاصل عند الهدف. أما المخطِّط (Planner) فيقرر كيف تصل الذراع إليه. لو تحرك كل مفصل مباشرة إلى زاويته المستهدفة في الوقت نفسه، فقد تصطدم الذراع بنفسها أو بالطاولة أو تتجاوز حدود سرعتها. لذلك يجب تخطيط الحركة.

يعمل مخطِّط المسار الحركي على ثلاث مراحل.
تخطيط المسار. إيجاد مسار خالٍ من التصادمات عبر فضاء المفاصل من التهيئة الحالية إلى الهدف. هذه مسألة بحث. تضم مجموعة الذراع خمسة مفاصل، لذا يكون فضاء البحث ذا 5 أبعاد. يأخذ المخطِّط عينات من تهيئات عشوائية، ويفحصها مقابل نموذج التصادم (ملف URDF مع أي عوائق تضيفها إلى المشهد)، ويربط العينات الصالحة في مسار. ويستخدم MoveIt لهذا الغرض مكتبة OMPL (Open Motion Planning Library). والخوارزمية الافتراضية هي RRTConnect (Rapidly-exploring Random Tree): تُنمّي شجرتين عشوائيتين، واحدة من البداية وأخرى من الهدف، ثم تصل بينهما. وهي سريعة وموثوقة للأذرع ذات العدد المتوسط من درجات الحرية.
تنعيم المسار الحركي. المسار الخام الناتج عن RRTConnect تتابع من نقاط الطريق (Waypoints، أي تهيئات وسيطة للمفاصل) بزوايا حادة. وعند الزاوية الحادة يجب أن تتغير السرعة لحظيًا، ما يسبب ارتجاج محركات السيرفو. يطبّق MoveIt خوارزمية TimeOptimalParameterization لتنعيم انعطافات المسار وإضافة معلومات التوقيت. والنتيجة تلتزم بحدود سرعة المفاصل وتسارعها المحددة في joint_limits.yaml.
فحص التصادمات. تُفحص كل تهيئة مأخوذة كعينة مقابل نموذج التصادم الذاتي للروبوت (من ملف SRDF، القسم 6) ومقابل أي أجسام في المشهد. إذا أضفت طاولة كجسم تصادم، يتجنبها المخطِّط.
المخرج رسالة من النوع JointTrajectory: قائمة من نقاط الطريق، لكل منها مواضع المفاصل وسرعاتها وطابع زمني. وهذا ما تنفذه وحدة التحكم.
4. ما الذي يفعله MoveIt 2
يحوّل MoveIt 2 هدف القابض إلى تتابع زمني من زوايا المفاصل. عقدته (Node) المركزية هي move_group: تستقبل طلبات التخطيط، وتخطط الحركة، وترسل النتيجة إلى وحدة التحكم. يجمع MoveIt بين الحركيات العكسية (القسم 2) وتخطيط الحركة (القسم 3) والتنفيذ: فهو يرسل المسار الحركي المخطط إلى وحدة تحكم عبر الإجراء (Action) FollowJointTrajectory ويبلغ بالنجاح أو الفشل. والإجراء في ROS 2 طلب يتضمن هدفًا وتغذية راجعة عن التقدم ونتيجة نهائية، وهو مناسب للحركات التي تستغرق وقتًا.

يدير MoveIt أيضًا مشهد التخطيط (Planning Scene: أجسام التصادم مثل الطاولات أو الجدران أو الروبوتات الأخرى) ومجموعات التخطيط (Planning Groups: مجموعات من المفاصل تخطط لكل منها على حدة، وهي هنا arm وgripper). وتتيح لك إضافته (Plugin) في RViz سحب القابض إلى وضعية مستهدفة والنقر على Plan وExecute.
يعمل MoveIt بالطريقة نفسها مع أي نظام خلف واجهة وحدة التحكم: محاكاة Gazebo، أو عتاد محاكى (Mock Hardware)، أو روبوت حقيقي. ما دام هناك ما يستجيب لأهداف FollowJointTrajectory، فإن MoveIt يخطط المسارات الحركية ويرسلها إليه. وهذا ما يتيح لي استخدام إعداد التخطيط نفسه للمحاكاة وللعتاد الحقيقي.
5. MoveIt Setup Assistant: تهيئة روبوتك
MoveIt Setup Assistant أداة رسومية تقرأ ملف URDF الخاص بك وتولّد حزمة إعداد MoveIt كاملة. بدلًا من كتابة ملفات YAML يدويًا، تنتقل بالنقر بين خطوات معالج إعداد. وفيما يلي الإعدادات المستخدمة لحزمة omarm_zero_moveit_config.
المتطلبات المسبقة
ثبّت MoveIt 2 وأداة Setup Assistant:
sudo apt install ros-jazzy-moveit ros-jazzy-moveit-setup-assistant -y
الخطوة 1: شغّل Setup Assistant
source /opt/ros/jazzy/setup.bash
source ~/omarm_zero_ws/install/setup.bash
ros2 launch moveit_setup_assistant setup_assistant.launch.py

انقر Create New MoveIt Configuration Package. انتقل إلى ملف URDF/Xacro الخاص بك (Xacro هي صيغة الماكرو من الجزء 2 التي تولّد ملف URDF):
~/omarm_zero_ws/src/omarm_zero_description/urdf/omarm_zero.xacro
انقر Load Files. يظهر نموذج الروبوت في العرض ثلاثي الأبعاد (3D). إذا ظهر أبيض بالكامل أو كانت الشبكات (Meshes) مفقودة، فتحقق من أن الحزمة omarm_zero_description مبنية وأن بيئتها محمّلة (Sourced).
الخطوة 2: مصفوفة التصادم الذاتي (Self-Collision Matrix)

انقر Self-Collisions في اللوحة اليسرى، ثم Generate Collision Matrix. يأخذ المساعد عينات من آلاف تهيئات المفاصل العشوائية ويفحص أي أزواج الوصلات لا يمكن أن تتصادم أبدًا. تُعطَّل هذه الأزواج لتسريع التخطيط. في OmArm Zero تُعطَّل 10 أزواج تصادم:
- الوصلات المتجاورة (القاعدة والقاعدة العلوية، والقاعدة العلوية ووصلة الذراع 1، وهكذا).
- أصابع القابض التي لا تصل أبدًا إلى بعض وصلات الذراع.
- إصبعا القابض أحدهما مع الآخر (فهما مصممان ليكونا متقاربين).
الخطوة 3: المفصل الافتراضي (Virtual Joint)

انقر Virtual Joints وأضف واحدًا:
| الحقل | القيمة |
|---|---|
| الاسم | world_joint |
| Child Link | base_link |
| Parent Frame | world |
| النوع | fixed |
يثبّت هذا الروبوت في الإطار العالمي (World Frame). ومن دونه لا يعرف MoveIt أين توجد القاعدة.
الخطوة 4: مجموعات التخطيط (Planning Groups)

انقر Planning Groups وأنشئ مجموعتين. أخطأت في هذه الخطوة مرتين قبل أن تنجح.
المجموعة “arm”:
| الحقل | القيمة |
|---|---|
| الاسم | arm |
| Kinematic Solver | kdl_kinematics_plugin/KDLKinematicsPlugin |
| Group Default Planner | RRTConnect |
| Definition | Kinematic Chain |
| Base Link | base_link |
| Tip Link | gripper_base_1 |
ينشئ هذا سلسلة تمر بالمفاصل من Revolute 1 إلى 5. أما الأسماء Revolute 1 حتى 7 وgripper_base_1 فمصدرها تصدير URDF من Fusion 360 في الجزء 2. ولأن أسماء المفاصل تحتوي على مسافة، تضعها ملفات YAML بين علامتي تنصيص (“Revolute 1”). Revolute 1 هو دوران القاعدة، وRevolute 2 الكتف، وRevolute 3 المرفق، وRevolute 4 دوران الساعد، وRevolute 5 إمالة الرسغ، وRevolute 6 و7 إصبعا القابض. وتتوقف السلسلة عند gripper_base_1، حيث تنتهي الذراع ويبدأ القابض.

المجموعة “gripper”:
| الحقل | القيمة |
|---|---|
| الاسم | gripper |
| Definition | Joints |
| Joints | Revolute 6, Revolute 7 |
يُعرَّف القابض كقائمة من المفاصل بدلًا من سلسلة: فالإصبعان مثبتان جنبًا إلى جنب على gripper_base_1، ولا يُركَّب أي منهما على الآخر.
الخطوة 5: وضعيات الروبوت (Robot Poses)

انقر Robot Poses وعرّف تهيئات مسماة يمكنك استدعاؤها لاحقًا.
“home” (المجموعة: arm):
| المفصل | القيمة (rad) |
|---|---|
| Revolute 1 | 0.8 |
| Revolute 2 | -0.8 |
| Revolute 3 | -1.2 |
| Revolute 4 | -0.8 |
| Revolute 5 | -2.6 |
“open” (المجموعة: gripper):
| المفصل | القيمة (rad) |
|---|---|
| Revolute 6 | -1.57 |
| Revolute 7 | -1.57 |
“closed” (المجموعة: gripper):
| المفصل | القيمة (rad) |
|---|---|
| Revolute 6 | 1.57 |
| Revolute 7 | 1.57 |
استخدم أشرطة التمرير لإيجاد وضعيات تناسب ذراعك. يجب أن تكون “home” وضعية راحة آمنة لا يكون فيها أي مفصل عند حدّه.
الخطوة 6: المستجيب الطرفي (End Effector)

انقر End Effectors وأضف واحدًا:
| الحقل | القيمة |
|---|---|
| الاسم | gripper_eef |
| End Effector Group | gripper |
| Parent Link | gripper_base_1 |
| Parent Group | arm |
يخبر هذا MoveIt بأن القابض هو المستجيب الطرفي لسلسلة الذراع، أي الأداة المثبتة في نهايتها. وعندما تخطط لمجموعة arm، يحرّك MoveIt القابض معها.
الخطوة 7: المفاصل الخاملة (Passive Joints)

انقر Passive Joints. تتيح لك هذه الشاشة تحديد المفاصل التي ليس لها مشغّل (Actuator) خاص بها، أي محرك: مفاصل تتحرك بحرية أو تقودها مفاصل أخرى، مثل العجلة الدوّارة الحرة (Caster). في OmArm Zero، لستة من مفاصل Revolute السبعة محرك سيرفو خاص بها؛ أما Revolute 7 (الإصبع الثاني) فيتبع Revolute 6 عبر تروس القابض. اترك المفاصل السبعة كلها في عمود Active Joints على أي حال، لأن الربط بين الإصبعين يُعالَج برمجيًا (القسم 8). لا تنقل أي مفصل إلى جانب Passive Joints.
الخطوة 8: تعديلات URDF الخاصة بـ ros2_control

انقر ros2_control URDF Mod. تضيف هذه الخطوة الوسم <ros2_control> إلى ملف URDF، وهو الوسم الذي يعرّف واجهة العتاد (إشارات الأوامر والحالة) لكل مفصل. اختر:
| الحقل | القيمة |
|---|---|
| Command Interfaces | position (مُفعَّل) |
| State Interfaces | position (مُفعَّل)، velocity (مُفعَّل) |
يولّد المساعد كود XML يتضمن واجهة أوامر للموضع (position) وواجهتي حالة للموضع والسرعة (position وvelocity) لكل مفصل. وهذا يطابق إعداد JointTrajectoryController: ترسل أوامر الموضع، وتقرأ وحدة التحكم الموضع والسرعة.
لا تحتفظ حزمة OmArm Zero النهائية بأي ملف من هذه الخطوة. يعود MoveIt إلى ملف Xacro من الجزء 2 المذكور في الملف المخفي .setup_assistant داخل الحزمة. ويحتوي ملف Xacro هذا بالفعل على كتلة <ros2_control> مخصصة لـ Gazebo؛ ويوضح القسم 9 كيف يستبدل demo.launch.py إضافة العتاد (Hardware Plugin) الخاصة بها.
الخطوة 9: وحدات التحكم في ROS 2

انقر ROS 2 Controllers. أنشئ وحدتي تحكم:
arm_controller:
| الحقل | القيمة |
|---|---|
| الاسم | arm_controller |
| النوع | joint_trajectory_controller/JointTrajectoryController |
| Joints | Revolute 1, Revolute 2, Revolute 3, Revolute 4, Revolute 5 |
gripper_controller:
| الحقل | القيمة |
|---|---|
| الاسم | gripper_controller |
| النوع | joint_trajectory_controller/JointTrajectoryController |
| Joints | Revolute 6, Revolute 7 |
تستخدم كلتاهما JointTrajectoryController، الذي يستقبل رسائل المسار الحركي ويستكمل القيم بين نقاط الطريق (Interpolation). يقترح Setup Assistant وحدتي تحكم منفصلتين للذراع والقابض. أما حزمة OmArm Zero فتستخدم بدلًا من ذلك وحدة joint_trajectory_controller واحدة للمفاصل السبعة كلها؛ ومع ذلك يتيح لك allow_partial_joints_goal (القسم 7) تحريك الذراع وحدها.
الخطوة 10: وحدات تحكم MoveIt

انقر MoveIt Controllers. يربط هذا مخرجات التخطيط في MoveIt بوحدات تحكم ROS 2 من الخطوة 9. أنشئ إدخالين:
arm_controller:
| الحقل | القيمة |
|---|---|
| الاسم | arm_controller |
| Action Type | FollowJointTrajectory |
| Joints | Revolute 1 إلى Revolute 5 |
gripper_controller:
| الحقل | القيمة |
|---|---|
| الاسم | gripper_controller |
| Action Type | FollowJointTrajectory |
| Joints | Revolute 6, Revolute 7 |
عندما يخطط MoveIt مسارًا حركيًا، يرسل هدف إجراء FollowJointTrajectory إلى وحدة التحكم المطابقة. تنفذ وحدة التحكم المسار الحركي وتبلغ بالنجاح أو الفشل. وفي الحزمة، يذكر الملف moveit_controllers.yaml مرة أخرى وحدة joint_trajectory_controller الوحيدة (القسم 7).
الخطوة 11: الإدراك (Perception)

انقر Perception. تُهيّئ هذه الشاشة مستشعرات 3D (كاميرات العمق أو مستشعرات السحابة النقطية) لتجنب التصادمات أثناء التخطيط. لا تملك OmArm Zero مستشعر 3D بعد؛ وستأتي الكاميرا في الجزء 4. اضبط القائمة المنسدلة على لا يوجد.
الخطوة 12: ملفات التشغيل (Launch Files)

انقر Launch Files. يجب أن تكون ملفات التشغيل الثمانية كلها مُفعَّلة:
- Robot State Publisher Launch.
- RViz Launch and Config.
- MoveGroup Launch.
- Static TF Launch.
- Spawn Controllers Launch.
- Demo Launch.
- Setup Assistant Launch.
- Warehouse DB Launch.
يشغّل demo.launch.py برنامج MoveIt مع عتاد محاكى (محركات محاكاة، القسم 9)، ويعيد setup_assistant.launch.py فتح المساعد لإجراء التغييرات. أما ملفات التشغيل الخاصة بـ Gazebo وبالروبوت الحقيقي (القسمان 10 و14) فقد أُضيفت إلى الحزمة يدويًا.
الخطوة 13: معلومات المؤلف (Author Information)

انقر Author Information وأدخل اسمك وبريدك الإلكتروني. يُكتبان في package.xml بوصفهما بيانات مسؤول صيانة الحزمة (Maintainer).
الخطوة 14: توليد الحزمة

انقر Configuration Files واضبط مسار الإخراج:
~/omarm_zero_ws/src/omarm_zero_moveit_config
انقر Generate Package. ينشئ المساعد الحزمة omarm_zero_moveit_config بكل ملفات الإعداد وملفات التشغيل والملف CMakeLists.txt. وتظهر في الأسفل رسالة خضراء: “Configuration package generated successfully!”.
أعد بناء مساحة العمل (colcon هي أداة البناء في ROS 2):
cd ~/omarm_zero_ws
colcon build
source install/setup.bash
6. فهم ملفات الإعداد
ولّد Setup Assistant الملفات التالية. ستعدّلها عندما يفشل التخطيط أو التنفيذ.
SRDF: الوصف الدلالي للروبوت
ملف SRDF (omarm_zero.srdf) هو رفيق ملف URDF. يصف URDF البنية الفيزيائية: الوصلات، والمفاصل، والشكل الهندسي. أما SRDF فيصف المعنى المبني فوقها: ما المجموعات الموجودة، وأي أزواج التصادم يجب تجاهلها، وما الوضعيات المسماة المتاحة.
<robot name="omarm_zero">
<!-- Fixed to the world -->
<virtual_joint name="world_joint" type="fixed"
parent_frame="world" child_link="base_link" />
<!-- Arm: kinematic chain from base to gripper mount -->
<group name="arm">
<chain base_link="base_link" tip_link="gripper_base_1" />
</group>
<!-- Gripper: two finger joints -->
<group name="gripper">
<joint name="Revolute 6" />
<joint name="Revolute 7" />
</group>
<!-- Gripper is the end effector of the arm -->
<end_effector name="gripper_eef" parent_link="gripper_base_1"
group="gripper" parent_group="arm" />
<!-- Named poses -->
<group_state name="home" group="arm">
<joint name="Revolute 1" value="0.8" />
<joint name="Revolute 2" value="-0.8" />
<joint name="Revolute 3" value="-1.2" />
<joint name="Revolute 4" value="-0.8" />
<joint name="Revolute 5" value="-2.6" />
</group_state>
<!-- Collision pairs that can never collide -->
<disable_collisions link1="base_link" link2="base_link_upper_1"
reason="Adjacent" />
<!-- ... 9 more pairs ... -->
</robot>
kinematics.yaml
يحدد أداة حل الحركيات العكسية لكل مجموعة تخطيط. تحصل مجموعة arm على KDL. ولا يحتاج القابض إلى أداة حل للحركيات العكسية، لأنك تتحكم فيه بقيم المفاصل (open/closed)، لا بالوضعية.
joint_limits.yaml
يستبدل حدود المفاصل الواردة في URDF أو يكمّلها. ويستخدمها MoveIt أثناء التخطيط لإبقاء المسارات الحركية ضمن حدود آمنة.
joint_limits:
"Revolute 1":
has_position_limits: true
min_position: -0.0005 # epsilon below 0
max_position: 3.14 # pi
max_velocity: 0.2 # rad/s
max_acceleration: 0.6 # rad/s^2
"Revolute 2":
min_position: -3.14 # -pi
max_position: 0.0005 # epsilon above 0
max_velocity: 0.2
max_acceleration: 0.6
# ... same pattern for Revolute 3-5
"Revolute 6":
min_position: -1.6 # covers open (-1.57) and closed (1.57)
max_position: 1.6
max_velocity: 4.0 # gripper: full servo speed
max_acceleration: 8.0
تمنع قيم epsilon (هامش صغير جدًا قدره 0.0005 rad) عند الحدود المخطِّطَ من أن يعلق تمامًا عند حدّ المفصل، وهو ما يسبب مشكلات عددية. خسرتُ عصر يوم كامل بسبب ذلك: فمن دون epsilon كان المحلِّل (Solver) يفشل أحيانًا عند الحدود دون أن يعطي أي رسالة خطأ مفيدة.
لماذا تقتصر المفاصل من Revolute 2 إلى 5 على المجال [-pi, 0] بينما مجال Revolute 1 هو [0, pi]؟ يعود ذلك إلى تعريفات محاور المفاصل في ملف URDF وإلى طريقة نمذجة الذراع في Fusion 360. فمحاور المفاصل تشير إلى اتجاهات مختلفة، ولذلك يختلف اتجاه الدوران الموجب من مفصل إلى آخر.
ompl_planning.yaml
يضبط هذا الملف خوارزميات تخطيط الحركة. تتوفر للذراع ثلاثة مخطِّطات:
planner_configs:
RRTConnectkConfigDefault:
type: geometric::RRTConnect # default, fast, bidirectional
RRTstarkConfigDefault:
type: geometric::RRTstar # optimal but slower
PRMkConfigDefault:
type: geometric::PRM # probabilistic roadmap
arm:
planner_configs:
- RRTConnectkConfigDefault
- RRTstarkConfigDefault
- PRMkConfigDefault
RRTConnect (القسم 3) هو المخطِّط الافتراضي: سريع، لكن المسار الناتج ليس الأقصر.
RRTstar يجد مسارات أقصر لكنه يستغرق وقتًا أطول. استخدمه عندما تكون جودة المسار أهم من سرعة التخطيط.
PRM (خريطة الطريق الاحتمالية، Probabilistic Roadmap) يبني خريطة طريق لفضاء المفاصل: شبكة من أوضاع الذراع الخالية من التصادم والوصلات بينها. افتراضيًا يبني MoveIt خريطة جديدة لكل طلب. ومع الإعداد multi_query_planning_enabled: true يحتفظ بالخريطة، فتصبح الطلبات اللاحقة في البيئة نفسها أسرع.
7. إعداد وحدات التحكم (Controllers)
يتولى ملفا إعداد أمر وحدات التحكم (Controllers): الملف ros2_controllers.yaml لوحدات تحكم ros2_control نفسها، والملف moveit_controllers.yaml لواجهة MoveIt معها. أما ros2_control فهو إطار العمل في ROS 2 الذي يربط وحدات التحكم بالعتاد، أو بالمحاكاة، عبر واجهة عتاد (Hardware Interface).
ros2_controllers.yaml
controller_manager:
ros__parameters:
update_rate: 100 # Hz
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
joint_trajectory_controller:
type: joint_trajectory_controller/JointTrajectoryController
joint_trajectory_controller:
ros__parameters:
joints:
- "Revolute 1"
- "Revolute 2"
- "Revolute 3"
- "Revolute 4"
- "Revolute 5"
- "Revolute 6"
- "Revolute 7"
command_interfaces:
- position
state_interfaces:
- position
- velocity
allow_partial_joints_goal: true
يحمّل controller_manager وحدتي تحكم هما joint_state_broadcaster وjoint_trajectory_controller (وليس التقسيم إلى ذراع وقابض الوارد في الخطوة 9).
Joint State Broadcaster يقرأ مواضع المفاصل وسرعاتها من واجهة العتاد وينشرها على الموضوع (Topic) /joint_states. وكل عقدة (Node) تحتاج إلى وضع الذراع الحالي (RViz، MoveIt، TF2) تشترك في هذا الموضوع. أما TF2 فهي مكتبة ROS 2 التي تدير شجرة TF من القسم 2.
Joint Trajectory Controller يستقبل رسائل JointTrajectory ويُجري استيفاءً (Interpolation) بين نقاط المسار (Waypoints) بتردد 100 Hz، ويرسل أوامر الموضع إلى واجهة العتاد. ويعني الإعداد allow_partial_joints_goal: true أنه يمكنك إرسال مسار للذراع وحدها (5 مفاصل) دون مفاصل القابض.
moveit_controllers.yaml
moveit_simple_controller_manager:
controller_names:
- joint_trajectory_controller
joint_trajectory_controller:
type: FollowJointTrajectory
action_ns: follow_joint_trajectory
default: true
joints:
- "Revolute 1"
- "Revolute 2"
- "Revolute 3"
- "Revolute 4"
- "Revolute 5"
- "Revolute 6"
- "Revolute 7"
يخبر هذا الملف MoveIt بكيفية الوصول إلى وحدة التحكم. يرسل MoveIt هدف إجراء (Action Goal) من النوع FollowJointTrajectory إلى /joint_trajectory_controller/follow_joint_trajectory. تنفّذ وحدة التحكم المسار ثم تعيد نتيجة (نجاح أو فشل).

8. القابض والمفاصل المحاكية (Mimic Joints)
يمتلك قابض OmArm Zero إصبعين يحرّكهما محرك سيرفو واحد من نوع SG90 عبر زوج من التروس المطبوعة. في ملف URDF هما Revolute 6 (الإصبع الأيسر) وRevolute 7 (الإصبع الأيمن)، ولن ترغب أبدًا في تحريك كلٍّ منهما على حدة. ميكانيكيًا، يعكس الإصبع الثاني حركة الأول عبر التروس. وبرمجيًا، يحصل مفصلا الإصبعين على القيمة نفسها، مثل 1.57 rad للوضع “closed”، بدلًا من استخدام وسم URDF <mimic> (مفصل mimic ينسخ زاوية مفصل آخر، مع إمكانية ضربها بمعامل). في ملف SRDF، يقع المفصلان كلاهما ضمن مجموعة التخطيط “gripper”.
في الجزء 2 بنيتُ عقدة gripper_mimic_bridge تنسخ كل أمر يصل إلى Revolute 6 إلى Revolute 7؛ ولأن محور المفصل معكوس، يدور الإصبع الثاني في الاتجاه المعاكس. أما مع MoveIt فالإعداد أبسط: الوضعيتان المسمّاتان “open” و“closed” تحددان قيمتي المفصلين كليهما، ووحدة التحكم بالمسار تحرّكهما معًا.
<!-- Both fingers get the same value -->
<group_state name="closed" group="gripper">
<joint name="Revolute 6" value="1.57" />
<joint name="Revolute 7" value="1.57" />
</group_state>
لماذا لا نستخدم مفصل mimic في URDF؟ لأن Gazebo وros2_control يتعاملان مع مفاصل mimic بطرق مختلفة باختلاف الإصدارات. جرّبتُ ذلك أولًا ثم استسلمت بعد يومين من تتبع الأخطاء. إرسال الأوامر إلى المفصلين مباشرة أكثر موثوقية وأسهل في استكشاف الأخطاء وإصلاحها.
9. عرض MoveIt التجريبي: عتاد محاكى (Mock Hardware)
يشغّل العرض التجريبي (Demo) لـ MoveIt سلسلة التخطيط كاملة مع عتاد وهمي (Mock Hardware): بلا Gazebo وبلا روبوت حقيقي. لا يحتوي العتاد الوهمي على محركات؛ بل يعيد كل موضع يُطلب منه على أنه الموضع الحالي. وهذه أسرع طريقة للتحقق من إعدادات MoveIt لديك.
شغّل العرض التجريبي
cd ~/omarm_zero_ws
source install/setup.bash
ros2 launch omarm_zero_moveit_config demo.launch.py

ماذا يفعل demo.launch.py
يحمّل demo.launch.py ملف URDF ثم يستبدل إضافة العتاد (Hardware Plugin) فيه. يحدد ملف URDF الإضافة gz_ros2_control/GazeboSimSystem (لـ Gazebo)، ويستبدلها العرض التجريبي بـ mock_components/GenericSystem، وهي واجهة عتاد محاكاة مدمجة في ros2_control. لا حاجة إلى Gazebo.
يُرتَّب تسلسل التشغيل باستخدام معالجات الأحداث (Event Handlers)، وهي قواعد في ملف التشغيل تستجيب لأحداث مثل بدء عملية أو انتهائها:
1. robot_state_publisher (loads URDF, publishes TF) 2. ros2_control_node (manages hardware interface) 3. joint_state_broadcaster (waits for ros2_control, then publishes /joint_states) 4. joint_trajectory_controller (waits for JSB, then accepts trajectories) 5. move_group + RViz (waits for controller, then starts planning)
تنتظر كل خطوة انتهاء الخطوة السابقة قبل أن تبدأ. يمنع ذلك حالة التسابق (Race Condition): أي أن تبدأ عمليتان بترتيب عشوائي، كأن يبدأ MoveIt التخطيط قبل أن توجد وحدة التحكم.
أول خطة حركة لك

ترى في RViz الروبوت مع مؤشر تفاعلي (Interactive Marker، وهو مقبض يمكن سحبه) على القابض. الذراع البرتقالية هي حالة الهدف. وبعد الضغط على Plan، يعرض RViz المسار المخطَّط بتحريك ذراع بنفسجية شبه شفافة على طوله.
- اسحب المؤشر الموجود على القابض إلى موضع جديد.
- في لوحة MotionPlanning، انقر على Plan.
- شاهد المسار المتحرك.
- انقر Execute. تتحرك الذراع الوهمية في RViz على طول المسار المخطَّط، دون أي محاكاة فيزيائية.

جرّب الوضعيات المسمّاة
في لوحة MotionPlanning ضمن تبويب Planning، افتح القائمة المنسدلة “Goal State”. اختر home، ثم انقر على Plan وExecute. انتقل إلى مجموعة التخطيط “gripper” وجرّب open و closed.
افحص مخطط عقد ROS 2
افتح طرفية جديدة ونفّذ:
ros2 node list
ros2 topic list
rqt_graph

يوضح المخطط كيف ترتبط العقد ببعضها:
- ترسل العقدة move_group المسارات إلى joint_trajectory_controller.
- يقرأ joint_trajectory_controller حالات المفاصل عبر ros2_control_node.
- ينشر joint_state_broadcaster الموضوع /joint_states.
- تشترك العقدة robot_state_publisher في /joint_states وتنشر /tf.
- تشترك العقدة rviz2 في /tf و/joint_states ومشهد التخطيط (Planning Scene).
10. MoveIt + محاكاة Gazebo
يستخدم العرض التجريبي عتادًا وهميًا. وللحصول على الفيزياء (الجاذبية، القصور الذاتي، التلامس)، شغّل MoveIt فوق محاكاة Gazebo من الجزء 2.
شغّل MoveIt مع Gazebo
ros2 launch omarm_zero_moveit_config gazebo_moveit.launch.py

يبدأ ملف التشغيل هذا ثلاث عمليات بالتتابع:
- t = 0 s: محاكاة Gazebo (من omarm_zero_description/gazebo.launch.py).
- t = 4 s: move_group مع use_sim_time: true (تستخدم move_group ساعة المحاكاة في Gazebo بدلًا من ساعة الحاسوب).
- t = 8 s: RViz مع إضافة MoveIt.
تمنح فترات التأخير هذه Gazebo وقتًا لإنشاء الروبوت في المحاكاة قبل أن يتصل MoveIt. على حاسوب أبطأ زِد هذه القيم: فهي فترتا TimerAction (تأخيرا البدء) في gazebo_moveit.launch.py.
الاختلافات عن العرض التجريبي
الفرق هو الفيزياء. في العرض التجريبي تتبع الذراع الوهمية الخطة بدقة تامة. أما في Gazebo فللذراع كتلة وقصور ذاتي، وعلى وحدة التحكم أن تثبّتها في مواجهة الجاذبية بينما تتبع المسار المخطَّط. خطّط لحركة كبيرة وراقب الذراع وهي تتبع المسار في نافذة Gazebo.
إذا عطّلت وحدات التحكم، تنهار الذراع تحت وزنها. تحافظ وحدة JointTrajectoryController على الموضع حتى عندما لا يكون هناك مسار قيد التنفيذ.
خطّط ونفّذ في Gazebo
خطّط ونفّذ كما في العرض التجريبي (القسم 9). تتحرك الذراع في RViz وفي Gazebo في الوقت نفسه.

انتقل إلى مجموعة التخطيط gripper لاختبار الفتح والإغلاق. اختر “closed” أو “open” بوصفها حالة الهدف ثم نفّذ.

مخطط عقد ROS 2 مع Gazebo أكبر من مخطط العرض التجريبي. تنقل العقدة ros_gz_bridge الرسائل بين Gazebo وROS 2، ويدير gz_ros2_control واجهة العتاد المحاكاة.

11. واجهة MoveIt البرمجية بلغة Python: برمجة الحركة بالسكربتات
يُستخدم RViz للاختبار اليدوي. أما للأتمتة فترسل الأهداف من الكود. ويوضح البرنامج النصي plan_joint_goal.py في الحزمة omarm_zero_moveit_config كيفية ذلك.

الاستخدام
# Plan only (don't execute)
ros2 run omarm_zero_moveit_config plan_joint_goal.py \
--group arm --target 1.0 -1.0 -1.5 -0.8 -2.0
# Plan and execute
ros2 run omarm_zero_moveit_config plan_joint_goal.py \
--group arm --target 1.0 -1.0 -1.5 -0.8 -2.0 --execute
# Close the gripper
ros2 run omarm_zero_moveit_config plan_joint_goal.py \
--group gripper --target 1.57 1.57 --execute
# Open the gripper
ros2 run omarm_zero_moveit_config plan_joint_goal.py \
--group gripper --target -1.57 -1.57 --execute
تتوقع المجموعة arm عدد 5 قيم (Revolute 1 إلى 5)، والمجموعة gripper عدد 2 (Revolute 6 و7).
آلية العمل
يستخدم البرنامج النصي إجراء MoveGroup، وهو الواجهة الرئيسية في MoveIt:
1. Wait for /joint_states to get the current arm configuration
2. Build a MoveGroup goal with:
- Planning group name ("arm" or "gripper")
- Current state as start state
- Target joint values as goal constraints
- Planning parameters (attempts, time, velocity scaling)
3. Send goal to /move_action (MoveGroup action server)
4. MoveIt plans the trajectory (plan_only: true)
5. If --execute: send the planned trajectory to
/joint_trajectory_controller/follow_joint_trajectory
6. Wait for execution result

المعاملات الرئيسية في البرنامج النصي
req.num_planning_attempts = 5 # try 5 times if planning fails
req.allowed_planning_time = 8.0 # seconds
req.max_velocity_scaling_factor = 0.2 # 20% of max joint velocity
req.max_acceleration_scaling_factor = 0.2
تحصر عوامل التحجيم (Scaling Factors) المسارَ المخطَّط في 20% من حدود السرعة والتسارع المحددة في joint_limits.yaml. في العرض التجريبي وفي Gazebo تتبع وحدة التحكم هذا التوقيت الأبطأ. أما على الذراع الحقيقية فتغيّر عوامل التحجيم المدة المخطَّطة، وينتظر الجسر (Bridge) هذه المدة قبل أن يُبلغ بالنجاح (القسم 12). وتأتي سرعة السيرفو نفسها من إعداد SPEED في البرنامج الثابت (القسم 14).
توسيع البرنامج النصي
يرسل plan_joint_goal.py أهدافًا في فضاء المفاصل. أما للأهداف الديكارتية (تحريك القابض إلى الموضع X، Y، Z) فترسل هدف وضعية (Pose Goal) عبر إجراء MoveGroup نفسه؛ وهذا ما يفعله برنامج الالتقاط والوضع في الجزء 4. ومن البدائل واجهة moveit_py البرمجية بلغة Python وواجهة MoveGroupInterface البرمجية بلغة C++. ويبقى النمط نفسه: هدف، ثم تخطيط، ثم تنفيذ.
12. بنية الجسر: من MoveIt إلى العتاد الحقيقي
المسار الذي يخططه MoveIt على حاسوب Linux لا بد أن يصل بعد ذلك إلى محركات السيرفو المتصلة بـ ESP32. الطريقة المعتادة في ROS 2 هي واجهة عتاد ros2_control: إضافة بلغة C++ تُصرَّف إلى مكتبة، مع قدر كبير من كود الإعداد الثابت. ولأن الأمر يتعلق بمحركات سيرفو للهواة عبر وصلة تسلسلية، تستخدم OmArm Zero جسرًا أبسط بلغة Python يوفر لـ MoveIt إجراء FollowJointTrajectory نفسه الذي توفره JointTrajectoryController، دون ros2_control.
استغرق مني الوصول إلى هذا الحل أسبوعًا، وكان الجواب أبسط مما توقعت.
جسر Python:
- يعمل بالطريقة نفسها تمامًا من منظور MoveIt (واجهة الإجراء نفسها)؛
- أسهل في التعديل وتتبع الأخطاء (Python وليس C++)؛
- يتعامل مباشرة مع بروتوكول الأوامر الخاص بـ ESP32 (سلاسل نصية تسلسلية، وJSON عبر HTTP).
الثمن المقابل: لا ضمانات للزمن الحقيقي (Real-Time) ولا تغذية راجعة للموضع. فمحركات السيرفو المخصصة للهواة لا تحتوي على مُرمِّزات (Encoders، وهي مستشعرات تقيس الزاوية الفعلية للعمود)، لذلك لن توفر واجهة C++ تغذية راجعة هي الأخرى.

عقد الجسر
تحل عقدتان بلغة Python محل منظومة ros2_control عند العمل مع العتاد الحقيقي.
follow_joint_trajectory_bridge هو خادم إجراءات (Action Server) له الواجهة نفسها التي لـ JointTrajectoryController. يرسل إليه MoveIt أهداف FollowJointTrajectory. يقبل الجسر الهدف، وينشر المسار على موضوع (Topic)، وينتظر المدة المخطَّطة ثم يُبلغ بالنجاح. ولا يتحقق من مواضع محركات السيرفو، لذلك يُبلغ MoveIt بالنجاح أيضًا حتى عندما يكون أحد المفاصل عالقًا أو ينقطع الاتصال بـ ESP32؛ وعندها يكتفي hardware_bridge_node بتسجيل تحذير.
MoveIt → FollowJointTrajectory action goal
→ follow_joint_trajectory_bridge (action server)
→ publishes JointTrajectory on /joint_trajectory_controller/joint_trajectory
hardware_bridge_node يشترك في موضوع المسار، ويأخذ نقطة المسار الأخيرة فقط، ويحوّل زوايا المفاصل من الراديان إلى درجات السيرفو، ثم يرسل الأمر MOVE إلى ESP32. بعدها يحرّك البرنامج الثابت كل سيرفو نحو هدفه بخطوات صغيرة. تصل الذراع الحقيقية إلى الهدف المخطَّط، لكنها لا تتبع المسار المخطَّط بين البداية والنهاية. ولا ضرر من ذلك عادةً في غياب العوائق. أما مع وجود عوائق في مشهد التخطيط، ففحص التصادم يشمل الخطة وليس الحركة الفعلية.
/joint_trajectory_controller/joint_trajectory
→ hardware_bridge_node
→ ServoCalibration: radians → degrees
→ ResourceManager: "MOVE:90,45,135,90,0,90"
→ ESP32 (serial or HTTP)
ينشر جسر العتاد أيضًا حالة صدى (Echo State) على /joint_states بتردد 50 Hz. وهذا تحكم بحلقة مفتوحة (Open-Loop): فهو ينشر ما أمر به، لا ما نفّذته محركات السيرفو فعلًا. ولأن محركات السيرفو لا تحتوي على مُرمِّزات، لا يمكن قراءة الموضع الفعلي.
خيارات النقل
يدعم ResourceManager (الخطوة في المخطط أعلاه التي ترسل السلسلة النصية MOVE إلى ESP32) وسيلتي نقل:
التسلسلي (Serial): يرسل MOVE:a1,a2,a3,a4,a5,a6\n بمعدل 115200 baud (بت في الثانية) إلى /dev/ttyUSB0. يقرأ ESP32 السلسلة النصية، ويستخرج الزوايا الست ثم يمررها إلى PCA9685، وهي لوحة قيادة سيرفو ذات 16 قناة.
HTTP: يرسل كائن JSON {“cmd”: “MOVE:a1,a2,a3,a4,a5,a6”} إلى /api/command على ESP32، على العنوان المحدد في http.host داخل hardware.yaml. يحتاج HTTP إلى برنامج ثابت يوفّر نقطة وصول واي فاي (Wi-Fi)، لأن omarm_zero_ros2_serial.ino لا يدعم الواي فاي. حمّل على اللوحة برنامج الويب الثابت OmArmZero_LittleFS.ino من المجلد src/firmware/ في مساحة العمل. تستخدم نقطة الوصول فيه العنوان الافتراضي لـ ESP32 وهو 192.168.4.1، وهو القيمة المضبوطة مسبقًا لـ http.host في hardware.yaml، كما أن الأمر HOME فيه يحرّك جميع محركات السيرفو إلى 90° مثل البرنامج الثابت التسلسلي. أما البرنامج الثابت المرفق بحزمة الجزء 1 فيستخدم العنوان 10.10.10.1 ووضعية HOME فيه عند 0°، لذلك لا تستخدمه إلا بعد ضبط http.host على 10.10.10.1. صِل الحاسوب أولًا بشبكة الواي فاي OmArmZero. اختر HTTP عندما يكون استخدام كابل USB بين الحاسوب وESP32 غير عملي.
# Launch with serial transport (default)
ros2 launch omarm_zero_moveit_config real_moveit.launch.py transport:=serial
# Launch with HTTP transport
ros2 launch omarm_zero_moveit_config real_moveit.launch.py transport:=http
13. معايرة محركات السيرفو
يحوّل الجسر زوايا المفاصل من الراديان (MoveIt) إلى درجات السيرفو (ESP32). ويحتاج هذا التحويل إلى معايرة لأن موضع المنتصف في السيرفو لا يطابق موضع الصفر في URDF.
معادلة المعايرة
servo_deg = 90 + direction * (joint_rad - zero_rad) * (180/pi) * scale
لكل مفصل ثلاثة معاملات معايرة، وتُقيَّد النتيجة ضمن مجال محدد.
| المعامل | المعنى |
|---|---|
| zero_rad | زاوية المفصل في URDF التي يكون عندها السيرفو في موضع المنتصف 90° |
| direction | +1 أو -1، لأن بعض محركات السيرفو مركّبة بالاتجاه المعاكس |
| scale | عادةً 1.0، لكن يمكنه تعويض نسب التروس |
| التقييد (Clamping) | يُقيَّد الخرج في المجال من 0° إلى 180° |
قيم المعايرة الافتراضية
# hardware.yaml, servo_calibration (lists in short form)
# one entry per servo: J1 to J6 = Revolute 1 to 6
servo_calibration:
zero_rad: [1.57, -1.57, -1.57, -1.57, -1.57, 0.0] # J1 pi/2, J2-J5 -pi/2, J6 0
direction: [1, 1, 1, 1, 1, -1] # J6: gripper servo is mirrored
scale: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
servo_min_deg: [0, 0, 0, 0, 0, 0]
servo_max_deg: [180, 180, 180, 180, 180, 180]
لا يوجد إدخال لـ Revolute 7 لأنه لا يملك سيرفو خاصًا به. ولا توجد قنوات PCA9685 في hardware.yaml: إذ يربط البرنامج الثابت المفاصل من J1 إلى J6 بالقنوات 0 و3 و4 و7 و8 و11، وفق التوصيلات من الجزء 1. وتمنع الفجوات بين القنوات تكدّس كابلات السيرفو في جانب واحد من اللوحة.

لماذا يهم zero_rad
في URDF، يعتمد موضع الصفر لكل مفصل على طريقة تجميع نموذج CAD. يتحرك Revolute 1 (دوران القاعدة) من 0 إلى pi في URDF. ومنتصف هذا المجال، أي pi/2، يوجّه الذراع إلى الأمام مباشرة، وهو ما يطابق موضع المنتصف 90° في السيرفو. لذلك zero_rad = pi/2.
تتحرك المفاصل من Revolute 2 إلى 5 من -pi إلى 0. ومنتصف مجالها، أي -pi/2، يقابل موضع المنتصف 90° في السيرفو، لذلك zero_rad = -pi/2.
يستخدم القابض (J6) القيمتين zero_rad = 0 وdirection -1. وضع الفتح (-1.57 rad) يعطي 90 + 90 = 180°، ووضع الإغلاق (1.57 rad) يعطي 90 – 90 = 0°.
ضبط المعايرة
إذا لم تطابق الذراع الحقيقية ما يعرضه RViz:
- شغّل real_moveit.launch.py.
- اضبط الذراع على الوضعية “home” في RViz.
- قارن الذراع الحقيقية بما يعرضه RViz.
- إذا كان أحد المفاصل منزاحًا، فاضبط قيمة zero_rad الخاصة به في hardware.yaml.
- شغّل ملف التشغيل من جديد.
الانحرافات الصغيرة (أقل من 5°) طبيعية بسبب تفاوتات السيرفو ودقة تركيب القطع المطبوعة بالطباعة ثلاثية الأبعاد (3D). في ذراعي، سيرفو القابض وحده يدور عكس اتجاه نموذج RViz، ولهذا تكون قيمة direction للمفصل J6 هي -1.
14. تشغيل الروبوت الحقيقي: كل شيء معًا
يخطط MoveIt مسارًا على حاسوبك، ويحوّله الجسر إلى أوامر للسيرفو، فتتحرك الذراع الحقيقية.
المتطلبات المسبقة
- لوحة ESP32 موصولة عبر USB (أو عبر واي فاي عند استخدام النقل عبر HTTP).
- لوحة ESP32 محمّل عليها البرنامج الثابت omarm_zero_ros2_serial.ino (ولاستخدام HTTP: برنامج ثابت يوفّر نقطة وصول واي فاي، راجع القسم 12).
- محركات سيرفو تُغذّى من مزود طاقة منفصل بجهد 5 V وتيار مقنن لا يقل عن 10 A (راجع الجزء 1)، ولا تُغذّى أبدًا من USB.
حمّل البرنامج الثابت على اللوحة
افتح omarm_zero_ros2_serial.ino في Arduino IDE. ستجده في src/firmware/ داخل مساحة العمل، وفي firmware/ros2_serial/ على GitHub. اختر اللوحة ESP32 Dev Module كما في الجزء 1 ثم ارفع البرنامج. يقبل البرنامج الثابت الأوامر التالية عبر الاتصال التسلسلي بمعدل 115200 baud:
STATUS → returns JSON with current servo positions HOME → all servos to 90 degrees ZERO → all servos to 0 degrees MOVE:a1,...,a6 → set target angles (0-180) SPEED:n → max. step per 80 ms in degrees (1-20)
يحدد SPEED:n أكبر خطوة يقطعها السيرفو في كل تحديث مدته 80 ms. القيمة 1 هي الأبطأ (12.5°/s)، و20 هي الأسرع (250°/s)، ويبدأ البرنامج الثابت بالقيمة 5 (62.5°/s). أما HOME فيضع كل مفصل في منتصف مجاله، حيث يكون السيرفو عند 90° (zero_rad، القسم 13). وهذا الوضع ليس الوضعية “home” في MoveIt من الخطوة 5.
اختبر الاتصال:
# Check the serial port
ls /dev/ttyUSB*
# Send a test command (install screen if needed)
screen /dev/ttyUSB0 115200
# Type: STATUS
# Expected: {"status":"ok","pos":[...]} with six servo angles
# Exit screen: Ctrl-A, then K, then Y
شغّل MoveIt مع العتاد الحقيقي
ros2 launch omarm_zero_moveit_config real_moveit.launch.py

يشغّل ملف التشغيل ما يلي:
- robot_state_publisher (من URDF إلى TF).
- hardware_bridge_node (جسر تسلسلي أو HTTP إلى ESP32).
- follow_joint_trajectory_bridge (خادم الإجراءات لـ MoveIt).
- move_group (التخطيط في MoveIt).
- rviz2 (العرض المرئي).
إذا كانت قيمة home_on_start هي true (القيمة الافتراضية)، تتحرك الذراع إلى وضعية HOME في البرنامج الثابت (جميع محركات السيرفو عند 90°) فور بدء التشغيل، لذلك أبعِد يديك وأي أجسام عن مدى وصولها. انتظر حتى تستقر قبل التخطيط. يظل الجسر يفترض 0 rad لكل مفصل حتى أول حركة للذراع، لذلك خطّط أولًا لهدف للذراع مثل “home”. فأي أمر للقابض وحده قبل ذلك يعيد استخدام قيم 0 rad المخزنة، فيرسل J1 إلى 0° والمفاصل من J2 إلى J5 إلى 180°.
في rqt_graph، تحل عقدتا الجسر من القسم 12 محل مدير وحدات التحكم (Controller Manager) ووحدات التحكم التابعة له.

خطّط ونفّذ على الذراع الحقيقية
خطّط ونفّذ كما في العرض التجريبي (القسم 9). في المرة الأولى التي نجح فيها ذلك على الذراع الحقيقية، بقيت أحدّق فيها عشر ثوانٍ.

يحرّك البرنامج الثابت omarm_zero_ros2_serial محركات السيرفو خطوة كل 80 ms؛ أما برنامج الويب الثابت في الجزء 1 فكان يستخدم 20 ms. تتحرك الذراع بخطوات صغيرة نحو آخر نقطة في المسار ولا تتبع توقيت المسار (القسم 12).
معاملات التشغيل
# All available parameters
ros2 launch omarm_zero_moveit_config real_moveit.launch.py \
transport:=serial \ # serial or http
config_file:=hardware.yaml \ # calibration config
home_on_start:=true \ # move to home on startup
home_wait_sec:=1.5 \ # wait time after homing
start_at_ros_zero:=false \ # start at ROS zero instead of home
use_rviz:=true # launch RViz
اترك start_at_ros_zero على القيمة false. فهو يرسل 0 rad لكل مفصل عند بدء التشغيل، ما يضع J1 عند 0° والمفاصل من J2 إلى J5 عند 180°: أي عند طرفي مجال السيرفو، حيث تتوقف محركات السيرفو قسرًا (Stall).
استخدام واجهة Python البرمجية مع العتاد الحقيقي
يعمل plan_joint_goal.py مع العتاد الحقيقي دون أي تعديل:
# Move the real arm to a target
ros2 run omarm_zero_moveit_config plan_joint_goal.py \
--group arm --target 1.2 -0.5 -1.0 -1.5 -2.0 --execute
# Open the gripper
ros2 run omarm_zero_moveit_config plan_joint_goal.py \
--group gripper --target -1.57 -1.57 --execute

استكشاف الأخطاء وإصلاحها
الذراع لا تتحرك: تحقق من ناتج ls /dev/ttyUSB*. جرّب screen /dev/ttyUSB0 115200 وأرسل STATUS. إذا لم يصل أي رد، فافحص كابل USB وتغذية ESP32 بالطاقة.
الذراع تتحرك في الاتجاه الخاطئ: اعكس قيمة direction في hardware.yaml للمفصل المعني، من 1 إلى -1 أو العكس.
موضع الذراع لا يطابق RViz: اضبط zero_rad للمفصل غير المتطابق. الانحرافات الصغيرة طبيعية.
فشل التخطيط: راجع مخرجات الطرفية. الأسباب الشائعة: الهدف خارج حدود المفاصل، أو أن الذراع ستصطدم بنفسها، أو أن المحلِّل لا يجد حلًا خلال المهلة الزمنية. تحقق من أن الحدود في joint_limits.yaml تطابق مجال حركة الذراع الفعلية. وإذا كان الهدف قريبًا من حدّ أحد المفاصل، فقد لا يجد المخطِّط مسارًا صالحًا.
حركة متقطعة: في المحاكاة، خفّض max_velocity_scaling_factor في طلب التخطيط؛ ويُفترض أن تكون الحركة سلسة مع القيمة الافتراضية 0.2 (20%). أما على الذراع الحقيقية فحجم الخطوة في كل 80 ms يأتي من قيمة SPEED في البرنامج الثابت (القسم 14)، وليس من عامل التحجيم.
انهيار MoveIt Setup Assistant: تحقق باستخدام check_urdf من أن ملف URDF يُحمَّل دون أخطاء. وإذا كان ملف URDF يستخدم تضمينات Xacro، فشغّل xacro أولًا ثم مرّر ملف URDF الناتج إلى المساعد.
وحدة التحكم غير موجودة (العرض التجريبي أو Gazebo): نفّذ ros2 control list_controllers. إذا كانت القائمة فارغة، فهذا يعني أن مدير وحدات التحكم لم يبدأ؛ افحص ملف التشغيل ومعاملات ros2_control. أما مع العتاد الحقيقي فلا يوجد مدير لوحدات التحكم؛ نفّذ ros2 action list وتحقق من وجود /joint_trajectory_controller/follow_joint_trajectory (الذي يوفّره follow_joint_trajectory_bridge).
الأسئلة الشائعة
هل أحتاج إلى حاسوب سريع لتشغيل MoveIt 2؟ لا. سجّلتُ مقاطع الشاشة لهذا المقال على حاسوبي المحمول Lenovo IdeaPad 320. وفيها خطّط MoveIt حركات الذراع في مدة من 0.04 إلى 0.14 s أثناء تشغيل Gazebo. الجزء الثقيل هو Gazebo: فأثناء تشغيله انخفض معدل RViz من نحو 30 إلى 3 أو 4 إطارات في الثانية، وكانت المحاكاة تعمل بنحو 40% من الزمن الحقيقي. ويوصي الجزء 2 بذاكرة RAM لا تقل عن 8 GB.
هل يمكنني استخدام هذا مع ذراع روبوتية أخرى؟ نعم، باستخدام ملف URDF ومعاملات المعايرة الخاصة بك. يتبع إعداد MoveIt (Setup Assistant، ووحدات التحكم، وملفات التشغيل) النمط نفسه لأي روبوت. أما جسر العتاد فهو خاص بـ ESP32 ولوحة قيادة السيرفو PCA9685، لكن يمكنك تكييفه مع متحكمات دقيقة أخرى.
ما التالي
في الجزء 4، تكتشف كاميرا الأجسام، ويرسل برنامج الالتقاط والوضع موضع كل جسم إلى MoveIt بوصفه هدف وضعية (Pose Goal).
المصادر
الشرح بالفيديو: شاهد على YouTube
