فصل اول را که تمام کردی، نقشه معماری ARCHO را در ذهن داشتی: میدانستی Node چیست، Topic چیست، کِی از Service
استفاده کنیم و کِی از Action. اما یک نقشه، بهتنهایی هیچوقت رباتی را حرکت نمیدهد. حالا وقتش رسیده که
واقعاً وارد شهر شویم: یک پوشه روی سیستم خودت باز کنی، یک فایل پایتون بنویسی، و برای اولین بار ببینی
کلمه Node که تا الان فقط یک مفهوم انتزاعی بود، به یک پردازش واقعی و زنده تبدیل میشود
که میتوانی آن را در ترمینال ببینی، متوقفش کنی، و دوباره اجرایش کنی.
این فصل سه ایستگاه دارد: اول یاد میگیریم Workspace چیست و چرا هر پروژه به یکی از اینها نیاز دارد؛ بعد وارد Package میشویم که کوچکترین واحد قابلbuild در ROS 2 است؛ و در پایان، اولین Node واقعی تیم ARCHO را خطبهخط میخوانیم، Build میکنیم و روی سیستم اجرا میکنیم.
تصور کن اولین روزی است که تیم ARCHO میخواهد کد بنویسد. یکی از اعضای تیم، بدون فکر زیاد، همهچیز را
مستقیم روی Desktop میریزد: camera.cpp، main.py، lidar_driver.cpp،
motor.py، map.yaml. بعد از یک هفته، هیچکس دیگر نمیداند کدام فایل مال کدام
بخش ربات است، کدام نسخه جدید است، و کدام باید کامپایل شود. این دقیقاً همان دیواری است که هر تیم بدون
یک ساختار پوشهبندی مشخص، دیر یا زود به آن میخورد.
راهحل ساده است: یک پوشه اصلی بساز و همهچیز را داخل آن مرتب نگهدار. ROS 2 دقیقاً همین ایده را با اسم Workspace رسمی کرده است.
Workspace پوشه اصلیای است که تمام پروژههای ROS، Packageها، فایلهای Build و خروجیهای نهایی یک پروژه در آن قرار میگیرند. هر پروژه مستقل (مثلاً ARCHO در برابر یک بازوی رباتیک دیگر) معمولاً یک Workspace جدا دارد.
وقتی یک Workspace میسازی، این چهار پوشه را میبینی:
archo_ws/
├── src/
├── build/
├── install/
└── log/
| پوشه | نقش | چقدر باید دستی دستکاریاش کنی؟ |
|---|---|---|
src/ | محل تمام کدهای تو؛ هر Package یک زیرپوشه اینجاست | تقریباً همیشه — ۹۵٪ کار اینجاست |
build/ | فایلهای موقت کامپایل | تقریباً هیچوقت |
install/ | نسخه نهایی و قابلاجرای پروژه بعد از Build | فقط برای source کردن |
log/ | گزارش هر Build و اجرا | فقط هنگام دیباگ خطای Build |
Workspace را مثل یک کارخانه تصور کن. src جایی است که مهندسان طراحی میکنند؛
build خط مونتاژ کارخانه است که قطعات خام را میسازد؛ install انبار محصول
نهایی و آماده تحویل است؛ و log دفتر گزارشهای روزانه کارخانه است. اگر یک روز محصول خراب
دربیاید، اول سراغ گزارشهای log میروی تا ببینی کجای خط تولید مشکل پیش آمده.
در پروژههای واقعی معمولاً بیش از یک Workspace همزمان روی سیستم داری، مثلاً ~/archo_ws
برای کد اصلی ربات، و ~/simulation_ws برای آزمایشهای شبیهسازی که هنوز آماده ادغام با
کد اصلی نیستند. این جداسازی از قاطیشدن آزمایشهای ناقص با کد پایدار جلوگیری میکند.
ترمینال را باز کن و این دستورها را به ترتیب اجرا کن:
اگر tree -L 1 را اشتباه بنویسی، مثلاً tree -l 1 (با l کوچک)، لینوکس فکر میکند
1 اسم یک پوشه است و پیام [error opening dir] میدهد. -L بزرگ
یعنی «فقط تا این عمق از پوشهها را نشان بده»؛ این یکی از رایجترین اشتباهات تایپی روز اول است.
در ترمینال خودت اجرا کن: cd ~/archo_ws، سپس pwd، سپس tree -L 1
و ls src. خروجی هرکدام را یادداشت کن و بگو کدام پوشه احتمالاً بیشترین تغییرات روزانه را
میبیند.
اگر Workspace کارخانه است، Package یکی از واحدهای داخل آن کارخانه است. یک ربات واقعی مثل ARCHO معمولاً چند Package مستقل دارد، نه یک پوشه غولپیکر:
archo_ws/
└── src/
├── archo_description # مدل فیزیکی و URDF
├── archo_bringup # راهانداز کل سیستم
├── archo_control # کنترل موتور و حرکت
├── archo_navigation # تنظیمات Nav2 و مسیریابی
├── archo_sensors # درایور دوربین، LiDAR، IMU
└── archo_interfaces # تعریف Message/Service/Action سفارشی
Package کوچکترین واحد سازمانیافته و قابل Build در ROS 2 است. یک Package میتواند شامل Node، کد Python یا C++، فایل Launch، تعریف Parameter، Message، Service، Action، فایل URDF، تنظیمات RViz و تست باشد.
وارد اولین Package تیم ARCHO شو و ساختار آن را ببین:
دیدن archo_bringup/archo_bringup/ اشتباه نیست. پوشه بیرونی، خود Package مربوط به ROS 2
است؛ پوشه داخلی، ماژول پایتون است که Nodeهای واقعی داخل آن قرار میگیرند.
ROS Package
└── Python Module
└── ROS Nodes
| فایل | نقش |
|---|---|
package.xml | شناسنامه Package: نام، نسخه، توضیح، نگهدارنده، وابستگیها |
setup.py | به Python و ROS میگوید نام Package چیست، چه فایلهایی نصب شوند و چه Nodeهایی قابل اجرا هستند |
setup.cfg | محل نصب فایلهای اجرایی پایتون را مشخص میکند |
__init__.py | به پایتون میگوید این پوشه یک ماژول است؛ حتی اگر خالی باشد، وجودش لازم است |
برای فهمیدن اینکه یک Package با Python ساخته شده یا C++، کافی است این را بررسی کنی:
grep build_type package.xml
# <build_type>ament_python</build_type> → پایتون
# <build_type>ament_cmake</build_type> → معمولاً C++
این دو را قاطی نکن. یک Workspace میتواند چند Package داشته باشد، و یک Package میتواند چند Node داشته
باشد — مثلاً archo_sensors میتواند همزمان camera_node، imu_node
و battery_node را در خودش جای دهد. اما در پروژههای حرفهای بهتر است هر Package روی یک
حوزه مسئولیت مشخص متمرکز بماند و بیش از حد بزرگ و نامرتب نشود.
داخل archo_bringup اجرا کن: grep build_type package.xml و cat setup.py.
در setup.py دنبال خطی با console_scripts بگرد و بگو این خط چه نامی برای اجرای
Node ثبت کرده است.
داخل setup.py پروژه ARCHO، این خط را میبینی:
entry_points={
'console_scripts': [
'simple_node = archo_bringup.simple_node:main',
],
},
این یک خط، سه پیام مهم دارد که باید بتوانی جداگانه بازش کنی:
| بخش | معنی |
|---|---|
simple_node (سمت چپ) | نامی که در ros2 run استفاده میکنی |
archo_bringup.simple_node | مسیر فایل: archo_bringup/simple_node.py |
:main | وقتی Node اجرا شود، تابع main() باید فراخوانی شود |
حالا محتوای simple_node.py را باز میکنیم:
import rclpy
from rclpy.node import Node
class SimpleNode(Node):
def __init__(self):
super().__init__('simple_node')
self.get_logger().info('Hello from ARCHO — first node alive!')
def main(args=None):
rclpy.init(args=args)
node = SimpleNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
حالا خطبهخط جلو میرویم — همانطور که یک مهندس باتجربه کد یک همتیمی جدید را مرور میکند.
import rclpy
rclpy کتابخانه اصلی ROS 2 برای پایتون است — مخفف ROS Client Library for Python.
نسخه C++ آن rclcpp نام دارد. بدون rclpy، برنامه پایتون تو فقط یک اسکریپت
معمولی است و هیچ راهی برای وصلشدن به شبکه ROS 2 ندارد.
from rclpy.node import Node
اینجا کلاس آماده Node را وارد میکنیم — همان کلاسی که در فصل اول دربارهاش صحبت کردیم.
این کلاس امکاناتی مثل ساخت Publisher، Subscriber، Service، Action، Parameter، Timer و Logger را از قبل
برایمان آماده کرده؛ ما فقط از آن ارثبری میکنیم.
class SimpleNode(Node): و super().__init__('simple_node')
اینجا یک تفاوت مهم وجود دارد که خیلی از تازهکارها قاطی میکنند: SimpleNode نام کلاس پایتون
است، اما simple_node (داخل super().__init__) نامی است که این Node در
شبکه ROS 2 با آن شناخته میشود. به همین دلیل وقتی بعداً ros2 node list
را اجرا کنی، /simple_node را میبینی، نه SimpleNode.
self.get_logger().info(...)
این خط با استفاده از سیستم Log داخلی ROS یک پیام چاپ میکند. میشد از print() پایتون استفاده
کرد، اما Logger چند مزیت دارد: نام Node را نشان میدهد، سطح پیام را مشخص میکند، زمان را ثبت میکند و
پیامها را میتوان روی Topic /rosout هم دنبال کرد.
| سطح | کاربرد |
|---|---|
debug() | جزئیات فنی، فقط برای توسعه |
info() | وضعیت عادی و پیامهای معمولی |
warning() | احتمال بروز مشکل |
error() | خطای رخداده |
fatal() | خطای بسیار جدی که ادامه کار را غیرممکن میکند |
main() — قلب اجرای برنامهترتیب پنج خط داخل main() اهمیت زیادی دارد و همیشه همین الگو تکرار میشود:
rclpy.spin(node) را مثل استخدام یک اپراتور تلفن تصور کن. بدون spin()،
اپراتور وارد شرکت میشود، یک جمله میگوید و بلافاصله به خانه میرود — Node ساخته میشود، پیام چاپ
میشود و بلافاصله خاموش میشود. اما spin() میگوید: «پشت تلفن بمان و منتظر تماسها باش.»
همین «تماسها» هستند که بعداً بهصورت Message، درخواست Service، اجرای Timer یا Goal یک Action ظاهر
میشوند.
و در پایان فایل، if __name__ == '__main__': main() فقط یک الگوی استاندارد پایتون است:
اگر این فایل مستقیم اجرا شود (نه از طریق ros2 run)، باز هم main() فراخوانی شود.
وقتی فایلی داخل src تغییر میکند، ROS هنوز نسخه نصبشده آن را نمیشناسد. باید Workspace را
از ریشه Build کنیم:
cd ~/archo_ws
colcon build --packages-select archo_bringup
colcon ابزار Build رسمی ROS 2 است. وظیفهاش پیدا کردن تمام Packageهای داخل
src، بررسی وابستگیها، کامپایل کردن آنها، ساخت پوشه install و ثبت فایلهای
اجرایی است. دستور colcon build بهتنهایی همه Packageها را میسازد؛ اضافهکردن
--packages-select فقط یکی را میسازد و در پروژههای بزرگ زمان زیادی صرفهجویی میکند.
بعد از Build، باید Workspace را «فعال» کنی تا ترمینال بداند این Packageها کجا هستند:
source install/setup.bash
هر ترمینال جدیدی که باز میکنی، دوباره باید cd ~/archo_ws و
source install/setup.bash را اجرا کنی. فراموشکردن این مرحله، رایجترین دلیل پیام
«Package not found» در روزهای اول است.
حالا Node را اجرا کن:
Node بعد از چاپ پیام روشن میماند — این طبیعی است، چون rclpy.spin(node) هنوز در حال اجراست.
برای متوقفکردنش Ctrl+C بزن.
این بخش یکی از عادتهای مهمی است که باید از همین ابتدا در تو شکل بگیرد: هر Node که اجرا میکنی را از یک ترمینال دیگر هم بررسی کن. ترمینال اول را باز نگهدار (Node در حال اجراست)، یک ترمینال دوم باز کن و:
source /opt/ros/jazzy/setup.bash
cd ~/archo_ws
source install/setup.bash
ros2 node list
باید /simple_node را ببینی. برای اطلاعات کاملتر:
ros2 node info /simple_node
خروجی فهرستی از Subscriberها، Publisherها، Service Serverها و Clientها، و Action Serverها و Clientهای
این Node را نشان میدهد — فعلاً همه خالیاند، چون این Node تنها یک پیام چاپ میکند. اما احتمالاً دو Topic
داخلی ROS را هم میبینی: /parameter_events و /rosout.
وقتی self.get_logger().info(...) را صدا میزنی، پیام علاوه بر چاپ در ترمینال، روی Topic
/rosout هم منتشر میشود. اگر در ترمینال دوم اجرا کنی:
ros2 topic echo /rosout
همان پیامهای Log را بهصورت زنده میبینی. این نشان میدهد حتی سیستم Log هم روی همان زیرساخت Publish/Subscribe فصل اول ساخته شده — در ROS 2 تقریباً همهچیز، حتی گزارشگیری داخلی، از همان چند مفهوم پایه استفاده میکند.
Node را در ترمینال اول اجرا کن، سپس در ترمینال دوم ros2 node info /simple_node را اجرا
کن. فهرست کامل خروجی را یادداشت کن و بگو چرا Publishers و Subscribers این Node (بهجز موارد داخلی ROS)
خالی است.
تا اینجا مسیر کامل را طی کردی: یک Workspace ساختی، داخل آن یک Package
پیدا کردی و کالبدشکافیاش کردی، اولین Node واقعی ARCHO را خطبهخط خواندی، آن را با
colcon build ساختی، با ros2 run اجرا کردی، و از یک ترمینال دوم زنده بازرسیاش
کردی. این اولین باری بود که مفاهیم انتزاعی فصل اول را روی سیستم واقعی خودت لمس کردی.
پروژه ARCHO حالا اولین Package و اولین Node واقعیاش را دارد — هنوز فقط یک پیام «سلام» چاپ میکند، اما زیرساخت Workspace و Package آماده است تا در فصل بعد این Node ساکت را به یک Publisher واقعی تبدیل کنیم.
در فصل سوم، همین simple_node را به یک Publisher تبدیل میکنیم که هر ثانیه یک Message
روی یک Topic واقعی منتشر میکند، و یک Subscriber جدا میسازیم که آن را دریافت کند. برای اولین بار
با چشم خودت میبینی که دو Node مستقل — دقیقاً همانطور که در فصل اول توصیف کردیم — دارند با هم حرف
میزنند.
source install/setup.bash بعد از باز کردن ترمینال جدید.SimpleNode) با نام Node در شبکه ROS (simple_node).rclpy.spin() و تعجب از اینکه چرا Node بلافاصله خاموش میشود.colcon build ساده بهجای --packages-select در پروژههای بزرگ، که وقت زیادی تلف میکند.