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

فصل دوم: محیط توسعه ROS 2

از نقشه ذهنی به اولین Node واقعی روی سیستم خودت
مخاطب: مبتدی تا مهندس رباتیک
پیش‌نیاز: فصل اول (معماری ROS 2)
پروژه پیوسته: ربات ARCHO (آرکو)
زمان مطالعه: ۷۰ تا ۹۰ دقیقه
در این فصل چه می‌خوانیم ۲.۱از نقشه به شهر — چرا این فصل فرق دارد ۲.۲Workspace — کارخانه پروژه ۲.۳Package — واحد سازمان‌یافته کد ۲.۴اولین Node واقعی — خط به خط ۲.۵Build و اجرا با colcon ۲.۶دیدن Node از بیرون ۲.۷جمع‌بندی، واژه‌نامه و تمرین‌ها

۲.۱از نقشه به شهر

فصل اول را که تمام کردی، نقشه معماری ARCHO را در ذهن داشتی: می‌دانستی Node چیست، Topic چیست، کِی از Service استفاده کنیم و کِی از Action. اما یک نقشه، به‌تنهایی هیچ‌وقت رباتی را حرکت نمی‌دهد. حالا وقتش رسیده که واقعاً وارد شهر شویم: یک پوشه روی سیستم خودت باز کنی، یک فایل پایتون بنویسی، و برای اولین بار ببینی کلمه Node که تا الان فقط یک مفهوم انتزاعی بود، به یک پردازش واقعی و زنده تبدیل می‌شود که می‌توانی آن را در ترمینال ببینی، متوقفش کنی، و دوباره اجرایش کنی.

این فصل سه ایستگاه دارد: اول یاد می‌گیریم Workspace چیست و چرا هر پروژه به یکی از این‌ها نیاز دارد؛ بعد وارد Package می‌شویم که کوچک‌ترین واحد قابل‌build در ROS 2 است؛ و در پایان، اولین Node واقعی تیم ARCHO را خط‌به‌خط می‌خوانیم، Build می‌کنیم و روی سیستم اجرا می‌کنیم.

flowchart RL A["Workspace
پوشه اصلی پروژه"] --> B["Package
واحد سازمان‌یافته کد"] B --> C["Node
simple_node.py"] C --> D["colcon build"] D --> E["ros2 run"] E --> F["فصل ۳: Publisher و Subscriber واقعی"] style A fill:#eef0ff,stroke:#3d4bf5,color:#211f1a style F fill:#eafaf3,stroke:#0e9e6e,color:#211f1a,font-weight:bold

۲.۲Workspace — کارخانه پروژه

مشکل: یک پوشه شلوغ به اسم Desktop

تصور کن اولین روزی است که تیم ARCHO می‌خواهد کد بنویسد. یکی از اعضای تیم، بدون فکر زیاد، همه‌چیز را مستقیم روی Desktop می‌ریزد: camera.cpp، main.py، lidar_driver.cpp، motor.py، map.yaml. بعد از یک هفته، هیچ‌کس دیگر نمی‌داند کدام فایل مال کدام بخش ربات است، کدام نسخه جدید است، و کدام باید کامپایل شود. این دقیقاً همان دیواری است که هر تیم بدون یک ساختار پوشه‌بندی مشخص، دیر یا زود به آن می‌خورد.

راه‌حل ساده است: یک پوشه اصلی بساز و همه‌چیز را داخل آن مرتب نگه‌دار. ROS 2 دقیقاً همین ایده را با اسم Workspace رسمی کرده است.

📖 تعریف 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 برای چند هدف

در پروژه‌های واقعی معمولاً بیش از یک Workspace هم‌زمان روی سیستم داری، مثلاً ~/archo_ws برای کد اصلی ربات، و ~/simulation_ws برای آزمایش‌های شبیه‌سازی که هنوز آماده ادغام با کد اصلی نیستند. این جداسازی از قاطی‌شدن آزمایش‌های ناقص با کد پایدار جلوگیری می‌کند.

حالا برویم روی سیستم خودت

ترمینال را باز کن و این دستورها را به ترتیب اجرا کن:

dev@archo:~$ cd ~/archo_ws dev@archo:~/archo_ws$ tree -L 1 . ├── build ├── install ├── log └── src 5 directories, 0 files dev@archo:~/archo_ws$ ls src archo_bringup
⚠️ نکته درباره حروف بزرگ و کوچک

اگر tree -L 1 را اشتباه بنویسی، مثلاً tree -l 1 (با l کوچک)، لینوکس فکر می‌کند 1 اسم یک پوشه است و پیام [error opening dir] می‌دهد. -L بزرگ یعنی «فقط تا این عمق از پوشه‌ها را نشان بده»؛ این یکی از رایج‌ترین اشتباهات تایپی روز اول است.

تمرین آسان

در ترمینال خودت اجرا کن: cd ~/archo_ws، سپس pwd، سپس tree -L 1 و ls src. خروجی هرکدام را یادداشت کن و بگو کدام پوشه احتمالاً بیشترین تغییرات روزانه را می‌بیند.

۲.۳Package — واحد سازمان‌یافته کد

اگر 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

Package کوچک‌ترین واحد سازمان‌یافته و قابل Build در ROS 2 است. یک Package می‌تواند شامل Node، کد Python یا C++، فایل Launch، تعریف Parameter، Message، Service، Action، فایل URDF، تنظیمات RViz و تست باشد.

وارد اولین Package تیم ARCHO شو و ساختار آن را ببین:

dev@archo:~$ cd ~/archo_ws/src/archo_bringup dev@archo:~/archo_ws/src/archo_bringup$ tree -L 3 archo_bringup/ ├── archo_bringup/ │ ├── __init__.py │ └── simple_node.py ├── package.xml ├── resource/ ├── setup.cfg ├── setup.py └── test/
⚠️ چرا دو پوشه هم‌نام داریم؟

دیدن archo_bringup/archo_bringup/ اشتباه نیست. پوشه بیرونی، خود Package مربوط به ROS 2 است؛ پوشه داخلی، ماژول پایتون است که Nodeهای واقعی داخل آن قرار می‌گیرند.

ROS Package
└── Python Module
    └── ROS Nodes

فایل‌های مهم Package

فایلنقش
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++
🔧 دید مهندسی: Package در برابر Node

این دو را قاطی نکن. یک 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 ثبت کرده است.

۲.۴اولین 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() اهمیت زیادی دارد و همیشه همین الگو تکرار می‌شود:

flowchart TB A["rclpy.init()
اتصال برنامه به شبکه ROS 2"] --> B["node = SimpleNode()
ساخت واقعی Node"] B --> C["rclpy.spin(node)
روشن نگه‌داشتن Node و گوش‌دادن به رویدادها"] C --> D["node.destroy_node()
پس از Ctrl+C، پاک‌سازی منظم"] D --> E["rclpy.shutdown()
قطع اتصال از ROS 2"] style A fill:#eef0ff,stroke:#3d4bf5,color:#211f1a style C fill:#fdf3e4,stroke:#c8862c,color:#211f1a,font-weight:bold style E fill:#eafaf3,stroke:#0e9e6e,color:#211f1a
🧠 تشبیه ساده: اپراتور تلفن

rclpy.spin(node) را مثل استخدام یک اپراتور تلفن تصور کن. بدون spin()، اپراتور وارد شرکت می‌شود، یک جمله می‌گوید و بلافاصله به خانه می‌رود — Node ساخته می‌شود، پیام چاپ می‌شود و بلافاصله خاموش می‌شود. اما spin() می‌گوید: «پشت تلفن بمان و منتظر تماس‌ها باش.» همین «تماس‌ها» هستند که بعداً به‌صورت Message، درخواست Service، اجرای Timer یا Goal یک Action ظاهر می‌شوند.

و در پایان فایل، if __name__ == '__main__': main() فقط یک الگوی استاندارد پایتون است: اگر این فایل مستقیم اجرا شود (نه از طریق ros2 run)، باز هم main() فراخوانی شود.

۲.۵Build و اجرا با colcon

وقتی فایلی داخل src تغییر می‌کند، ROS هنوز نسخه نصب‌شده آن را نمی‌شناسد. باید Workspace را از ریشه Build کنیم:

cd ~/archo_ws
colcon build --packages-select archo_bringup
📖 colcon چیست؟

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 را اجرا کن:

dev@archo:~/archo_ws$ ros2 run archo_bringup simple_node [INFO] [simple_node]: Hello from ARCHO — first node alive!

Node بعد از چاپ پیام روشن می‌ماند — این طبیعی است، چون rclpy.spin(node) هنوز در حال اجراست. برای متوقف‌کردنش Ctrl+C بزن.

۲.۶دیدن Node از بیرون

این بخش یکی از عادت‌های مهمی است که باید از همین ابتدا در تو شکل بگیرد: هر 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.

🌍 حتی Log هم روی یک Topic منتقل می‌شود

وقتی self.get_logger().info(...) را صدا می‌زنی، پیام علاوه بر چاپ در ترمینال، روی Topic /rosout هم منتشر می‌شود. اگر در ترمینال دوم اجرا کنی:

ros2 topic echo /rosout

همان پیام‌های Log را به‌صورت زنده می‌بینی. این نشان می‌دهد حتی سیستم Log هم روی همان زیرساخت Publish/Subscribe فصل اول ساخته شده — در ROS 2 تقریباً همه‌چیز، حتی گزارش‌گیری داخلی، از همان چند مفهوم پایه استفاده می‌کند.

$ ros2 run archo_bringup simple_node [INFO] [simple_node]: Hello from ARCHO... (node stays alive — spin) ترمینال ۱ — اجرای Node $ ros2 node list /simple_node $ ros2 topic echo /rosout msg: "Hello from ARCHO..." ترمینال ۲ — بازرسی زنده
شکل ۲.۱ — همیشه Node را از یک ترمینال دوم هم بازرسی کن؛ این عادت، بعداً برای دیباگ Nodeهای پیچیده حیاتی می‌شود.
تمرین سخت‌تر

Node را در ترمینال اول اجرا کن، سپس در ترمینال دوم ros2 node info /simple_node را اجرا کن. فهرست کامل خروجی را یادداشت کن و بگو چرا Publishers و Subscribers این Node (به‌جز موارد داخلی ROS) خالی است.

۲.۷جمع‌بندی فصل دوم

تا اینجا مسیر کامل را طی کردی: یک Workspace ساختی، داخل آن یک Package پیدا کردی و کالبدشکافی‌اش کردی، اولین Node واقعی ARCHO را خط‌به‌خط خواندی، آن را با colcon build ساختی، با ros2 run اجرا کردی، و از یک ترمینال دوم زنده بازرسی‌اش کردی. این اولین باری بود که مفاهیم انتزاعی فصل اول را روی سیستم واقعی خودت لمس کردی.

✅ نقطه بازبینی یادگیری
  • می‌توانم بگویم Workspace چیست و چرا هر پروژه به یکی از این‌ها نیاز دارد.
  • می‌دانم نقش هرکدام از پوشه‌های src، build، install و log چیست.
  • می‌توانم Package را از Node تشخیص بدهم و بگویم رابطه‌شان چیست.
  • می‌توانم خط‌به‌خط یک فایل ساده rclpy را توضیح دهم.
  • می‌دانم چرا rclpy.spin() لازم است و بدون آن چه اتفاقی می‌افتد.
  • می‌توانم یک Node را Build، اجرا و از ترمینال دوم بازرسی کنم.

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

پروژه ARCHO حالا اولین Package و اولین Node واقعی‌اش را دارد — هنوز فقط یک پیام «سلام» چاپ می‌کند، اما زیرساخت Workspace و Package آماده است تا در فصل بعد این Node ساکت را به یک Publisher واقعی تبدیل کنیم.

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

در فصل سوم، همین simple_node را به یک Publisher تبدیل می‌کنیم که هر ثانیه یک Message روی یک Topic واقعی منتشر می‌کند، و یک Subscriber جدا می‌سازیم که آن را دریافت کند. برای اولین بار با چشم خودت می‌بینی که دو Node مستقل — دقیقاً همان‌طور که در فصل اول توصیف کردیم — دارند با هم حرف می‌زنند.

واژه‌نامه فصل دوم

Workspace
پوشه اصلی که تمام Packageها، فایل‌های Build و خروجی‌های یک پروژه ROS 2 در آن قرار می‌گیرند.
Package
کوچک‌ترین واحد سازمان‌یافته و قابل Build در ROS 2؛ می‌تواند شامل Node، Launch، Parameter و موارد دیگر باشد.
colcon
ابزار رسمی Build در ROS 2 که Packageهای داخل src را کامپایل و نصب می‌کند.
rclpy
کتابخانه کلاینت ROS 2 برای پایتون؛ پل ارتباطی برنامه پایتون با شبکه ROS 2.
spin()
تابعی که Node را روشن و منتظر رویدادهای ROS (Message، Service، Timer، Action) نگه می‌دارد.
Logger
سیستم گزارش‌گیری داخلی ROS که علاوه بر چاپ در ترمینال، پیام‌ها را روی Topic /rosout هم منتشر می‌کند.
entry_points
بخشی از setup.py که نام قابل‌اجرای یک Node را به فایل و تابع main آن متصل می‌کند.

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