تا اینجا ARCHO فقط در ذهن ما و در چند Node ساده وجود داشت. از این فصل به بعد، برای اولین بار به آن یک بدنه فیزیکی میدهیم. اما قبل از باز کردن هر ویرایشگری، یک اشتباه رایج مبتدیها را دور بزنیم: عجله برای نوشتن کد. یک مهندس رباتیک حرفهای، قبل از هر خط کد، یک سؤال ساده میپرسد: «چه اجزایی دارم و این اجزا چطور به هم وصل میشوند؟»
ARCHO یک ربات دوچرخ محرک (Differential Drive) است: دو چرخ اصلی که هرکدام مستقل کنترل میشوند، و یک چرخ هرزگرد برای تعادل. اگر سرعت دو چرخ برابر باشد، ربات مستقیم میرود؛ اگر یکی سریعتر باشد، میچرخد؛ و اگر یکی جلو و دیگری عقب برود، ربات تقریباً حول محور خودش میچرخد — دقیقاً همان چیزی که برای مانور در راهروهای باریک انبار لازم است.
پروژه کامل ARCHO از هشت لایه تشکیل میشود که هرکدام روی لایه قبلی خودش بنا میشود:
هر لایه باید قبل از اتصال به لایه بعدی، بهتنهایی آزمایش شود. اگر یک روز ARCHO در Nav2 حرکت نکرد، مشکل میتواند از URDF اشتباه باشد، جهت چرخ اشتباه، TF ناقص، Odometry غلط، LiDAR اشتباه یا Controller بد تنظیمشده. اگر همهچیز را یکجا بسازی، پیدا کردن علت تقریباً غیرممکن است؛ اگر مرحلهبهمرحله جلو بروی و هر مرحله را تأیید کنی، مشکل همیشه دقیقاً مشخص است. به همین دلیل این فصل و فصلهای بعد، هرکدام یک Checkpoint مشخص در پایان دارند.
در فصل دوم یک 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
Packageهایی که عمدتاً فایلهای URDF، Xacro، Launch و Config دارند — نه کد Python اجرایی — معمولاً با
ament_cmake ساخته میشوند. این مدل Build برای نصب و مدیریت فایلهای غیر-اجرایی مناسبتر
است. Packageهایی که Node پایتون دارند (مثل archo_bringup در فصل قبل) معمولاً
ament_python هستند.
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> قرار بگیرد.
اولین Frameای که میسازیم، بدنه واقعی ربات نیست — یک Frame فرضی روی سطح زمین است:
<link name="base_footprint"/>
مرکز بدنه معمولاً بالاتر از زمین است — مثلاً اگر ارتفاع بدنه ۰.۱۸ متر باشد، مرکز آن حدود ۰.۰۹ متر
بالاتر از زمین قرار دارد. اما Navigation2 دوست دارد یک Frame دقیقاً روی زمین داشته باشد، بدون Roll و
Pitch. برای همین دو Frame میسازیم: base_footprint روی زمین، و base_link
که Frame اصلی فیزیکی بدنه است و همه قطعات دیگر (چرخها، LiDAR، IMU) به آن متصل میشوند.
در ROS همیشه: X = جلو، Y = چپ ربات، Z = بالا. این قرارداد را باید از همین ابتدا حفظ کنی — اگر جهتها را اشتباه بگیری، ممکن است ربات عقب برود، LiDAR برعکس دیده شود، یا چرخشها با TF ناسازگار شوند.
ابتدا ابعاد بدنه را بهجای عدد ثابت، بهصورت 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 | جرم، مرکز جرم و مقاومت در برابر چرخش چقدر است؟ |
ظاهر (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 چقدر باید باشد؟
چرخهای 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)، این کار چطور انجام میشود؟
دو چرخ محرک بهتنهایی نمیتوانند بدنه را متعادل نگه دارند؛ یک چرخ هرزگرد (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 تا حد امکان نزدیک مرکز هندسی/جرمی بدنه نصب میشود.
ساختار درخت TF ما اکنون این شکل است:
این دقیقاً همان درخت TF است که در فصل پنجم بهطور کامل دربارهاش صحبت میکنیم.
یک فایل Xacro فقط زمانی مفید است که به URDF خالص تبدیل شود و اعتبارسنجی گردد. اول Workspace را Source کن:
برای دیدن نمودار کامل ساختار:
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
xacro archo.urdf.xacro -o /tmp/archo.urdf بدون خطا اجرا میشود.check_urdf پیام Successfully Parsed XML میدهد.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 موقعیت هر قطعه ربات را نسبت به بقیه — و نسبت به دنیا — دنبال میکند.
axis xyz="0 1 0" روی Joint چرخها.