مسیر یادگیری ROS 2  ·  کتاب آموزشی تصویری

فصل چهارم: بدنه ARCHO با URDF و Xacro

از یک XML خالی تا یک ربات دوچرخ محرک قابل شبیه‌سازی
پیش‌نیاز: فصل ۱ تا ۳
پروژه پیوسته: ربات ARCHO
ابزار: Xacro، check_urdf
زمان مطالعه: ۱۲۰ تا ۱۵۰ دقیقه
در این فصل چه می‌خوانیم ۴.۱قبل از کد: معماری کامل ARCHO ۴.۲یک پروژه، پنج Package ۴.۳URDF و Xacro چه هستند ۴.۴base_footprint و base_link ۴.۵ساخت بدنه: Visual، Collision، Inertial ۴.۶چرخ‌های محرک با یک Macro ۴.۷چرخ هرزگرد، LiDAR و IMU ۴.۸تبدیل و اعتبارسنجی مدل ۴.۹جمع‌بندی، واژه‌نامه و تمرین‌ها

۴.۱قبل از کد: معماری کامل ARCHO

تا اینجا ARCHO فقط در ذهن ما و در چند Node ساده وجود داشت. از این فصل به بعد، برای اولین بار به آن یک بدنه فیزیکی می‌دهیم. اما قبل از باز کردن هر ویرایشگری، یک اشتباه رایج مبتدی‌ها را دور بزنیم: عجله برای نوشتن کد. یک مهندس رباتیک حرفه‌ای، قبل از هر خط کد، یک سؤال ساده می‌پرسد: «چه اجزایی دارم و این اجزا چطور به هم وصل می‌شوند؟»

ARCHO یک ربات دوچرخ محرک (Differential Drive) است: دو چرخ اصلی که هرکدام مستقل کنترل می‌شوند، و یک چرخ هرزگرد برای تعادل. اگر سرعت دو چرخ برابر باشد، ربات مستقیم می‌رود؛ اگر یکی سریع‌تر باشد، می‌چرخد؛ و اگر یکی جلو و دیگری عقب برود، ربات تقریباً حول محور خودش می‌چرخد — دقیقاً همان چیزی که برای مانور در راهروهای باریک انبار لازم است.

مدل حرکتی Differential Drive هر دو چرخ برابر مستقیم چرخ راست سریع‌تر ↖ چرخش به چپ چرخ‌ها خلاف هم ↻ چرخش درجا
شکل ۴.۱ — سه رفتار پایه یک ربات دوچرخ محرک، فقط با تغییر نسبت سرعت دو چرخ.

پروژه کامل ARCHO از هشت لایه تشکیل می‌شود که هرکدام روی لایه قبلی خودش بنا می‌شود:

flowchart TB A["URDF / Xacro
بدنه ربات"] --> B["Gazebo
شبیه‌سازی فیزیکی"] B --> C["ros2_control
فرمان به حرکت"] A --> D["TF2
موقعیت اجزا"] D --> E["Odometry
تخمین حرکت"] E --> F["SLAM / AMCL
نقشه و موقعیت‌یابی"] F --> G["Navigation2
مسیریابی خودکار"] style A fill:#eef0ff,stroke:#3d4bf5,color:#211f1a,font-weight:bold style G fill:#eafaf3,stroke:#0e9e6e,color:#211f1a,font-weight:bold
🧠 قانون طلایی پروژه‌های رباتیک

هر لایه باید قبل از اتصال به لایه بعدی، به‌تنهایی آزمایش شود. اگر یک روز ARCHO در Nav2 حرکت نکرد، مشکل می‌تواند از URDF اشتباه باشد، جهت چرخ اشتباه، TF ناقص، Odometry غلط، LiDAR اشتباه یا Controller بد تنظیم‌شده. اگر همه‌چیز را یک‌جا بسازی، پیدا کردن علت تقریباً غیرممکن است؛ اگر مرحله‌به‌مرحله جلو بروی و هر مرحله را تأیید کنی، مشکل همیشه دقیقاً مشخص است. به همین دلیل این فصل و فصل‌های بعد، هرکدام یک Checkpoint مشخص در پایان دارند.

۴.۲یک پروژه، پنج Package

در فصل دوم یک Package تنها برای یادگیری کافی بود. اما یک پروژه واقعی ربات معمولاً بین چند Package تقسیم می‌شود — دقیقاً همان‌طور که یک کارخانه بین چند بخش تخصصی تقسیم می‌شود، نه یک سالن بزرگ و شلوغ.

archo_ws/
└── src/
    ├── archo_description/   # بدنه ربات: URDF/Xacro، Mesh، RViz
    ├── archo_gazebo/        # دنیای شبیه‌سازی، Worldها
    ├── archo_bringup/       # Launch اصلی که همه را روشن می‌کند
    ├── archo_slam/          # تنظیمات SLAM و نقشه‌ها
    └── archo_navigation/    # تنظیمات Nav2
مزیت تفکیک به چند Packageتوضیح
نگهداری آسان‌ترهر Package فقط یک مسئولیت دارد
عیب‌یابی آسان‌ترمشکل بلافاصله به یک حوزه مشخص محدود می‌شود
استفاده مجددمثلاً archo_description را می‌توان در پروژه دیگری هم استفاده کرد
شباهت به صنعتتقریباً تمام پروژه‌های ROS 2 متن‌باز و صنعتی از همین الگو استفاده می‌کنند

در این فصل فقط با archo_description کار داریم — Packageای که مسئول پاسخ به این سؤال است: «ربات چه شکلی است، چه قطعاتی دارد، و هر قطعه نسبت به بقیه کجاست؟»

cd ~/archo_ws/src
ros2 pkg create archo_description --build-type ament_cmake
cd archo_description
mkdir -p urdf meshes launch rviz config
📖 چرا ament_cmake به‌جای ament_python؟

Packageهایی که عمدتاً فایل‌های URDF، Xacro، Launch و Config دارند — نه کد Python اجرایی — معمولاً با ament_cmake ساخته می‌شوند. این مدل Build برای نصب و مدیریت فایل‌های غیر-اجرایی مناسب‌تر است. Packageهایی که Node پایتون دارند (مثل archo_bringup در فصل قبل) معمولاً ament_python هستند.

۴.۳URDF و Xacro چه هستند

📖 تعریف

URDF (Unified Robot Description Format) قالب استاندارد توصیف ساختار فیزیکی یک ربات در ROS است: چه قطعاتی دارد، Jointها کجا هستند، ظاهر و برخورد فیزیکی هر قطعه چگونه است، و جرم و ممان اینرسی آن چقدر است. Xacro یک لایه اضافه روی URDF است که امکان تعریف متغیر، محاسبه، Macro و تقسیم فایل به چند بخش را می‌دهد — چون نوشتن URDF خام برای یک ربات با ده‌ها قطعه به‌سرعت غیرقابل مدیریت می‌شود.

Xacro → URDF → robot_state_publisher → TF + RobotModel

فایل اصلی را می‌سازیم:

touch ~/archo_ws/src/archo_description/urdf/archo.urdf.xacro
<?xml version="1.0"?>
<robot
  xmlns:xacro="http://www.ros.org/wiki/xacro"
  name="archo">
</robot>

خط xmlns:xacro="..." قابلیت‌های Xacro مثل <xacro:property> و <xacro:macro> را فعال می‌کند. هر Link، Joint و Macro باید بین همین تگ باز و بسته <robot> قرار بگیرد.

۴.۴base_footprint و base_link

اولین Frameای که می‌سازیم، بدنه واقعی ربات نیست — یک Frame فرضی روی سطح زمین است:

<link name="base_footprint"/>
🧠 چرا بدنه را مستقیماً روی زمین نمی‌گذاریم؟

مرکز بدنه معمولاً بالاتر از زمین است — مثلاً اگر ارتفاع بدنه ۰.۱۸ متر باشد، مرکز آن حدود ۰.۰۹ متر بالاتر از زمین قرار دارد. اما Navigation2 دوست دارد یک Frame دقیقاً روی زمین داشته باشد، بدون Roll و Pitch. برای همین دو Frame می‌سازیم: base_footprint روی زمین، و base_link که Frame اصلی فیزیکی بدنه است و همه قطعات دیگر (چرخ‌ها، LiDAR، IMU) به آن متصل می‌شوند.

دو Frame پایه ARCHO base_link (مرکز بدنه) Fixed Joint — نصف ارتفاع بدنه base_footprint سطح زمین
شکل ۴.۲ — base_footprint دقیقاً روی زمین است؛ base_link در مرکز هندسی بدنه، با یک Fixed Joint به آن وصل می‌شود.
🔧 قرارداد محورهای ROS

در ROS همیشه: X = جلو، Y = چپ ربات، Z = بالا. این قرارداد را باید از همین ابتدا حفظ کنی — اگر جهت‌ها را اشتباه بگیری، ممکن است ربات عقب برود، LiDAR برعکس دیده شود، یا چرخش‌ها با TF ناسازگار شوند.

۴.۵ساخت بدنه: Visual، Collision، Inertial

ابتدا ابعاد بدنه را به‌جای عدد ثابت، به‌صورت Property تعریف می‌کنیم — دقیقاً همان فلسفه Parameter که در فصل اول یاد گرفتیم، این‌بار در دنیای URDF:

<xacro:property name="base_length" value="0.50"/>
<xacro:property name="base_width"  value="0.36"/>
<xacro:property name="base_height" value="0.18"/>
<xacro:property name="base_mass"   value="12.0"/>
⚠️ همه اندازه‌ها بر حسب متر هستند

URDF از واحدهای SI استفاده می‌کند. نوشتن <box size="50 36 18"/> یعنی یک بدنه ۵۰ در ۳۶ در ۱۸ متر — یک ساختمان، نه یک ربات! مقدار درست 0.50 0.36 0.18 است.

حالا خود base_link را با یک رنگ سفارشی می‌سازیم:

<material name="archo_blue">
  <color rgba="0.12 0.35 0.70 1.0"/>
</material>

<link name="base_link">
  <visual>
    <origin xyz="0 0 0" rpy="0 0 0"/>
    <geometry>
      <box size="${base_length} ${base_width} ${base_height}"/>
    </geometry>
    <material name="archo_blue"/>
  </visual>

  <collision>
    <origin xyz="0 0 0" rpy="0 0 0"/>
    <geometry>
      <box size="${base_length} ${base_width} ${base_height}"/>
    </geometry>
  </collision>

  <inertial>
    <origin xyz="0 0 0" rpy="0 0 0"/>
    <mass value="${base_mass}"/>
    <inertia
      ixx="${base_mass * (base_width*base_width + base_height*base_height) / 12.0}"
      ixy="0.0" ixz="0.0"
      iyy="${base_mass * (base_length*base_length + base_height*base_height) / 12.0}"
      iyz="0.0"
      izz="${base_mass * (base_length*base_length + base_width*base_width) / 12.0}"/>
  </inertial>
</link>
بخشبه چه سؤالی پاسخ می‌دهد
visualدر RViz/Gazebo چه شکلی دیده شود؟
collisionموتور فیزیک برای برخورد چه شکلی در نظر بگیرد؟
inertialجرم، مرکز جرم و مقاومت در برابر چرخش چقدر است؟
🌍 چرا Collision را ساده نگه می‌داریم

ظاهر (visual) بدنه می‌تواند یک Mesh پیچیده و زیبا از یک نرم‌افزار CAD باشد، اما collision معمولاً یک شکل ساده مثل Box است. اگر موتور فیزیک مجبور باشد برخورد را روی یک Mesh با میلیون‌ها سطح محاسبه کند، شبیه‌سازی به‌شدت کند می‌شود. این دقیقاً همان تعادل بین دقت ظاهری و کارایی محاسباتی است که در فصل بعد، هنگام کار با Gazebo، دوباره به آن برمی‌گردیم.

در پایان، دو Frame را با یک Fixed Joint به هم وصل می‌کنیم:

<joint name="base_footprint_joint" type="fixed">
  <parent link="base_footprint"/>
  <child link="base_link"/>
  <origin xyz="0 0 ${base_height / 2.0}" rpy="0 0 0"/>
</joint>

عدد ${base_height / 2.0} تصادفی نیست: چون Frame بدنه در مرکز Box قرار دارد، برای اینکه کف بدنه دقیقاً روی زمین بنشیند، مرکزش باید نصف ارتفاع بالاتر از base_footprint باشد.

⚠️ اشتباه رایج: نصف بدنه زیر زمین

اگر این origin را xyz="0 0 0" بنویسی، مرکز بدنه دقیقاً روی زمین قرار می‌گیرد و در نتیجه نصف بدنه داخل زمین فرو می‌رود — رایج‌ترین اشتباه اولین باری که این Joint را می‌نویسی.

تمرین آسان

فایل بالا را کامل بنویس، سپس ابعاد بدنه را به 0.55 × 0.40 × 0.20 متر تغییر بده. مقدار جدید origin در Joint چقدر باید باشد؟

۴.۶چرخ‌های محرک با یک Macro

چرخ‌های ARCHO با استوانه (Cylinder) ساخته می‌شوند. اما یک نکته ظریف وجود دارد: در URDF، محور طولی Cylinder به‌طور پیش‌فرض در راستای Z است، درحالی‌که چرخ باید حول محور Y بچرخد. پس Geometry را ۹۰ درجه حول X می‌چرخانیم.

<xacro:property name="pi" value="3.141592653589793"/>
<xacro:property name="wheel_radius" value="0.09"/>
<xacro:property name="wheel_width" value="0.04"/>
<xacro:property name="wheel_mass" value="0.8"/>
<xacro:property name="wheel_y_offset" value="0.20"/>

به‌جای نوشتن یک بلوک تکراری برای چرخ چپ و دوباره برای چرخ راست، یک Macro می‌سازیم — دقیقاً همان اصل «تکرار نکن» که در برنامه‌نویسی هم رعایت می‌کنیم:

<xacro:macro name="drive_wheel" params="prefix y_position">
  <link name="${prefix}_wheel_link">
    <visual>
      <origin xyz="0 0 0" rpy="${pi/2.0} 0 0"/>
      <geometry>
        <cylinder radius="${wheel_radius}" length="${wheel_width}"/>
      </geometry>
      <material name="wheel_black"/>
    </visual>
    <collision>
      <origin xyz="0 0 0" rpy="${pi/2.0} 0 0"/>
      <geometry>
        <cylinder radius="${wheel_radius}" length="${wheel_width}"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="${wheel_mass}"/>
      <origin xyz="0 0 0" rpy="0 0 0"/>
      <inertia
        ixx="${wheel_mass * (3*wheel_radius*wheel_radius + wheel_width*wheel_width) / 12.0}"
        ixy="0" ixz="0"
        iyy="${wheel_mass * wheel_radius * wheel_radius / 2.0}"
        iyz="0"
        izz="${wheel_mass * (3*wheel_radius*wheel_radius + wheel_width*wheel_width) / 12.0}"/>
    </inertial>
  </link>

  <joint name="${prefix}_wheel_joint" type="continuous">
    <parent link="base_link"/>
    <child link="${prefix}_wheel_link"/>
    <origin xyz="0 ${y_position} 0" rpy="0 0 0"/>
    <axis xyz="0 1 0"/>
  </joint>
</xacro:macro>

<xacro:drive_wheel prefix="left"  y_position="${wheel_y_offset}"/>
<xacro:drive_wheel prefix="right" y_position="${-wheel_y_offset}"/>
سؤالپاسخ
چرا type="continuous" و نه revolute؟revolute محدودیت زاویه دارد (مثلاً ۹۰-±)؛ چرخ باید بتواند بی‌نهایت دور بچرخد
چرا axis xyz="0 1 0"؟یعنی چرخ حول محور Y می‌چرخد؛ اگر اشتباه باشد، چرخ مثل سکه می‌چرخد یا ربات حرکت نمی‌کند
چرا Macro به‌جای کپی-پیست؟یک تغییر (مثلاً شعاع چرخ) در یک‌جا اعمال می‌شود و هر دو چرخ خودکار به‌روزرسانی می‌شوند
تمرین متوسط

فرض کن می‌خواهی به ARCHO یک نسخه بزرگ‌تر با شعاع چرخ ۰.۱۲ متر و فاصله چرخ‌ها ۰.۳۰ متر بسازی. فقط با تغییر Propertyها (نه خود Macro)، این کار چطور انجام می‌شود؟

۴.۷چرخ هرزگرد، LiDAR و IMU

دو چرخ محرک به‌تنهایی نمی‌توانند بدنه را متعادل نگه دارند؛ یک چرخ هرزگرد (Caster) کمکی لازم است. برای سادگی، آن را با یک کره می‌سازیم — کره اصطکاک جانبی کمتری دارد و شبیه‌سازی را پایدارتر می‌کند:

<link name="caster_link">
  <visual><geometry><sphere radius="0.04"/></geometry></visual>
  <collision><geometry><sphere radius="0.04"/></geometry></collision>
  <inertial>
    <mass value="0.15"/>
    <inertia ixx="0.000096" ixy="0" ixz="0" iyy="0.000096" iyz="0" izz="0.000096"/>
  </inertial>
</link>

<joint name="caster_joint" type="fixed">
  <parent link="base_link"/>
  <child link="caster_link"/>
  <origin xyz="-0.18 0 -0.05" rpy="0 0 0"/>
</joint>

حالا LiDAR و IMU — همان دو حسگری که در فصل اول واژه‌نامه‌شان را دیدیم، اینجا برای اولین بار Frame فیزیکی می‌گیرند:

<link name="laser_link">
  <visual><geometry><cylinder radius="0.045" length="0.04"/></geometry></visual>
  <collision><geometry><cylinder radius="0.045" length="0.04"/></geometry></collision>
  <inertial>
    <mass value="0.2"/>
    <inertia ixx="0.0002" ixy="0" ixz="0" iyy="0.0002" iyz="0" izz="0.0002"/>
  </inertial>
</link>

<joint name="laser_joint" type="fixed">
  <parent link="base_link"/>
  <child link="laser_link"/>
  <origin xyz="0.12 0 0.16" rpy="0 0 0"/>
</joint>

<link name="imu_link">
  <visual><geometry><box size="0.04 0.03 0.015"/></geometry></visual>
</link>

<joint name="imu_joint" type="fixed">
  <parent link="base_link"/>
  <child link="imu_link"/>
  <origin xyz="0 0 0.11" rpy="0 0 0"/>
</joint>
🧠 چرا IMU نزدیک مرکز جرم نصب می‌شود؟

وقتی IMU از مرکز دوران ربات فاصله بگیرد، شتاب‌های ناشی از چرخش هم وارد اندازه‌گیری‌اش می‌شوند و داده را نویزی‌تر می‌کنند. برای همین معمولاً IMU تا حد امکان نزدیک مرکز هندسی/جرمی بدنه نصب می‌شود.

ساختار درخت TF ما اکنون این شکل است:

graph TD F["base_footprint"] --> B["base_link"] B --> LW["left_wheel_link"] B --> RW["right_wheel_link"] B --> C["caster_link"] B --> L["laser_link"] B --> I["imu_link"] style F fill:#eafaf3,stroke:#0e9e6e style B fill:#eef0ff,stroke:#3d4bf5

این دقیقاً همان درخت TF است که در فصل پنجم به‌طور کامل درباره‌اش صحبت می‌کنیم.

۴.۸تبدیل و اعتبارسنجی مدل

یک فایل Xacro فقط زمانی مفید است که به URDF خالص تبدیل شود و اعتبارسنجی گردد. اول Workspace را Source کن:

dev@archo:~$ xacro ~/archo_ws/src/archo_description/urdf/archo.urdf.xacro -o /tmp/archo.urdf dev@archo:~$ check_urdf /tmp/archo.urdf robot name is: archo ---------- Successfully Parsed XML --------------- root Link: base_footprint has 1 child child(1): base_link child(1): left_wheel_link child(2): right_wheel_link child(3): caster_link child(4): laser_link child(5): imu_link

برای دیدن نمودار کامل ساختار:

cd /tmp && urdf_to_graphiz archo.urdf && xdg-open archo.pdf
خطای رایجعلامتراه‌حل
تگ XML بسته نشدهmismatched tagتعداد تگ‌های باز و بسته را بررسی کن
Property تعریف‌نشده (مثلاً base_lenght)خطای Xacro در تبدیلاملای Property را با تعریف آن مقایسه کن
جرم صفرGazebo در فصل بعد بی‌ثبات می‌شودmass باید همیشه مثبت باشد
واحد اشتباه (سانتی‌متر به‌جای متر)ربات غول‌پیکر یا ذره‌بینیURDF همیشه با متر کار می‌کند

در آخر، فایل‌ها را طوری تنظیم کن که هنگام Build نصب شوند — این بخش را در CMakeLists.txt اضافه کن:

install(
  DIRECTORY urdf meshes launch rviz config
  DESTINATION share/${PROJECT_NAME}
)
cd ~/archo_ws
colcon build --symlink-install --packages-select archo_description
source install/setup.bash
ros2 pkg prefix archo_description
✅ Checkpoint فصل چهارم
  • xacro archo.urdf.xacro -o /tmp/archo.urdf بدون خطا اجرا می‌شود.
  • check_urdf پیام Successfully Parsed XML می‌دهد.
  • درخت Link شامل base_footprint، base_link، دو چرخ، caster، laser و imu است.
  • ros2 pkg prefix archo_description مسیر Package نصب‌شده را برمی‌گرداند.

۴.۹جمع‌بندی فصل چهارم

برای اولین بار، ARCHO از یک نام روی کاغذ به یک مدل هندسی معتبر تبدیل شد: بدنه‌ای با جرم و اینرسی درست، دو چرخ محرک متصل با Joint از نوع continuous، یک چرخ هرزگرد، و محل نصب مشخص برای LiDAR و IMU. هنوز ربات هیچ حرکتی نمی‌کند و در هیچ دنیای مجازی‌ای وجود ندارد — این دقیقاً کاری است که در فصل بعد با Gazebo انجام می‌دهیم.

ارتباط با پروژه اصلی

پروژه ARCHO اکنون یک فایل archo.urdf.xacro معتبر و اعتبارسنجی‌شده دارد که در همه فصل‌های بعدی — Gazebo، ros2_control، TF2، SLAM و Nav2 — به‌عنوان پایه فیزیکی ربات استفاده می‌شود.

فصل بعد چه چیزی اضافه می‌کند

در فصل پنجم، درخت TF که همین الان با URDF ساختیم را از نزدیک بررسی می‌کنیم: چطور ROS 2 موقعیت هر قطعه ربات را نسبت به بقیه — و نسبت به دنیا — دنبال می‌کند.

واژه‌نامه فصل چهارم

URDF
Unified Robot Description Format؛ قالب استاندارد توصیف ساختار فیزیکی ربات در ROS.
Xacro
لایه‌ای روی URDF که متغیر (Property)، محاسبه و Macro را اضافه می‌کند.
Link
یک قطعه صلب از ربات (بدنه، چرخ، حسگر).
Joint
رابطه حرکتی یا ثابت بین دو Link؛ انواع رایج: fixed، continuous، revolute.
base_footprint
Frame فرضی روی سطح زمین، بدون Roll/Pitch؛ مرجع اصلی Navigation.
base_link
Frame اصلی فیزیکی بدنه ربات؛ محل اتصال سایر قطعات.
check_urdf
ابزار خط فرمان برای اعتبارسنجی ساختار XML و درخت Link/Joint یک فایل URDF.

خطاهای رایج فصل چهارم — جمع‌بندی