من Fusion 360 إلى توأم رقمي في ROS 2: نموذج URDF وRViz وGazebo مطابقة للذراع الحقيقية (OmArm One، الجزء 2)

Left, the 3D printed OmArm One arm on the bench; right, the same arm in RViz in the same pose

سلسلة OmArm One

تحتاج إلى ذراع OmArm One مكتملة من الجزء 1، أو أي ذراع يتوفر لديك نموذج CAD لها. لا يُحمَّل أي برنامج ثابت (Firmware) على اللوحة في هذا الجزء.

يُحمَّل الفيديو من YouTube (Google)، وقد يضع ملفات تعريف الارتباط (Cookies). سياسة الخصوصية

نسخة الفيديو ومدتها 16 دقيقة: التصدير، وقواعد التجهيز التي تحدد جودته، والنموذج وهو يعمل في RViz وGazebo.

منح الجزء 1 الذراعَ جسمًا وتطبيق ويب، ومنحها الجزء 1.5 يد تحكم للألعاب (Game Controller). وكلاهما يحرّك الذراع مفصلًا تلو الآخر: شريط التمرير يحدد زاوية المفصل، وعصا التحكم تحدد سرعته. كما أنهما يتركان لك الجزء الأصعب: حساب الموضع الذي سينتهي إليه القابض. فست زوايا للمفاصل ليست موضعًا في الفضاء. إذا أردت أن تصل الذراع إلى نقطة على الطاولة، فعليك حل المسألة في الاتجاه العكسي، ولا أشرطة التمرير ولا عصا التحكم تفعل ذلك نيابةً عنك. في الجزء 1 استبعدت الحركيات العكسية (حساب زوايا المفاصل انطلاقًا من نقطة هدف) من تطبيق الويب عن قصد. فمن دون مخطِّط حركة يفحص المسار أولًا بحثًا عن التصادمات، كانت نسخة تعمل نصف عمل ستكون أسوأ من عدم وجودها أصلًا.

تستطيع البرمجيات حساب موضع القابض من زوايا المفاصل، وحساب زوايا المفاصل من نقطة هدف، إذا توفر لديها نموذج للروبوت. في ROS 2 يُكتب هذا النموذج بصيغة ملفات تُسمى URDF. وبمجرد وجوده، يعرض RViz الروبوت، ويضعه Gazebo في عالم فيزيائي، وفي الجزء 3 يخطط MoveIt حركاته.

النموذج المُصدَّر من CAD يصف تجميعة CAD، والروبوت الحقيقي يختلف عنه بفروق تبدو صغيرة على الشاشة لكنها مهمة عمليًا. في هذه الذراع: مفصل يدور في الاتجاه الخاطئ، وموضع صفر منزاح بمقدار 45 درجة، وحد سرعة أعلى بتسع عشرة مرة من السرعة القصوى للسيرفو عند 5 V. فيما يلي التصدير وكل تعديل أجريته بعده، مع الأسباب والحسابات، لتتمكن من تكراره على ذراعك.

Diagram of the changes between the exported package and the corrected one: two bench corrections in section 8 and seven changes for simulation and planning in section 9

نظرة سريعة

ما ستبنيه: حزمة ROS 2 تصف ذراع OmArm One بدقة كافية لتتمكن البرمجيات من التعامل معها. تُصدِّر النموذج من CAD، وتدقق النتيجة، وتصحح اتجاهات المفاصل ومواضع الصفر حتى يطابق النموذج الذراع الموجودة على طاولة عملك. بعد ذلك تجعله صالحًا فيزيائيًا للمحاكي، وتنتهي بالذراع منتصبة في RViz وتعمل في Gazebo. ووسيط (Argument) واحد يبدّل الوصف نفسه بين المحاكاة والعتاد الحقيقي. لرؤية الذراع على الشاشة دون تصديرها بنفسك، استخدم الحزمة الجاهزة: المسار A في القسم 4، ثم القسم 11.

ما ستتعلمه: ما هو ملف URDF، والمعادلة الوحيدة التي تقف وراء كل مفصل. كيف تجهّز تجميعة CAD لتكون نتيجة التصدير قابلة للاستخدام، وكيف تقرأ النموذج المُصدَّر بعين ناقدة. الفرق بين صفر المفصل واتجاهه ومعامل مقياسه (Scale)، ولماذا لا يستطيع قياس واحد أن يفصل بينها. كيف تُدخل إزاحة مَقيسة في هندسة المفصل: رقم واحد لمفصل يقع على محور إحداثي، ودوران مركّب لمفصل مائل. لماذا قد يفشل في Gazebo نموذجٌ يقبله RViz.

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

التكلفة الإجمالية: 0 € للقطع، لأن كل شيء في هذا الجزء يعمل على الحاسوب. الحزمة الجاهزة وسكربت التدقيق مرفقان مع التنزيل المدفوع للجزء 2 (انظر الملفات التي ستحتاجها).

مستوى الصعوبة: متوسط. لا رياضيات روبوتية تتجاوز متطابقة مصفوفات واحدة مكتوبة بالتفصيل، واستدعاء دالة من مكتبة لحالة المحور المائل. يُفترض أن تكون مرتاحًا في استخدام الطرفية (Terminal).

السلامة: معظم العمل في هذا الجزء يتم على الحاسوب. تحتاج إلى الذراع موصولة بالطاقة فقط لقياسَي المعايرة في القسم 8 وللفحوصات في القسمين 11 و13. حرّك مفصلًا واحدًا في كل مرة، وأبقِ منطقة العمل خالية. حرّك الذراع باستخدام تطبيق الويب أو يد التحكم من الجزء 1.5؛ فلا شيء في هذا الجزء يرسل أوامر إلى محركات السيرفو.

ما تحتاج إليه: Ubuntu 24.04 مع ROS 2 Jazzy، ونموذج CAD لذراع. نموذج OmArm One مرفق مع الحزمة، أو استخدم نموذجك الخاص. ذراع مكتملة من الجزء 1 لإجراء القياسات والفحوصات المذكورة في فقرة السلامة.

الملفات التي ستحتاجها: ‏ omarm_description على شكل حزمة، مع سكربت التدقيق، ضمن تنزيل الجزء 2، الذي يتضمن أيضًا هذا الدليل بصيغة PDF.

1. ما هو التوأم الرقمي، وما الذي لا يُعدّ كذلك

في هذا الجزء، يعني التوأم الرقمي وصفًا لهندسة الروبوت وبنية مفاصله وكتلته، دقيقًا بما يكفي ليعمل عليه مخطِّط الحركة. إنه ليس صورة مُصيَّرة (Rendering)، ولا يحل محل البرنامج الثابت: فمحرك الحركة لا يزال يعمل على ESP32 كما في الجزء 1.

يُستخدم التوأم الرقمي على ثلاث مراحل، ويغطي هذا الجزء المرحلتين الأوليين.

يمكنك رؤية حالة الروبوت. يرسم RViz النموذج عند أي زوايا مفاصل تزوده بها. زوّده بالزوايا الحقيقية للذراع فترى صورة حية لآلة لا تملك شاشة. وزوّده بقيم أشرطة التمرير فتستطيع فحص النموذج نفسه.

يمكنك محاكاته. يأخذ Gazebo النموذج نفسه، ويضيف الجاذبية والتلامس وحلقة فيزيائية، ويتيح لك اختبار حركة كانت ستصطدم بالطاولة دون إتلاف أي شيء. تستخدم هذه المرحلة قيم الكتلة والقصور الذاتي (Inertia) من النموذج.

يمكنك التخطيط باستخدامه. يأخذ MoveIt النموذج ويجيب عن السؤال العكسي: ما زوايا المفاصل التي تضع القابض هناك، وما المسار الذي يوصله إليه دون تصادم. التخطيط هو موضوع الجزء 3، وهو سبب فحص هذا الجزء للنموذج بهذه العناية: فمخطِّط الحركة يثق بالنموذج ثقة تامة. إذا قال نموذجك إن الذراع أقصر بمقدار 10 mm مما هي عليه، فسيخطط المخطِّط مسارًا يدفع القابض إلى داخل الطاولة، ولن تُبلغ أي طبقة برمجية عن خطأ.

حدود: يعرف التوأم الرقمي المكان الذي طلبت من الذراع الذهاب إليه، ولا يعرف أبدًا مكانها الفعلي. لا تحتوي OmArm One على مشفّرات (Encoders، أي مستشعرات تقيس الزاوية الحقيقية للمفصل). يشرح الجزء 1 ما يفعله البرنامج الثابت حيال ذلك. قد يكون النموذج صحيحًا ومع ذلك يعرض وضعية ليست الذراع فيها، مثلًا بعد أن يتحرك مفصل دون أمر. يتناول دليل PDF المرفق في تنزيل الجزء 2 هذه المسألة في الفصل 12، وللسبب نفسه توجد إيماءة التأكيد في الجزء 1.5.

2. ROS 2 باختصار

إذا كنت تعمل بالفعل مع ROS 2، فانتقل مباشرة إلى القسم 3. وإذا كان الجزءان 1 و1.5 كل تجربتك مع الروبوتات حتى الآن، فهذا القسم يقدّم المصطلحات التي ستحتاجها في بقية هذا المقال.

The five ROS 2 concepts used here: nodes, topics, TF, packages and launch files

العقدة (Node) برنامج يؤدي مهمة واحدة. robot_state_publisher عقدة. وRViz عقدة. وفي الجزء 3، مخطِّط MoveIt عقدة. إنها عمليات منفصلة، يمكن تشغيل كل منها وإيقافه على حدة، ويمكن أن تعمل على أجهزة مختلفة.

الموضوعات (Topics) قنوات مسمّاة بين العقد. تنشر العقدة رسائل على موضوع ما؛ ويمكن لأي عدد من العقد الاشتراك فيه. /joint_states يحمل زوايا المفاصل الحالية. والناشر لا يعرف من يستمع إليه. لذلك يمكنك أن تضع برنامج تشغيل (Driver) لروبوت حقيقي مكان واجهة رسومية ذات أشرطة تمرير، دون تغيير أي شيء في المراحل اللاحقة: فالموضوع هو الواجهة.

مكتبة TF، وهي مكتبة ROS 2 التي تتتبع الأطر الإحداثية، تجيب عن سؤال واحد: أين يقع ذلك الشيء بالنسبة إلى هذا. للروبوت أطر إحداثية كثيرة (الإطار الإحداثي نقطة أصل مع ثلاثة محاور)، إطار لكل وصلة، إضافةً إلى إطار العالم. تنشر TF باستمرار موضع كل إطار واتجاهه بالنسبة إلى إطاره الأب. ويمكن لأي عقدة أن تسأل “أين يقع gripper_base_1 في إحداثيات العالم” وتحصل على إجابة. robot_state_publisher هي العقدة التي تُنتج أطر الذراع: فهي تقرأ ملف URDF الخاص بك، وتشترك في /joint_states، وتنشر إطار TF لكل وصلة. تؤدي هذه العقدة معظم العمل الذي يقوم عليه ملف الإطلاق (Launch File) الخاص بـ RViz في القسم 11.

الحزمة مجلد يحتوي على ملف package.xml. فهي تجمع الملفات المترابطة، وتُعلم نظام البناء بما تعتمد عليه. omarm_description حزمة تضم: النموذج، وملفات Mesh (ملفات الأشكال ثلاثية الأبعاد 3D للقطع)، وملفات الإطلاق. ومساحة العمل (Workspace) مجلد يحتوي على src/ ، وهو مجلد يضم الحزم. وتبنيها باستخدام colcon build وتفعّلها باستخدام source install/setup.bash.

ملف الإطلاق (Launch File) يشغّل عدة عقد دفعة واحدة مع إعداداتها. وهو يوفّر عليك فتح طرفية لكل عقدة. display.launch.py في هذه الحزمة يشغّل ثلاث عقد ويسلّم النموذج إلى العقدة التي تحتاج إليه.

3. URDF: الوصلات والمفاصل والمعادلة الوحيدة المهمة

URDF اختصار لـ Unified Robot Description Format (الصيغة الموحدة لوصف الروبوتات). وهو بصيغة XML، ويصف الروبوت على أنه شجرة من الأجسام الصلبة المتصلة بمفاصل. في هذه الشجرة، كل وصلة باستثناء الجذر الوحيد هي ابن لمفصل واحد بالضبط، ولا توجد حلقات مغلقة.

Anatomy of a URDF link (visual, collision, inertial) and of a joint (origin, axis, limit)

‏ الوصلة (Link) هي جسم صلب واحد له ما يصل إلى ثلاثة أوصاف. <visual> هو الشكل الذي يرسمه RViz، و <collision> هو الشكل المستخدم في فحص التلامس. <inertial> يحتوي على الكتلة وعزوم القصور الذاتي (ixx, iyy, izz، بوحدة kg·m²)، وهي تبيّن مدى صعوبة تدوير الجسم حول كل محور:

<link name="link_1_1">
  <inertial>   <!-- mass and inertia: physics only, Gazebo reads this, RViz ignores it -->
    <mass value="0.3149105967000781"/>   <!-- the exporter writes full float precision; harmless, and left as it came -->
    <inertia ixx="0.001311" iyy="0.001223" izz="0.000194" ixy="1e-06" iyz="0.000215" ixz="1e-06"/>
  </inertial>
  <visual>     <!-- what you SEE -->
    <origin xyz="0.03063 0.0 -0.09157" rpy="0 0 0"/>
    <geometry><mesh filename="file://.../link_1_1.stl" scale="0.001 0.001 0.001"/></geometry>
  </visual>
  <collision>  <!-- what collision checking uses -->
    <origin xyz="0.03063 0.0 -0.09157" rpy="0 0 0"/>
    <geometry><mesh filename="file://.../link_1_1.stl" scale="0.001 0.001 0.001"/></geometry>
  </collision>
</link>

تفصيلتان هنا تحتاجان إلى شرح. الخاصية scale="0.001 0.001 0.001" موجودة لأن ملف STL (صيغة ملفات شائعة للشبكات ثلاثية الأبعاد 3D) مقيس بالمليمتر بينما يعمل ROS بالمتر. ومن دونها يُرسم الروبوت أكبر بـ 1000 مرة مما ينبغي. أما العنصر <origin> داخل <visual> فموجود لأن الشبكة (Mesh) تحمل نقطة أصل إحداثيات خاصة بها من تجميعة CAD، حيث تقع كل قطعة في موضعها داخل التجميعة. أما إطار الوصلة فيقع على محور المفصل. وأصل العنصر المرئي (Visual Origin) هو الإزاحة التي تنقل أحدهما إلى الآخر. لذلك فإن لكل شبكة تحت القاعدة أصلًا مثل 0.03063 0.0 -0.09157، بينما base_link، الواقع على أصل التجميعة، أصله 0 0 0.

وتترتب على ذلك نتيجة واحدة: شجرة TF الصحيحة لا تُثبت صحة الصورة. إذا كان أصل العنصر المرئي خاطئًا، فإن الوصلة تكون في مكانها الصحيح بينما تُرسم شبكتها في مكان آخر. ساعدني هذا التمييز عندما بدت إحدى قطع القابض وكأنها تطفو بجوار القاعدة. كانت أطر TF لجميع وصلات القابض مكتملة، وأصول العناصر المرئية متسقة، فلم يكن العيب في النموذج. كان السبب في إعدادات MoveIt، التي يتناولها الجزء 3.

‏ المفصل (Joint) يحدد كيف تتحرك الوصلة الابن بالنسبة إلى الوصلة الأب:

<joint name="Revolute 2" type="revolute">
  <origin xyz="-0.03063 0.0 0.049" rpy="-1.356194163 0 0"/>
  <parent link="base_link_upper_1"/>
  <child  link="link_1_1"/>
  <axis xyz="1.0 -0.0 -0.0"/>
  <limit upper="3.141593" lower="0.0" effort="3.5" velocity="5.2"/>
</joint>
العنصر المعنى
origin xyz الإزاحة من إطار الأب إلى إطار المفصل، بالمتر
origin rpy الدوران لإطار المفصل: الالتفاف (Roll)، والانحدار (Pitch)، والانعراج (Yaw)، بالراديان
axis الاتجاه الذي يدور حوله المفصل، معبَّرًا عنه في إطار المفصل
limit lower / upper المدى المسموح به للدوران، بالراديان
limit velocity السرعة القصوى، بالراديان في الثانية
type revolute يدور وله حدود، fixed لا يتحرك، continuous يدور بلا حدود

يضع المفصل إطار الابن بالنسبة إلى إطار الأب على خطوتين. وفي صورة مصفوفة:

T_joint = T_origin · R_axis(q).

الجزء الثابت أولًا، ثم الدوران بزاوية المفصل q حول المحور. القسم 8 يستخدم هذه المعادلة لإدخال معايرة مَقيسة في الهندسة. وهي أيضًا سبب خطأ الطريقة المختصرة “اطرح من قيمة roll” التي ستجدها في المنتديات، بالنسبة إلى خمسة من مفاصل الذراع الستة في هذا الروبوت.

The kinematic chain of OmArm One after the corrections, with every joint axis

يُظهر المخطط النموذج النهائي بعد التصحيحات في القسمين 8 و9. يحتوي الملف المُصدَّر الخام على وصلة واحدة ومفصل واحد أقل، وأربعة من هذه المحاور تشير في الاتجاه المعاكس. مفصل واحد فقط في هذه الذراع هو مفصّلة بسيطة حول المحور +X ، وهذا أمر مهم في القسم 8.

xacro، لأن URDF الخام لا يناسب النماذج الكبيرة

كتابة ملف XML هذا يدويًا لاثنتي عشرة وصلة عمل بطيء وعرضة للأخطاء. كما أن URDF العادي لا يملك طريقة للتعبير عن “إذا كانت هذه محاكاة، فاستخدم برنامج تشغيل مختلفًا”. Xacro معالج مسبق (Preprocessor) يضيف إلى URDF التضمينات والمتغيرات ووحدات الماكرو والشروط، ثم يتوسع إلى URDF عادي قبل أن يراه أي برنامج آخر.

تستخدم هذه الحزمة أربعة ملفات، والملف الرئيسي يتضمن الملفات الثلاثة الأخرى:

urdf/omarm.xacro         links, joints, and the includes below
urdf/materials.xacro     colour definitions
urdf/omarm.ros2control   which driver moves the joints
urdf/omarm.gazebo        surface properties and the Gazebo plugin

افحص ما أنتجه xacro؛ فالملف الموسَّع هو ما تقرؤه كل البرامج الأخرى:

xacro src/omarm_description/urdf/omarm.xacro > /tmp/robot.urdf

4. إعداد الحاسوب

كل ما يلي يعمل على Ubuntu 24.04 مع ROS 2 Jazzy، وهو الثنائي الذي بُنيت عليه مساحة العمل هذه واختُبرت. تتغير تعليمات التثبيت الرسمية لـ ROS 2 كثيرًا، لذا لا نكررها هنا. اتبع الدليل الرسمي لتوزيعتك، ثم أضف المكونات التي يحتاجها هذا الجزء:

sudo apt install \
  ros-jazzy-rviz2 \
  ros-jazzy-robot-state-publisher \
  ros-jazzy-xacro \
  ros-jazzy-joint-state-publisher-gui \
  ros-jazzy-ros2-control ros-jazzy-ros2-controllers \
  ros-jazzy-ros-gz \
  ros-jazzy-gz-ros2-control \
  ros-jazzy-tf2-tools

وظيفة كل حزمة:

  • rviz2: عارض 3D (القسم 11).
  • robot_state_publisher: يحوّل زوايا المفاصل إلى أطر TF.
  • xacro: يوسّع ملفات النموذج.
  • joint_state_publisher_gui: أشرطة التمرير في القسم 11.
  • ros2_control و ros2_controllers: إطار عمل وحدات التحكم (Controllers).
  • ros_gz: التكامل مع Gazebo.
  • gz_ros2_control: يتيح لوحدات التحكم نفسها قيادة الروبوت المحاكى.
  • tf2_tools: يرسم شجرة TF (القسم 13).

مساحة العمل مجلد يحتوي على المجلد src/ بداخله:

mkdir -p ~/omarm_one_ws/src

كل ما عدا ذلك ينتظر حتى توجد حزمة في src/: إما الحزمة الموجودة في تنزيل الجزء 2، أو الحزمة التي تُصدِّرها في القسم 6. وبمجرد وجودها:

cd ~/omarm_one_ws
colcon build
source install/setup.bash

colcon build يترجم (Compile) ويثبّت كل حزمة داخل src/. source install/setup.bash يتيح العثور عليها، ويجب تكراره في كل طرفية جديدة. تقريبًا كل رسالة “package not found” لحزمة موجودة أمامك سببها أمر source مفقود.

عادتان توفران عليك الوقت لاحقًا. ابنِ حزمة واحدة فقط بينما تعدّل عليها مرارًا:

colcon build --packages-select omarm_description

وبعد تغيير أي شيء في urdf/, config/ أو meshes/، أعد البناء قبل التشغيل. فهذه الملفات تُثبَّت في install/، وملفات الإطلاق تقرأ النسخة المثبَّتة لا النسخة التي عدّلتها. إذا عدّلت ملف URDF ولم ترَ أي تغيير، فالسبب في الغالب هو هذا.

طريقان من هنا

هناك مساران لبقية هذا المقال، ويلتقيان مجددًا في الطرفية نفسها.

Route A and Route B side by side: the finished package in five commands versus preparing CAD, exporting, auditing and correcting, both ending at the same launch command

المسار A، الحزمة الجاهزة. خذ omarm_one_ws من تنزيل الجزء 2، وابنِها، وشغّلها، ثم انتقل إلى القسم 11. النموذج الموجود فيها مطابَق مسبقًا للذراع الموجودة على طاولة عملي (الاتجاهات ومواضع الصفر وحدود السرعة)، وهو العمل الذي يصفه القسمان 8 و9. اقرأهما عندما تريد أن تعرف لماذا يعمل النموذج، أو عندما لا تكون ذراعك هي هذه الذراع.

المسار B، نموذج CAD الخاص بك. الأقسام من 5 إلى 10 بالترتيب، ثم الأقسام من 11 إلى 13. اختر هذا المسار إذا لم تكن الذراع الموجودة على مكتبك من نوع OmArm One.

# Route A. The download unpacks to a folder OmArm-One-Digital-Twin with the
# workspace omarm_one_ws inside it. Put the workspace in your home folder:
cp -r ~/Downloads/OmArm-One-Digital-Twin/omarm_one_ws ~/
cd ~/omarm_one_ws
colcon build --packages-select omarm_description
source install/setup.bash
ros2 launch omarm_description display.launch.py

5. تجهيز نموذج CAD في Fusion 360

صمّمت OmArm One في Autodesk Inventor من أجل الجزء 1. أداة التصدير تعمل في Fusion 360، لذلك أعددت التجميعة في Fusion من أجل هذا الجزء. تقرأ أداة التصدير البنية التي أنشأتها هناك، وجودة التصدير تتحدد بشكل شبه كامل قبل أن تشغّلها.

The Fusion 360 browser tree of the OmArm One assembly with the Joints group and the per-link components expanded, next to the 3D model

يظهر مكوّن واحد لكل وصلة في أسفل لوحة Browser، وفوقه مجموعة Joints. تمر أداة التصدير على هذه الشجرة، لذا فالأسماء الموجودة فيها هي الأسماء التي ستكتبها في ملفات YAML (ملفات إعدادات بنص عادي) طوال بقية السلسلة، بما في ذلك المسافات.

مكوّن واحد لكل وصلة. وصلة URDF جسم صلب واحد، لذا يجب أن تكون كل قطعة تتحرك باستقلالية مكوّنًا مستقلًا في Fusion. أما القطع التي لا تتحرك أبدًا بعضها بالنسبة إلى بعض، مثل حامل وبراغيه، فمكانها المكوّن نفسه. وفصلها لا يضيف سوى وصلات لا يمكنها فعل أي شيء.

المفاصل، بترتيب السلسلة. أنشئ المفاصل من القاعدة نحو الخارج، بحيث يربط كل مفصل مكوّنًا بالمكوّن الذي يسبقه. تحوّل أداة التصدير شجرة مفاصل Fusion مباشرةً إلى شجرة URDF، فإذا كانت السلسلة خاطئة هنا فستكون خاطئة في كل مكان بعد ذلك. استخدم مفاصل دوّارة (Revolute) لمحاور الذراع الستة ولتروس القابض؛ واستخدم مفاصل صلبة (Rigid) حيث تُثبَّت القطع بعضها ببعض.

The Motion tab of the Edit Joint dialog in Fusion 360 on the base joint, with Joint Motion Limits set to a minimum of 0 and a maximum of 180 degrees

اضبط حدود الحركة. في تبويب Motion من نافذة إعداد المفصل، فعّل حدود الحركة وأدخل المدى الذي يستطيع المفصل بلوغه فيزيائيًا: المدى الميكانيكي للسيرفو، لا المدى الذي تنوي استخدامه. في OmArm One يكون ذلك من 0 إلى 180 درجة لكل مفصل من مفاصل الذراع. يملك سيرفو الهواة القياسي مدى حركة يبلغ 180 درجة، وكل سيرفو يحرّك مفصله مباشرةً عبر ذراع السيرفو (Horn، أي القطعة الرافعة المثبتة على عمود الخرج في السيرفو). تصبح القيمتان lower و upper في URDF، وفي الجزء 3 يرفض MoveIt التخطيط خارجهما.

حدّد المادة. تقرأ أداة التصدير الكتلة والقصور الذاتي من المادة الفيزيائية المعيّنة في Fusion. عيّن مادة PLA (البلاستيك الذي طُبعت منه القطع) للقطع المطبوعة قبل التصدير. وإلا فستحصل على كتلة المادة الافتراضية، وسيحاكي Gazebo تلك الكتلة. في الجزء 1 وُزنت الذراع المكتملة فبلغ وزنها 1283 g، ومجموع كتل نموذج OmArm One يبلغ 1284 g. هذا التطابق ناتج عن خطأين يلغي أحدهما الآخر: فالنموذج يعامل كل قطعة مطبوعة على أنها مادة مصمتة، ولا يتضمن محركات السيرفو ولا المحامل ولا عناصر التثبيت (الجزء 1، القسم 4). في ذراعك الخاصة لا تعتمد على ذلك: أعطِ محركات السيرفو والمحامل وعناصر التثبيت داخل كل مكوّن كتلها الخاصة في Fusion. يطبع سكربت التدقيق المجموع، لذا يمكنك التحقق منه في سطر واحد.

تحقق من نقطة الأصل. يصبح أصل التجميعة إطار القاعدة للروبوت. ضعه حيث تلامس الذراع الطاولة، مع توجيه المحور Z إلى الأعلى، فتوفّر على نفسك تصحيحًا لاحقًا.

ما لا يمنحك إياه أيٌّ من ذلك. صفر المفصل في Fusion هو الوضع الذي تكون فيه المكوّنات لحظة تشغيل أداة التصدير؛ إذ تقرأ أداة التصدير الموضع والتوجيه الحاليين لكل مكوّن، لا الوضعية التي أُنشئ فيها المفصل. أما صفر الذراع الحقيقية فهو الوضع الذي كان عليه ذراع السيرفو (Horn) لحظة تثبيته بالمسامير. لا علاقة بين الاثنين، لذا لا يستطيع أي عمل دقيق في CAD أن يطابق بينهما. القسم 8 يسدّ هذه الفجوة.

6. توليد حزمة ROS 2

أداة التصدير هي fusion360-urdf-ros2 (برخصة MIT)، وهي الأداة نفسها التي استخدمتها في OmArm Zero. تقرأ قيم الإزاحات والقصور الذاتي من التجميع، فلا حاجة إلى قياس أي شيء يدويًا. والناتج حزمة ROS 2 كاملة وليس ملفًا منفردًا: ملفات الشبكات (Meshes)، وملفات xacro، وملفات الإطلاق (Launch Files)، package.xml, setup.py. توجد عدة أدوات تصدير مثلها، وتختلف في التفاصيل. المهم هو ما تُخرجه، والتدقيق في القسم التالي يصلح أيًّا كانت الأداة التي استخدمتها.

تثبيت أداة التصدير في Fusion

The four steps of installing the Fusion 360 URDF exporter: download the ZIP, unpack it, press Shift+S in Fusion, add the inner folder with the green plus

نزّلها من GitHub: الزر الأخضر Code ، ثم Download ZIP، ثم فكّ ضغطها في مكان يمكنك العثور عليه لاحقًا. ستحصل على مجلد اسمه fusion360-urdf-ros2-main، وفي داخله على مستوى واحد أدنى مجلد اسمه Fusion_URDF_Exporter_ROS2. هذا المجلد الداخلي هو الذي يحتاجه Fusion. يحتوي على Fusion_URDF_Exporter_ROS2.py والملف المطابق له .manifest، ويبحث Fusion عن هذين الملفين مباشرةً في المجلد الذي تحدده له. والخطأ المعتاد يقع في الخطوة الأخيرة: اختر المجلد الداخلي، لا مجلد الأرشيف.

The fusion360-urdf-ros2 repository on GitHub: the green Code button, the file list with InstallURDFExporter.bat, and the MIT licence in the About sidebar

تُسجَّل الأداة بوصفها سكربت (Script)، لا إضافة (Add-in)؛ فملف manifest الخاص بها ينص على "type": "script". وهذا يحدد المكان الذي تظهر فيه. في Fusion، اضغط Shift + S (المسار عبر القائمة: Utilities → ADD-INS) لفتح نافذة Scripts and Add-Ins. انقر على الزر الأخضر +، واختر Script or add-in from device، ثم حدد Fusion_URDF_Exporter_ROS2. بعدها تظهر تحت My Scripts وتبقى هناك بين الجلسات. تحديدها هناك والضغط على Run هو كل ما تتطلبه عملية التصدير.

The Scripts and Add-Ins dialog in Fusion 360 with Fusion_URDF_Exporter_ROS2 in the script list and its Run button

على Windows توجد طريقة مختصرة. يأتي المستودع مع الملف InstallURDFExporter.bat، الذي ينسخ المجلد نفسه إلى %APPDATA%\Autodesk\Autodesk Fusion 360\API\Scripts\ نيابةً عنك؛ أعد تشغيل Fusion وستجد السكربت جاهزًا هناك. أما على macOS فتستخدم الزر الأخضر +.

The four steps of running the Fusion 360 to ROS 2 URDF exporter: the Scripts and Add-Ins dialog, the plugin's welcome message, the Gazebo Harmonic question, and the success message with the duplicated components in the browser

ملاحظتان حول التشغيل نفسه. تسألك الأداة هل تعمل على Gazebo Harmonic أم Gazebo Classic؛ والجواب على Jazzy هو Harmonic. يغيّر هذا الاختيار ملفات الإطلاق التي تكتبها الأداة، لا النموذج. كما أنها تعيد هيكلة مستند التصميم الخاص بك: إذ يُنسخ كل مكوّن إلى مكوّن جديد ينتهي اسمه باللاحقة _1، وتُعاد تسمية المكوّنات الأصلية إلى old_component (1), old_component (2) وهكذا. ومن هذه النسخ تأتي اللاحقة _1 في link_1_1 و gripper_base_1 . ولأن أداة التصدير تغيّر تصميمك بهذه الطريقة، أغلق Fusion بعد ذلك دون حفظ.

ما الذي أنتجته الأداة:

omarm_description/
├── urdf/
│   ├── omarm.xacro          341 lines: 12 links, 11 joints
│   ├── materials.xacro      colour definitions
│   ├── omarm.gazebo         surface properties per link
│   └── omarm.ros2control    an empty <robot> element
├── meshes/                  12 STL files, one per link
├── launch/
│   ├── display.launch.py    RViz with sliders
│   └── gazebo.launch.py     spawn into Gazebo
├── config/                  RViz configurations, bridge config
├── package.xml
└── setup.py

ملفات الشبكات (Meshes) هي الهندسة الحقيقية وهي صحيحة؛ ففي النموذج النهائي هي الملفات نفسها التي كتبتها أداة التصدير، بايتًا ببايت. لا يمسّ أيٌّ من التصحيحات أدناه البيانات ثلاثية الأبعاد (3D). كل تصحيح هو رقم أو سمة (Attribute) في ملف نصي: ملف xacro للنموذج، وملف YAML واحد لوحدات التحكم (Controllers)، وسطران من Python في ملف إطلاق Gazebo. يمكنك إعادة كل ذلك في محرر نصوص. أما التصدير من جديد إلى المجلد نفسه فيستبدل كل ملف في الشجرة أعلاه، بما في ذلك الملفات الأربعة في urdf/ وملفا الإطلاق كلاهما، فتضيع التصحيحات التي فيها. وحده ملف YAML الخاص بوحدات التحكم يبقى سليمًا، لأن أداة التصدير لا تكتبه.

7. قراءة الملف المُصدَّر بعين ناقدة

قبل أن تغيّر أي شيء، اعرف ما لديك. وسّع ملف xacro (Expand) ودقّقه. السكربت أدناه مرفق مع هذا الجزء، وهو لا يفحص إلا المشكلات التي واجهتها على هذه الذراع.

cd ~/omarm_one_ws
colcon build --packages-select omarm_description && source install/setup.bash
xacro src/omarm_description/urdf/omarm.xacro > /tmp/robot.urdf
python3 tools/check_description.py /tmp/robot.urdf

خطوتا البناء (Build) وتهيئة البيئة (Source) ليستا اختياريتين هنا. تشير ملفات xacro إلى بعضها وإلى كل ملف شبكة عبر $(find omarm_description)، وهو تعبير لا يستطيع xacro حلّه إلا لحزمة مثبّتة وموجودة في المسار. كما يسحب التوسيع النسخ المثبّتة من materials.xacro, omarm.ros2control و omarm.gazebo. لذا بعد تعديل أي منها، أعد البناء قبل التدقيق، وإلا فستدقق النسخة السابقة.

check_description.py يوجد في tools/ في جذر مساحة العمل (Workspace)، بجوار src/، لا داخل الحزمة. إذا اتبعت المسار B، فانسخ tools/ من الملفات التي نزّلتها إلى جذر مساحة العمل أولًا. إنه ملف واحد لا يعتمد على شيء سوى مكتبة Python القياسية.

Three columns: seventeen problems the script flags, three values it prints without flagging, and the two mismatches only the real arm reveals
Terminal output of check_description.py on the exported model, listing the joint table, four zero inertia values and seventeen problems, ending with exit code 1

عند تشغيله على التصدير الخام، يطبع جدولًا كاملًا بالمفاصل. وهذه نسخة مختصرة تقتصر على الأسطر المهمة هنا:

links      : 12
joints     : 11
root link(s)          : ['base_link']
                        [... joint table, 11 rows ...]
zero principal inertia: ['gripper_gear_left_1.iyy', 'gripper_gear_right_1.iyy',
                         'gripper_finger_left_1.iyy', 'gripper_finger_right_1.iyy']
mimic joints          : none
gazebo plugin(s)      : none

17 problem(s):
  - Revolute 1: velocity 100.0 rad/s = 5730 deg/s - an exporter default, not a servo
  - Revolute 1: effort 100 Nm - an exporter default, not a servo torque
  [... the same pair for Revolute 2, 3, 4, 5, 6, 8 and 9; there is no Revolute 7 ...]
  - zero inertia makes the physics engine misbehave: [...]

ثمانية تحذيرات عن السرعة (velocity)، وثمانية عن حد العزم (effort)، وتحذير واحد عن القصور الذاتي: هذه هي المشكلات السبع عشرة. لا يظهر أيٌّ منها على شكل رسالة خطأ في أي مكان آخر. لا يوجد Revolute 7 لأن المفصل 7 هو Rigid 7، وهو المفصل الثابت بين وصلة الأداة وقاعدة القابض؛ إذ ترقّم أداة التصدير كل المفاصل في تسلسل واحد. المفاصل من Revolute 1 إلى Revolute 6 هي J1 إلى J6 من الجزء 1، من دوران القاعدة (Yaw) إلى دوران الأداة (Roll). أما القابض، وهو J7 في الجزء 1 وفي البرنامج الثابت، فهو هنا Revolute 8، وRevolute 9 هو ترسه الثاني.

تنقسم المشكلات السبع عشرة إلى ثلاثة أنواع. ويُظهر الناتج مشكلة رابعة لا ينبّه إليها السكربت: لا يوجد مفصل تابع (Mimic Joint). القسم 9 يعالج المشكلات الأربع جميعًا.

نتائج السكربت (تُصلَح في القسم 9)

  • كل مفصل متحرك يحمل القيمة velocity="100". أما المفاصل الثابتة الثلاثة فلا تحمل أي عنصر <limit> على الإطلاق. القيمة 100 تعني 100 راديان في الثانية، أي 5730 °/s، وهذا نحو تسعة عشر ضعفًا من 300 °/s التي يبلغها السيرفو عند 5 V. يستخدم MoveIt هذه القيمة حدًّا أعلى عندما يحوّل مسارًا مخططًا إلى مسار زمني (Trajectory: قائمة بمواضع المفاصل، لكلٍّ منها زمن محدد). لذلك قد يُنتج حركة لا تستطيع الذراع مجاراتها.
  • ويحمل كل مفصل متحرك أيضًا القيمة effort="100". أي 100 N·m. بينما يعطي DS3240-180 عزمًا قدره 36 kg·cm عند 5 V، أي 3.5 N·m، ويعطي SG90 نحو 0.18 N·m. إنها القيمة الافتراضية نفسها التي رأيناها في السرعة، ومن المصدر نفسه، وتأثيرها مماثل: المخطِّط الذي يستخدم هذه القيمة قد يسلّمك مسارًا زمنيًا لا يستطيع السيرفو تحمّله.
  • أربع قيم للقصور الذاتي تساوي صفرًا تمامًا. أجزاء القابض صغيرة إلى درجة أن Fusion قرّب القيمة iyy إلى الصفر بالدقة التي تكتب بها أداة التصدير. الجسم الصلب ذو القصور الذاتي الصفري حول محور ما لا معنى له فيزيائيًا، وقد يجعل محرك الفيزياء غير مستقر.
  • لا يوجد مفصل تابع (Mimic Joint). يحرّك سيرفو واحد إصبعَي القابض عبر زوج من التروس. أما في التصدير فهما مفصلان مستقلان، ولا شيء يربط بينهما. اقرأ عمود المحور (axis) قبل أن تكتب مفصلًا تابعًا: يعطي التصدير المفصل Revolute 8 المحور 0 -0.173648 0.984808 ويعطي Revolute 9 سالبه تمامًا. المحوران يشيران أصلًا إلى اتجاهين متعاكسين، لذا فإن معامل ضرب (multiplier) قدره 1 يجعل الترسين يدوران في اتجاهين متعاكسين، وهذا ما يفعله أي زوج من التروس المتعشّقة. أما إذا كان المحوران في تصديرك متوازيين، فستحتاج إلى -1.

نتائج الفحص على طاولة العمل (تُصلَح في القسم 8)

نقطتان لا يستطيع أي سكربت فحصهما، لأنهما تحتاجان إلى الروبوت الحقيقي بجانبك:

  • اتجاهات المفاصل. أربعة من مفاصل الذراع الستة تدور في الاتجاه المعاكس لاتجاه الذراع الحقيقية.
  • نقاط صفر المفاصل. في مفصلين يبعد الصفر الحقيقي بمقدار 45 و55 درجة عن الصفر في CAD.

القسم 8 يعالج بندَي طاولة العمل أولًا، لأن إصلاحات الفيزياء تذهب سدى على نموذج يدور في الاتجاه الخطأ. القسم 9 يُجري بعد ذلك سبعة تغييرات، منها الأربعة المذكورة أعلاه.

8. جعل النموذج يدور كما تدور الذراع

شغّل RViz أولًا (القسم 11 يشرح ما الذي يشغّله):

cd ~/omarm_one_ws
colcon build --packages-select omarm_description
source install/setup.bash
ros2 launch omarm_description display.launch.py

تحقق أولًا من Fixed Frame : يجب أن يسمّي إطارًا (Frame) يُنشَر فعلًا. ملف display.rviz المرفق يستخدم base_link; world يصلح أيضًا، لأن الاثنين مرتبطان بمفصل ثابت. أما القيمة الافتراضية في RViz نفسه، map، فلا شيء في هذه الحزمة ينشرها؛ ومعها ستحصل على خطأ باللون الأحمر وعرض 3D فارغ. إذا لم يُحمَّل ملف الإعدادات المرفق، فأضف عرض RobotModel بنفسك واضبط Description Topic الخاص به على /robot_description.

تختبئ ثلاثة ادعاءات منفصلة في عبارة «النموذج يطابق الذراع»، والخلط بينها هو سبب معظم أخطاء المعايرة.

The three independent claims behind a matching model: zero, direction and scale
  • الصفر. عندما يشير النموذج إلى 0 درجة، يكون المفصل الحقيقي عند صفره الخاص.
  • الاتجاه. عندما تزداد زاوية النموذج، يدور المفصل الحقيقي في الاتجاه نفسه.
  • المقياس. عشر درجات في النموذج تساوي عشر درجات على الذراع.

في مفصل يحرّكه السيرفو مباشرةً لا توجد علبة تروس تغيّر المقياس، لكن جدول تحويل النبضة إلى زاوية في البرنامج الثابت قد يغيّره. فالسيرفو الذي تخبره أن المدى من 500 إلى 2500 µs يساوي 180° كثيرًا ما يتحرك أكثر من ذلك بقليل أو أقل. يكشف ذلك قياسان متباعدان، أما قياس واحد فلن يكشفه أبدًا. عمليًا تصلح الاتجاه والصفر، ويؤكد القياسان أن المقياس صحيح.

لا يستطيع قياس واحد أن يفصل بين الصفر والاتجاه. حرّك مفصلًا إلى 90° في النموذج وقِس 110° على الذراع، فستعرف أن هناك خطأً ما، لكنك لن تعرف هل هو إزاحة قدرها +20° أم اتجاه معكوس مع إزاحة أخرى. خذ دائمًا قياسين يفصل بينهما 60 درجة على الأقل.

الاتجاه: اعكس المحور في النموذج

يُصلَح الاتجاه في URDF بعكس إشارة متجه محور المفصل. وعلى هذه الذراع احتاجت أربعة مفاصل إلى ذلك:

المفصل المحور المُصدَّر المحور المصحَّح
Revolute 3 1.0 0.0 -0.0 -1.0 0.0 0.0
Revolute 4 -0.0 0.984808 0.173648 0.0 -0.984808 -0.173648
Revolute 5 1.0 0.0 -0.0 -1.0 0.0 0.0
Revolute 6 -0.0 0.984808 0.173648 0.0 -0.984808 -0.173648

اثنان من هذه المحاور لا يتوافقان مع أي محور إحداثي على الإطلاق. فالمتجه 0 -0.984808 -0.173648 مائل بنحو 10 درجات عن المحور Y في المستوى YZ. تحصل على محور كهذا عندما يكون المفصل في CAD مركّبًا بزاوية. وهذا المحور المائل هو سبب فشل الطريقة المختصرة «الطرح من roll» مع المفصلين 4 و6 (انظر أدناه).

احتاجت أربعة مفاصل أيضًا إلى تدوير إطارها، وهذه عملية مختلفة عن عكس المحور. فالمحور يحدد اتجاه دوران المفصل؛ أما rpy فيحدد توجيه إطار المفصل نفسه قبل أن يدور.

المفصل القيمة المُصدَّرة لـ rpy القيمة المصحَّحة لـ rpy مكوّنات القيمة
Revolute 1 0 0 0 0 0 -1.570796 إطار القاعدة مُدار (Yaw) بزاوية −90°
Revolute 2 0 0 0 -1.356194163 0 0 تصحيح التوجيه −0.570796 و القيمة المقيسة 45°
Revolute 3 0 0 0 1.359931089 0 0 تصحيح التوجيه +0.4 و القيمة المقيسة 55°
Revolute 5 0 0 0 +1.570796 0 0 إطار المعصم مُدار (Roll) بزاوية +90°

يخفي هذا الجدول عملية من مرحلتين، وتُستخرج كل مرحلة بطريقة مختلفة. المرحلة الأولى هي التوجيه. بعد عكس المحاور، أدِر إطار كل مفصل حتى تستقر الوصلة (Link) وتدور في المستوى نفسه الذي تدور فيه على الذراع الحقيقية. على هذه الذراع: yaw بقيمة -1.570796 (−90°) على المفصل 1، وroll بقيمة +1.570796 (+90°) على المفصل 5، وroll بقيمتي -0.570796 و 0.4 على المفصلين 2 و3. تجد هذه القيم بالنظر إلى النموذج بجانب الآلة واستنتاج الوضعية التي يجب أن يتخذها كل إطار. وهي تعتمد على طريقة بناء نموذج CAD، لذا ستختلف قيمك. المرحلة الثانية هي الصفر المقيس، وهي عملية حسابية تُبنى على المرحلة الأولى. وبقية هذا القسم مخصصة للمرحلة الثانية.

ملاحظة إن كنت تقرأ الكود المصدري المرفق. التعليق على Revolute 2 في omarm.xacro يتضمن الحساب نفسه ويبيّن مصدر كل حد: فالقيمة -0.570796 هي تصحيح التوجيه من المرحلة الأولى، وليست قيمة من CAD. فالتصدير الخام يحتوي على rpy="0 0 0" في ذلك المفصل؛ ويمكنك التحقق من ذلك في نسخة ما قبل التصحيح من omarm.xacro. سكربت التدقيق يطبع المحاور، لا نقاط الأصل (Origins).

الصفر: مكانان لوضع التصحيح

بعد أن تصبح الاتجاهات صحيحة، قِس. اضبط كل منزلق (Slider) في RViz على 0، ثم حرّك المفصل الحقيقي حتى تقف الذراع في الوضعية نفسها التي تظهر في الصورة، واقرأ أمر السيرفو الذي أوصلك إليها. كن دقيقًا في تحديد اتجاه عدم التطابق، لأن إشارة كل ما يلي تعتمد عليه.

على هذه الذراع، طابق المفصل 2 وضعية الصفر في النموذج عند زاوية سيرفو 45°، والمفصل 3 عند زاوية سيرفو 55°. أي أن أرقام النموذج غير المصحَّح كانت متأخرة عن السيرفو بهذا المقدار. اكتب ذلك في معادلة واحدة قبل أن تلمس أي شيء:

model_old = servo - 45°        (joint 2, measured; joint 3 is the same with 55)

يمكنك تصحيح ذلك في مكانين. الخيار A يترك النموذج خاطئًا ويعوّض الفرق في برنامج التشغيل (Driver)؛ والخيار B يصلح النموذج نفسه.

The two places a calibration offset can live, with the trade-offs of each

الخيار A: إزاحة في برنامج التشغيل. ‏ omarm.ros2control يمكن لهذا الملف أن يحتوي على المعامل servo_offset_deg ، ويحوّل برنامج تشغيل العتاد بين راديان URDF ودرجات السيرفو. الخيار A بسيط، وتحتفظ المحاكاة بنموذج CAD الخالص. لكنك ستعيش حينها مع model = servo - 45 إلى الأبد: يقرأ هذا المفصل من −45 إلى 135 بينما يقرأ كل مفصل آخر من 0 إلى 180. ويجب أن تُنقل حدود المفصل معه، وتظل المحاكاة تعرض الوضعية الخاطئة، لأن Gazebo لا يمر أبدًا عبر برنامج التشغيل هذا.

الخيار B: أدِر نقطة أصل المفصل (Origin) في URDF. يقع التصحيح في الهندسة نفسها. فتصبح زاوية URDF مساوية لزاوية السيرفو، ويبقى المدى من 0 إلى 180 مثل كل مفصل آخر، وتُصلَح المحاكاة أيضًا لأنها تقرأ ملف URDF نفسه.

يستخدم هذا المشروع الخيار B، لأن ملف URDF يجب أن يصف الروبوت الذي تملكه.

الحساب، والفخ الكامن فيه

ابدأ من T = T_origin · R_axis(q). تريد أن يرسم النموذج عند زاوية السيرفو s ما يرسمه حاليًا عند s - d، حيث d هي الإزاحة المقيسة. وهذه الإزاحة تساوي +45° للمفصل 2 و+55° للمفصل 3، وهي المقدار الذي تتأخر به الأرقام القديمة عن السيرفو. لذا تحتاج إلى:

R_new · R_axis(s) = R_old · R_axis(s − d).

يمكن تطبيق دورانين حول المحور نفسه بأي ترتيب، وتُجمع زاويتاهما، لذا تُختزل المعادلة إلى:

R_new = R_old · R_axis(−d).

النتيجة دقيقة. أما طرح d من roll فيعطي النتيجة نفسها فقط عندما يكون المحور +X و rpy دورانًا خالصًا حول X؛ ويبيّن الجدول الحالات الأخرى. وعلى هذه الذراع، يصح ذلك لمفصل واحد.

Why subtracting from roll is wrong for five of this robot's six arm joints
المفصل axis العملية الصحيحة
Revolute 2 +X roll − d
Revolute 3 -X roll + d، تنعكس الإشارة
Revolute 5 -X roll + d
Revolute 1 +Z yaw − d، وليس roll
Revolute 4 0 -0.985 -0.174 لا يوجد مكوّن rpy واحد بعينه
Revolute 6 0 -0.985 -0.174 لا يوجد مكوّن rpy واحد بعينه

لو طُبّقت وصفة المفصل 2 على المفصل 3 لزاد خطؤه من 55° إلى 110° بدلًا من إزالته. أما المفصلان 4 و6 فلا يمكن تصحيحهما بتعديل رقم واحد إطلاقًا: فمحورهما مائل بمقدار 10° عن Y في المستوى YZ، لذا يتوزع التصحيح على قيم rpy الثلاث كلها.

إليك الحساب للمفصلين اللذين جرى قياسهما.

المفصل 2 محوره +X، و rpy دوران خالص حول X، لذا تُجمع القيمتان مباشرةً:

rpy_roll = -0.570796  (orientation fix)  -  0.785398163  (45°)  =  -1.356194163

المفصل 3، المحور -X، لذا فإن الإزاحة تُضاف:

rpy_roll = 0.4  (orientation fix)  +  0.959931089  (55°)  =  1.359931089

لاحظ المنازل العشرية التسع. التقريب إلى ست منازل يعطي خطأً بنحو 10⁻⁵ درجة، وهو خطأ لا أهمية له فيزيائيًا لكنه يُفسد أي تحقق تجريه لاحقًا؛ والأرقام الإضافية لا تكلّف شيئًا.

لا تطبّق التصحيح مرتين. بمجرد أن تستقر الزاوية 45° في المفصل 2 ضمن قيمة rpy، فإن servo_offset_deg الخاص بذلك المفصل يجب أن يُحذف. فإبقاء الاثنين معًا يحسب الإزاحة مرتين، وينتهي الأمر بالذراع منحرفة بمقدار 45° في الاتجاه الآخر. وملف omarm.ros2control المرفق يحمل تعليقًا على ذلك المفصل ينبّه إلى هذا.

الإزاحة تغيّر الأرقام، ولا تغيّر الأشكال أبدًا. أيًّا كان الخيار الذي تختاره، فالتصحيح يغيّر الرقم المعروض. أما الشكل الذي يرسمه RViz عند رقم معين فيأتي من ملفات الشبكات ونقاط أصل المفاصل. فإذا تطابقت الأرقام لكن الصورة لا تزال لا تشبه الذراع، فإن وضعية الصفر في CAD نفسها خاطئة، ولن تصلحها أي إزاحة. واختبار المظهر الخارجي (Silhouette) في القسم 13 يكشف ذلك.

للمحاور المائلة (اختياري)

المتطابقة أعلاه لا تزال صالحة، لكنها لم تعد حسابًا يمكنك إجراؤه في ذهنك. R_new = R_old · R_axis(-d)، حيث R_axis هو دوران بمقدار -d حول متجه محور المفصل نفسه. تبني scipy، وهي مكتبة Python للحوسبة العلمية، هذا الدوران من المحور والزاوية باستخدام صيغة Rodrigues (الصيغة القياسية للدوران حول أي محور). ابنِ R_old من قيمة rpy الخاصة بالمرحلة الأولى وفق اصطلاح URDF، أي R = Rz(yaw)·Ry(pitch)·Rx(roll). وتسمي scipy هذا الترتيب extrinsic 'xyz': أي دورانات حول المحاور الثابتة X وY وZ، تُطبَّق بهذا الترتيب. اضرب، ثم استخرج قيم roll وpitch وyaw الجديدة من حاصل الضرب. إذا لم تكن scipy مثبّتة، فثبّتها بالأمر sudo apt install python3-scipy.

from scipy.spatial.transform import Rotation as Rot
import numpy as np

axis = np.array([0.0, -0.984808, -0.173648])   # the joint's axis after the direction fix
d    = np.deg2rad(12.0)                        # the measured offset, in radians
R_old = Rot.from_euler('xyz', [0.0, 0.0, 0.0]) # the rpy after stage one
R_new = R_old * Rot.from_rotvec(-d * axis)
print(R_new.as_euler('xyz'))                   # the three numbers for the file

شغّل ذلك على المفصل 2 (المحور 1 0 0، d = 45°، وقيمة المرحلة الأولى لـ rpy -0.570796 0 0) وسيطبع [-1.35619416 0. 0.]، وهو الرقم الذي حسبناه يدويًا أعلاه.

لم أحتج إلى استخدامه قط على OmArm One: فقد خرج المفصلان 4 و6 من CAD وصفرهما مطابق للآلة أصلًا، فلم يكن هناك ما يحتاج إلى تصحيح. وهو موجود هنا حتى لا تعلق إن لم يكن الأمر كذلك عندك.

9. ما تحتاج إليه المحاكاة والتخطيط

يرسم RViz كل ما يقوله النموذج دون التحقق من الفيزياء؛ أما Gazebo فيحاكي الجاذبية والتلامس، لذا تسبب القيم غير الصالحة فيزيائيًا مشكلات هناك. يحتاج Gazebo إلى مفصل world، وقصور ذاتي غير صفري، وإضافة وحدة التحكم (Controller Plugin). robot_state_publisher يحتاج إلى المفصل التابع. ويستخدم MoveIt حدود السرعة وعزم الدوران والقابض. والمجموع سبعة تغييرات.

يجب تثبيت الروبوت بالعالم (world). يصف ملف URDF روبوتًا يطفو في الفراغ. يقبل RViz ذلك؛ أما في Gazebo فتسقط الذراع غير المثبّتة بفعل الجاذبية. والحل وصلة (Link) اسمها world ومفصل ثابت يربط القاعدة بها:

<link name="world"/>

<joint name="world_to_base" type="fixed">
  <parent link="world"/>
  <child link="base_link"/>
  <origin xyz="0 0 0" rpy="0 0 0"/>
</joint>

تغيّر وصلة world جذر الشجرة (إذ يُظهر سكربت التدقيق الآن world بدلًا من base_link). وهذا يعني أيضًا أن ارتفاع الإنشاء (Spawn) في ملف الإطلاق يجب أن يكون 0، لا 32 cm التي استخدمها ملف الإطلاق المُصدَّر. فالذراع التي تُنشأ على ارتفاع 32 cm ثم تُثبَّت بالعالم تبقى على ارتفاع 32 cm.

يجب ألا يكون القصور الذاتي صفرًا. خرجت أجزاء القابض الأربعة بالقيمة iyy="0.0"، للسبب المذكور في القسم 7. والحل: اضبط iyy على قيمة ixx الخاصة بالوصلة، أي عزم قصورها الذاتي حول محور عمودي، بدلًا من إعادة حسابها. ففي الأجزاء الأربعة كلها تكون ixx و izz متساويتين، لذا تعطي أيٌّ منهما الرقم نفسه:

<!-- gripper_finger_right_1: iyy was 0.0 -->
<inertia ixx="3e-06" iyy="3e-06" izz="3e-06" ixy="-0.0" iyz="-0.0" ixz="0.0"/>

هذه القيمة بديل مؤقت يجعل الجسم صالحًا فيزيائيًا. إنها حد أعلى، وليست خاصية مقيسة. فالحد الذي قُرِّب إلى الصفر هو بالتعريف أصغر الحدود الثلاثة بدقة أداة التصدير. iyy خرجت أقل من 5·10⁻⁷، بينما ixx و izz بقيتا عند 1·10⁻⁶ في الترسين و3·10⁻⁶ في الإصبعين. لذا فإن نسخ القيمة من محور عمودي يبالغ في تقدير iyy. في قطع تزن خمسة غرامات وعشرة غرامات عند طرف ذراع كتلتها 1.28 kg، لا يغيّر ذلك شيئًا يمكنك رؤيته. أما في وصلة أثقل فسيغيّر، وعندها تعود إلى ملف CAD وتصدّر قيم القصور الذاتي كما ينبغي.

يجب أن يُربط الإصبعان معًا. من الناحية الميكانيكية، القابض عبارة عن محرك سيرفو واحد يدير زوجًا من التروس. وفي URDF يقابل ذلك مفصل محاكاة (mimic joint):

<joint name="Revolute 9" type="revolute">
  ...
  <mimic joint="Revolute 8" multiplier="1" offset="0"/>
</joint>

لا يملك Revolute 9 محركًا، ولذلك لا يُدرَج في ros2_control. robot_state_publisher و MoveIt يحسبان زاويته انطلاقًا من Revolute 8. لذلك يتحرك مفصل الترس الأيمن في Gazebo بحرية: فـ TF والمخطِّط يلتزمان بعلاقة المحاكاة (mimic)، أما المحاكي فلا. ويُعدّ هذا المفصل الحر مقبولًا ما دام لا شيء يضغط على الإصبع. أعد النظر فيه قبل أن يُطلب من القابض حمل ثقل في المحاكاة. ولا ترسل أوامر إلى Revolute 9 أيضًا: فعندها سترسل وحدتا تحكم أوامر متعارضة إلى آلية واحدة.

لا بد من شيء يحرّك المفاصل. ملف URDF يصف الروبوت، لكنه لا يحرّكه. وفي المحاكاة تتولى هذه المهمة gz_ros2_control، الذي يعمل داخل Gazebo ويقرأ مقطع <ros2_control> ويقدّم المفاصل لإطار وحدات التحكم المعتاد في ROS 2. ويُحمَّل عبر مقطع إضافة (plugin) في omarm.gazebo، مغلَّف بحيث لا يُضاف إلا عند المحاكاة.

<xacro:if value="$(arg use_sim)">
  <gazebo>
    <plugin filename="gz_ros2_control-system"
            name="gz_ros2_control::GazeboSimROS2ControlPlugin">
      <ros>
        <remapping>/controller_manager/robot_description:=/robot_description</remapping>
      </ros>
      <parameters>$(find omarm_description)/config/ros2_controllers.yaml</parameters>
    </plugin>
  </gazebo>
</xacro:if>

‏ <parameters> هو السطر الذي يشير إلى ملف إعدادات وحدات التحكم، الذي يسرد كل وحدة تحكم والمفاصل التابعة لها:

controller_manager:
  ros__parameters:
    update_rate: 100  # Hz
    omarm_arm_controller:
      type: joint_trajectory_controller/JointTrajectoryController
    omarm_gripper_controller:
      type: joint_trajectory_controller/JointTrajectoryController
    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster

omarm_arm_controller:
  ros__parameters:
    joints: ["Revolute 1", "Revolute 2", "Revolute 3",
             "Revolute 4", "Revolute 5", "Revolute 6"]
    command_interfaces: [position]
    state_interfaces: [position, velocity]

omarm_gripper_controller:
  ros__parameters:
    joints: ["Revolute 8"]
    command_interfaces: [position]
    state_interfaces: [position, velocity]

ويغيب Revolute 9 عن هذه القائمة للسبب نفسه. update_rate: 100 هو معدّل حلقة وحدات التحكم، وليس معدّل خطوة الفيزياء في Gazebo التي تعمل أسرع منه. والاثنان مرتبطان: gz_ros2_control يستدعي مدير وحدات التحكم (controller manager) من داخل خطوة التحديث الخاصة بـ Gazebo نفسه، ويتخطى الدورات حتى يحين موعد الفترة التالية بمعدل 100 Hz.

في ملفات تنزيل الجزء 2، يوجد هذا الملف في omarm_description/config/ros2_controllers.yaml، لذا فإن حزمة الوصف (description package) مكتفية بذاتها وتعمل دون تثبيت أي شيء من الجزء 3. أما حزمة MoveIt في الجزء 3 فتأتي بنسختها الخاصة؛ وعندما تكون كلتاهما على المسار، فإن النسخة المذكورة في omarm.gazebo هي التي تُحمَّل.

يجب أن تطابق حدود السرعة محرك السيرفو. velocity="100" أصبح velocity="5.2"، أي 298 °/s. تذكر ورقة بيانات DS3240-180 زمن 0.20 s لكل 60° عند 5 V، أي 300 °/s دون حمل، وتقع قيمة 5.2 rad/s تحتها بقليل. وتذكر الورقة أيضًا 0.17 s لكل 60° عند 6.8 V، أي نحو 353 °/s. وعند 6 V، يعطي الاستيفاء بين السطرين نحو 0.183 s، أي نحو 327 °/s. يشغّل الجزء 1 الذراع على خط تغذية 6 V، لكن الحد يبقى على سطر 5 V. فهبوط جهد التغذية يدفع محركات السيرفو نحو ذلك السطر، ولذلك فهو الحالة المتحفظة. أما محرك SG90 في القابض فحصل على 8.0 rad/s، أي 458 °/s. وتذكر ورقة بياناته 0.1 s لكل 60° عند 4.8 V، أي نحو 600 °/s؛ فالسيرفو الصغير هو الأسرع في الذراع، وتبقى قيمة 458 °/s أدنى من قيمة الورقة بدل أن تساويها.

وكان لا بد من خفض حدود عزم الدوران معها. effort="100" أصبح effort="3.5" في مفاصل الذراع الستة، و effort="0.18" في القابض. فقيمة 36 kg·cm عند 5 V تساوي 3.5 N·m، وقيمة 1.8 kg·cm لمحرك SG90 تساوي 0.18 N·m. تعتمد قيمة الذراع على سطر 5 V مثل حد السرعة؛ أما على خط تغذية 6 V فيعطي DS3240 قيمة 41 kg·cm، أي نحو 4.0 N·m. لا شيء في الجزء 2 يفرض هذا الحد: فـ RViz يتجاهله، ووحدات التحكم في الموضع داخل Gazebo تحرّك المفصل مباشرة، لذا لن ترى فرقًا بعد. إنه موجود من أجل الجزء 3، حيث يقرؤه المخطِّط، ولأن محرك سيرفو الهواة لا يقدّم 100 N·m.

أصبح حد القابض 1.6 rad بدلًا من π/2. تعادل 1.6 rad زاوية 91.67°، أي أن الحد يقع الآن بعد زاوية 90° التي تستخدمها الآلية بمقدار 1.7°. ويمنع هذا الهامش خطأً ناتجًا عن التقريب. فمع حدٍّ قيمته 1.570796، فإن البرنامج الذي يحسب π/2 على أنها 1.5707963267948966 يجد المفصل خارج حده بقليل ويرفض التخطيط (Joint ... from the starting state is outside bounds). يحدث هذا عندما يُركَن المفصل عند حده تمامًا. واجهتُ ذلك في المفصل 2 حين كانت الإزاحة (offset) لا تزال في برنامج التشغيل (الخيار A في القسم 8). كان حده الأدنى -0.785398، وكانت الذراع مركونة عند الزاوية 0 للسيرفو، فرفض MoveIt كل خطة. أما مفاصل الذراع فتحتفظ بالقيمة 3.141593، وهي آمنة للسبب المعاكس: فبعد تقريبها إلى الأعلى تقع فوق π بقليل لا تحتها.

10. نموذج واحد ونظامان خلفيان

يجب أن يُقاد الروبوت نفسه من نظامين مختلفين: في المحاكاة بواسطة فيزياء Gazebo، وعلى طاولة العمل بواسطة ESP32 عبر USB. أريد وصفًا واحدًا للحالتين، لأنه مع ملفَّي URDF سرعان ما يغيب التعديل الذي يُجرى في أحدهما عن الآخر. لذلك يأخذ النموذج وسيطًا (argument):

The use_sim switch: one description, two hardware back ends, identical joints
<xacro:arg name="use_sim" default="false" />

<ros2_control name="OmArmHardware" type="system">
  <hardware>
    <xacro:if value="$(arg use_sim)">
      <plugin>gz_ros2_control/GazeboSimSystem</plugin>
    </xacro:if>
    <xacro:unless value="$(arg use_sim)">
      <plugin>omarm_hardware/OmArmSystem</plugin>
      <param name="device">auto</param>
      <param name="baudrate">115200</param>
      ...
    </xacro:unless>
  </hardware>

  <!-- everything below here is identical for both -->
  <joint name="Revolute 1">
    <command_interface name="position"/>
    <state_interface name="position"><param name="initial_value">1.5708</param></state_interface>
    <state_interface name="velocity"/>
  </joint>
  ...

أسماء المفاصل وواجهاتها وحدودها متطابقة. لا يختلف سوى الواجهة الخلفية للعتاد (hardware back end)، إضافةً إلى مقطع إضافة Gazebo عند use_sim:=true. ويحمل فرع العتاد أيضًا أحد عشر معاملًا لبرنامج التشغيل لا تحتاج إليها المحاكاة، لذا فإن الفرق (diff) أطول من سطر واحد. كل ما يلي مقطع العتاد مشترك، لذا تقود واجهة مسار الحركة (trajectory) نفسها كلًّا من المحاكاة والذراع الحقيقية. يمكنك رؤية الفرق بنفسك:

xacro src/omarm_description/urdf/omarm.xacro                 > /tmp/robot_hw.urdf
xacro src/omarm_description/urdf/omarm.xacro use_sim:=true   > /tmp/robot_sim.urdf
diff /tmp/robot_hw.urdf /tmp/robot_sim.urdf

يجب أن يتحقق شرطان كي يعمل هذا التبديل.

يجب أن يمرّر ملف الإطلاق (launch file) التعيين (mapping). والملف المُصدَّر لم يكن يفعل ذلك.

# exported
robot_description_config = xacro.process_file(robot_description_file)

# corrected
robot_description_config = xacro.process_file(
    robot_description_file,
    mappings={'use_sim': 'true'})

ويجب أن يعلن ملف xacro عن الوسيط قبل ملفات التضمين (include) التي تقرؤه، لأن xacro يعالج الملف من الأعلى إلى الأسفل، و omarm.ros2control يُضمَّن من ملف منفصل ولا يُكتب نصه داخل الملف. لذا يأتي الإعلان أولًا:

<xacro:arg name="use_sim" default="false" />

<xacro:include filename="$(find omarm_description)/urdf/materials.xacro" />
<xacro:include filename="$(find omarm_description)/urdf/omarm.ros2control" />
<xacro:include filename="$(find omarm_description)/urdf/omarm.gazebo" />

أحد هذين الخطأين يتوقف برسالة خطأ؛ والآخر يفشل دون رسالة.

مع رسالة خطأ: إذا لم يُعلَن عن الوسيط قبل ملفات التضمين التي تقرؤه، يتوقف التوسيع (expansion) مع الرسالة Undefined substitution argument use_sim ورمز خروج غير صفري. لا يظهر الخطأ إلا في التشغيل الذي لا يمرّر أي تعيين، وهو مسار RViz. أما تمرير use_sim:=true فيكتب القيمة في جدول الاستبدال مباشرة، لذا يستمر ذلك التشغيل في العمل ويُخفي المشكلة. وستكتشفها لحظة تشغيل RViz.

دون رسالة خطأ: إذا نسي ملف الإطلاق mappings={'use_sim': 'true'}، فلن يعترض أي شيء. فالقيمة الافتراضية المُعلنة false هي التي تُطبَّق، فيبدأ Gazebo وقد حمّل برنامج تشغيل العتاد، ويحاول فتح منفذ تسلسلي غير موجود. في مساحة العمل الخاصة بي، ظل ملف الإطلاق يحمل لأشهر تعليقًا يقول إنه يمرّر use_sim، لكن السطر نفسه كان مفقودًا، وظل Gazebo يحاول فتح /dev/ttyUSB0. وأمر diff أعلاه يكشف ذلك.

11. عرض النموذج: RViz

بعد تصحيح النموذج، يصبح RViz أسرع وسيلة لديك للحصول على تغذية راجعة. إذا لم يكن يعمل بعد، فشغّله بالأوامر الموجودة في بداية القسم 8.

What display.launch.py and gazebo.launch.py each start: three nodes for RViz, four launch actions for Gazebo

display.launch.py يشغّل ثلاث عُقد (nodes)؛ أما gazebo.launch.py فينفّذ أربعة إجراءات إطلاق (launch actions). تُفعَّل وحدات التحكم بعد ذلك في طرفية ثانية (القسم 12). إذا تهدّلت الذراع في المحاكاة، فغالبًا ما تكون هذه الخطوة هي المفقودة.

RViz showing the OmArm One model with TF axes as coloured triads per link, and the joint state publisher GUI window with seven sliders, one per driven joint

‏ joint state publisher GUI تعطيك شريط تمرير لكل مفصل مُقاد، وتنشر زوايا الأشرطة على /joint_states. هناك سبعة أشرطة تمرير، وليس اثني عشر: فالمفاصل الثابتة الأربعة لا تتحرك، ويُحسب Revolute 9 من Revolute 8 بواسطة وسم mimic. robot_state_publisher يحوّل زوايا المفاصل هذه إلى إطارات TF (القسم 2)، ويرسم RViz الشبكات (meshes) عند تلك الإطارات. في الجزء 3 لا يتغير إلا مصدر /joint_states إذ ينتقل من نافذة أشرطة التمرير إلى الذراع الحقيقية.

بعد ضبط Fixed Frame كما في القسم 8، افحص النموذج بهذا الترتيب:

هل يدور كل مفصل في الاتجاه الصحيح؟ حرّك شريط تمرير واحدًا في كل مرة في الاتجاه الموجب، وتحقق من أن الذراع الحقيقية تدور في الاتجاه نفسه. يختبر هذا الفحص ما قيل عن الاتجاه في القسم 8.

هل تتطابق الأرقام؟ حرّك الذراع الحقيقية إلى 90° في مفصل واحد واضبط شريط التمرير على 90°. يجب أن تتوافق الصورة مع الرقم.

ثم قارن الهيئة الخارجية في عدة وضعيات (القسم 13).

12. إدخاله في عالم الفيزياء: Gazebo

ros2 launch omarm_description gazebo.launch.py
The OmArm One model standing in an empty Gazebo world, with the entity tree showing ground_plane, sun and omarm

يستخدم Gazebo قيم الكتلة والقصور الذاتي؛ أما RViz فيتجاهلها. ويظهر في حقل Source File Path في لوحة الكيانات (entity panel) النص <urdf-string>، لأن النموذج أُدرج في المحاكاة (spawn) من الموضوع /robot_description (topic)، وليس من ملف على القرص.

يشغّل ملف الإطلاق هذا Gazebo بعالم فارغ، ويشغّل robot_state_publisher و ros_gz parameter bridge (جسر المعاملات)، ثم ينتظر خمس ثوانٍ ويُدرج الروبوت في المحاكاة من الموضوع /robot_description (topic). الانتظار مهم: فإدراج الروبوت في محاكٍ لم يكتمل تشغيله يفشل بطريقة تبدو كأن النموذج معطوب.

بما أن ملف URDF عولج باستخدام use_sim:=true، فإن gz_ros2_control يبدأ بوصفه إضافة (plugin) داخل Gazebo ويشغّل مدير وحدات تحكم للمفاصل. أما وحدات التحكم نفسها فلا يزال يجب تفعيلها في طرفية ثانية نُفّذ فيها source بالطريقة نفسها (فالأولى مشغولة بتشغيل Gazebo).

cd ~/omarm_one_ws && source install/setup.bash

ros2 control list_controllers
ros2 control load_controller --set-state active joint_state_broadcaster
ros2 control load_controller --set-state active omarm_arm_controller
ros2 control load_controller --set-state active omarm_gripper_controller

بعدها يمكنك إرسال مسار حركة (trajectory) مباشرة، دون مخطِّط:

ros2 topic pub --once /omarm_arm_controller/joint_trajectory \
  trajectory_msgs/msg/JointTrajectory \
  '{joint_names: ["Revolute 1","Revolute 2","Revolute 3","Revolute 4","Revolute 5","Revolute 6"],
    points: [{positions: [1.57, 1.2, 1.57, 1.57, 1.57, 1.57], time_from_start: {sec: 3}}]}'

لاحظ علامات الاقتباس حول كل اسم مفصل. فالأسماء تحتوي على مسافة, Revolute 1 بدلًا من Revolute_1، لأن هذا ما أنتجه التصدير من CAD. الأسماء غير المحاطة بعلامات اقتباس ستعمل في هذا الأمر، لكنها تتعطل بمجرد أن يحتوي اسم على فاصلة أو قوس، أو عند إزالة علامات الاقتباس المفردة الخارجية. ووضع كل اسم بين علامتي اقتباس يتجنب الحالتين.

إعادة تسميتها ستكون أنظف، وستطال أيضًا ملف URDF وإعدادات وحدات التحكم، وفي الجزء 3 الوصف الدلالي (semantic description) وبرنامج التشغيل. أبقيتُ على الأسماء المُصدَّرة.

13. التحقق من التوأم الرقمي

أحد الفحصين يُجرى على الملف، والآخر على طاولة العمل.

آليًّا، على الملف. سكربت التدقيق من القسم 7، مُشغَّلًا على كلا التوسيعين:

cd ~/omarm_one_ws
colcon build --packages-select omarm_description && source install/setup.bash

xacro src/omarm_description/urdf/omarm.xacro > /tmp/robot.urdf
python3 tools/check_description.py /tmp/robot.urdf

xacro src/omarm_description/urdf/omarm.xacro use_sim:=true > /tmp/robot_sim.urdf
python3 tools/check_description.py /tmp/robot_sim.urdf

يجب أن ينتهي كلا التشغيلين بالسطر all checks passed. عند الفشل يخرج السكربت برمز غير صفري، لذا يمكنك تشغيله ضمن الفحوص الآلية (التكامل المستمر، CI). القيم قبل التعديل وبعده في جدول واحد:

الفحص التصدير الخام بعد المعالجة اللاحقة
الوصلات / المفاصل 12 / 11 13 / 12
الوصلة الجذرية base_link world
سرعة مفاصل الذراع 100 rad/s = 5730 °/s 5.2 rad/s = 298 °/s
سرعة مفصل القابض 100 rad/s = 5730 °/s 8.0 rad/s = 458 °/s
قيم القصور الذاتي الصفرية 4 0
مفاصل المحاكاة (mimic) لا شيء Revolute 9 يحاكي Revolute 8
المفاصل تحت إدارة ros2_control لا شيء 7
إضافة Gazebo مع use_sim:=true لا شيء gz_ros2_control-system
حد عزم المفصل (effort) 100 N·m 3.5 N·m للذراع، 0.18 N·m للقابض
الكتلة الكلية 1284.4 g 1284.4 g، مقابل 1283 g بالقياس الفعلي
نتيجة التدقيق 17 مشكلة all checks passed

بالعين، مقابل الآلة الحقيقية. حرّك الذراع إلى ثلاث أو أربع وضعيات مختلفة بوضوح، وقارن الهيئة الخارجية في كل مرة. فالوضعية الواحدة لا تثبت إلا صحة الصفر عند تلك الوضعية. إذا تطابقت الهيئة في وضعية ولم تتطابق في غيرها، فلديك مشكلة في الاتجاه أو المقياس، لذا عُد وقِس نقطتين تفصل بينهما 60° على الأقل. وإذا لم تتطابق أبدًا رغم توافق الأرقام، فإن وضعية الصفر في CAD نفسها خاطئة (القسم 8).

Three photographs of the real OmArm One in clearly different poses: folded with the tool down, elbow high with the tool up, and reaching out

صوّر الذراع في وضعيات كهذه، واضبط أشرطة التمرير على الأرقام نفسها، وقارن الأشكال.

يمكنك أيضًا فحص شجرة TF مباشرة. أسرع طريقة لذلك داخل RViz: وسّع عنصر العرض TF فيعرض لك كل إطار يستقبله، ولكل إطار حالته الخاصة.

The TF display in RViz expanded, listing world, base_link, base_link_upper_1, link_1_1 to link_4_1, tool_link_1 and the five gripper frames, each marked Transform OK

ثلاثة عشر إطارًا، واحد لكل وصلة، ومنها world ، وجميعها بحالة OK. إذا كانت وصلة ما غائبة عن هذه القائمة، فإن robot_state_publisher لا ينشر إطارها. والسبب المعتاد أنه لا توجد عقدة تنشر زاوية ذلك المفصل على /joint_states؛ فإطار المفصل المتحرك لا يظهر إلا بعد وصول أول زاوية له. والسبب الآخر مفصل مفقود من ملف URDF. والإطار المفقود ليس أبدًا مشكلة في الشبكة (mesh).

للحصول على بنية الشجرة بدلًا من القائمة (الأب والابن ومعدل النشر على كل حافة)، صدّرها إلى ملف PDF:

ros2 run tf2_tools view_frames

view_frames يكتب ملف frames_<timestamp>.pdf في المجلد الحالي. وهو الملف الأنسب لإرفاقه بتقرير خطأ؛ أما قائمة RViz فهي الأنسب لفحص سريع مدته خمس ثوانٍ.

14. استكشاف الأخطاء وإصلاحها

العَرَض السبب الحل
Package 'omarm_description' not found بُنيت مساحة العمل لكن لم يُنفَّذ source لها source install/setup.bash في كل طرفية جديدة
لا يعرض RViz شيئًا، مع خطأ أحمر بشأن إطار يشير Fixed Frame إلى إطار لا ينشره أحد، وعادةً ما يكون الإطار الافتراضي في RViz، أي map اضبط Fixed Frame على base_link أو world
الشبكات (meshes) مفقودة، والوصلات مرسومة كمحاور صغيرة تعذّر العثور على مسارات ملفات STL، أو بُنيت الحزمة قبل إضافة الشبكات أعد البناء؛ وتحقق من أن setup.py يثبّت meshes/
الروبوت أكبر بألف مرة من حجمه الحقيقي مقياس الشبكة (scale) مفقود كل شبكة تحتاج إلى scale="0.001 0.001 0.001" لملفات STL المقيسة بالمليمتر
الوصلة في المكان الصحيح، والشبكة في مكان آخر خطأ في <origin> داخل <visual> قارن مع إحداثيات التجميع في CAD؛ فصحة شجرة TF لا تثبت صحة هذا
الذراع تسقط في Gazebo لا world كوصلة، ولا مفصل ثابت القسم 9
الذراع تنهار تحت تأثير الجاذبية لا توجد وحدات تحكم مفعّلة، لذا لا شيء يثبّت المفاصل ros2 control load_controller --set-state active ...
المفاصل في المحاكاة لا تتحرك أبدًا، والسجل يذكر /dev/ttyUSB0 وُسِّع النموذج باستخدام الواجهة الخلفية للعتاد تحقق من أن use_sim مُعلَن في omarm.xacro و يُمرَّر من ملف الإطلاق
وحدة التحكم تبلّغ عن مفصل مجهول الاسم في ملف الإعدادات لا يطابق ملف URDF حرفًا بحرف (مسافة مفقودة، أو شرطة سفلية، أو قوس زائد) انسخ الاسم مباشرة من جدول المفاصل في سكربت التدقيق
الإصبع الأيمن ثابت، والأيسر يتحرك مفصل المحاكاة (mimic) مفقود، أو تُرسَل أوامر إلى Revolute 9 وسم mimic واحد، وأبقِ Revolute 9 خارج ros2_control
Joint ... from the starting state is outside bounds المفصل يقع على حده تمامًا، والحد المخزَّن فيه منازل عشرية أقل من القيمة المحسوبة اترك هامشًا من درجة أو درجتين عند الطرف الذي تركن عليه الذراع
مفصل يتحرك في الاتجاه الخاطئ اتجاه المحور اعكس إشارة متجه المحور، لا إشارة أوامرك
النموذج والذراع يتطابقان في وضعية واحدة، لا في غيرها الاتجاه أو المقياس، لا الصفر أعد القياس بنقطتين تفصل بينهما 60° على الأقل

15. الأسئلة الشائعة

هل يمكنني تصدير ملف URDF من SolidWorks أو Onshape بدلًا من ذلك؟ نعم، لكليهما أدوات تصدير. يختلف الناتج في التفاصيل، لكن التدقيق في القسم 7 والتصحيحات في القسمين 8 و 9 تنطبق بالطريقة نفسها.

لماذا يستخدم النموذج الشبكات (meshes) للتصادم (collision) وللعرض المرئي (visual) معًا؟ من أجل الدقة، على حساب الحمل الحسابي. أشكال التصادم المبسطة أسرع، وهي الخيار المعتاد في الأنظمة الإنتاجية؛ أما في ذراع مكتبية من اثنتي عشرة قطعة صغيرة فلا يبرر التوفير مجموعة ثانية من الملفات.

هل ستبقى تصحيحاتي إذا أعدت التوليد من CAD؟ لا. التصدير الجديد يستبدل الملفات التي تكتبها أداة التصدير (الملفات الأربعة في urdf/، وملفي الإطلاق، وملفات إعدادات RViz والجسر، و package.xml و setup.py)، بما في ذلك كل التصحيحات التي فيها. لا يبقى إلا ملف YAML الخاص بوحدات التحكم، لأن أداة التصدير لا تكتبه. احفظ الحزمة المصحَّحة في نظام التحكم في الإصدارات، وقارن أي تصدير جديد بها باستخدام diff بدلًا من استبدالها به.

16. ما القادم

في الجزء 3، يستخدم MoveIt 2 هذا النموذج لحساب زوايا المفاصل التي تضع القابض عند نقطة ما، ولتخطيط مسار خالٍ من التصادم إليها. ويملأ برنامج تشغيل ESP32 الفرع use_sim:=false بوصفه واجهة ros2_control الخلفية الثانية (back end).

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

المصادر

أضف تعليق