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

فصل پنجم: TF2 و Coordinate Frames

چطور ARCHO می‌فهمد هر قطعه‌اش کجاست
پیش‌نیاز: فصل ۴ (URDF/Xacro)
پروژه پیوسته: ربات ARCHO
ابزار: tf2_echo، view_frames
زمان مطالعه: ۸۰ تا ۱۰۰ دقیقه
در این فصل چه می‌خوانیم ۵.۱چرا یک ربات به سیستم مختصات نیاز دارد ۵.۲Transform، Frame و درخت TF ۵.۳Transform ثابت در برابر متحرک ۵.۴robot_state_publisher: از URDF به TF زنده ۵.۵زنجیره ناوبری: map → odom → base_link ۵.۶بازرسی TF از خط فرمان ۵.۷خطاهای رایج TF ۵.۸جمع‌بندی، واژه‌نامه و تمرین‌ها

۵.۱چرا یک ربات به سیستم مختصات نیاز دارد

در فصل قبل، ARCHO یک بدنه گرفت: چرخ‌ها، یک چرخ هرزگرد، محل نصب LiDAR و IMU. اما یک سؤال ساده هنوز بی‌جواب مانده: وقتی LiDAR می‌گوید «یک مانع در فاصله ۲ متری من است»، این «من» دقیقاً کجاست؟ و آن ۲ متر نسبت به چه نقطه‌ای اندازه‌گیری شده — مرکز ربات، لبه بدنه، یا خودِ LiDAR که چند سانتی‌متر جلوتر از مرکز نصب شده؟

این دقیقاً همان مشکلی است که TF2 (نسخه دوم کتابخانه Transform در ROS) حل می‌کند: یک سیستم استاندارد برای دنبال‌کردن اینکه هر قطعه ربات، در هر لحظه، کجا نسبت به بقیه قطعات و نسبت به دنیای اطراف قرار دارد.

🧠 تشبیه ساده

فرض کن در یک ساختمان اداری هستی و می‌خواهی به کسی آدرس بدهی. می‌توانی بگویی «طبقه سوم، اتاق ۱۲» (نسبت به ساختمان)، یا «دو قدم جلوتر از آسانسور» (نسبت به یک نقطه محلی). هر دو آدرس درست‌اند، فقط نسبت به مرجع‌های متفاوت. TF2 دقیقاً همین کار را برای ربات انجام می‌دهد: به هر بخش می‌گوید موقعیتش را نسبت به «والد» خودش تعریف کند، و بعد به‌طور خودکار محاسبه می‌کند که هر نقطه نسبت به هر نقطه دیگر کجاست.

۵.۲Transform، Frame و درخت TF

📖 تعریف

یک Frame (قاب مختصات) یک نقطه مرجع در فضا با جهت مشخص است — مثلاً base_link یا laser_link. یک Transform رابطه هندسی (جابه‌جایی + چرخش) بین دو Frame است. مجموعه همه Transformهای یک ربات یک درخت تشکیل می‌دهد: هر Frame دقیقاً یک والد دارد، اما می‌تواند چند فرزند داشته باشد.

درخت TF فعلی ARCHO — دقیقاً همان چیزی که در فصل قبل با URDF ساختیم — این شکل است:

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

وقتی می‌پرسیم «LiDAR نسبت به کجای زمین است؟»، TF2 مسیر laser_link → base_link → base_footprint را طی می‌کند و Transformها را زنجیره می‌کند تا جواب نهایی را بسازد. تو هیچ‌وقت مجبور نیستی این محاسبه را خودت دستی انجام دهی — این دقیقاً کاری است که TF2 پشت صحنه برایت می‌کند.

۵.۳Transform ثابت در برابر متحرک

در فصل چهارم دو نوع Joint دیدیم: fixed برای اتصالات ثابت (مثل LiDAR روی بدنه)، و continuous برای اتصالات متحرک (مثل چرخ‌ها). TF2 دقیقاً همین تمایز را نگه می‌دارد:

نوع Transformمثال در ARCHOچطور منتشر می‌شود
Staticbase_link → laser_linkیک‌بار، در ابتدای اجرا؛ هیچ‌وقت تغییر نمی‌کند
Dynamicbase_link → left_wheel_linkهر لحظه که چرخ می‌چرخد، دوباره منتشر می‌شود
Dynamic (ناوبری)odom → base_linkبا حرکت ربات، پیوسته به‌روزرسانی می‌شود
🔧 دید مهندسی

Static Transformها معمولاً با ابزار static_transform_publisher یا مستقیم از URDF منتشر می‌شوند و منبع پردازشی تقریباً صفر دارند، چون فقط یک‌بار فرستاده می‌شوند و روی گیرنده کش (Cache) می‌شوند. Dynamic Transformها با نرخ بالا (معمولاً چندین بار در ثانیه) منتشر می‌شوند، چون وضعیت‌شان دائماً تغییر می‌کند.

۵.۴robot_state_publisher: از URDF به TF زنده

حالا سؤال عملی: چه کسی واقعاً این Transformها را از روی فایل Xacro فصل قبل می‌سازد و منتشر می‌کند؟ پاسخ Node استانداردی به نام robot_state_publisher است.

flowchart LR A["Xacro/URDF
ساختار ثابت ربات"] --> B["robot_description
پارامتر رشته‌ای"] B --> C["robot_state_publisher"] D["/joint_states
زاویه لحظه‌ای هر Joint"] --> C C --> E["درخت TF زنده"] style C fill:#eef0ff,stroke:#3d4bf5,color:#211f1a,font-weight:bold style E fill:#eafaf3,stroke:#0e9e6e,color:#211f1a

این Node دو چیز را با هم ترکیب می‌کند: ساختار ثابت ربات (از URDF — اینکه کدام Link به کدام Joint وصل است) و زاویه لحظه‌ای هر Joint (از Topic /joint_states — مثلاً «چرخ چپ الان ۴۵ درجه چرخیده»). حاصل این ترکیب، یک درخت TF زنده است که هر بار زاویه چرخ‌ها عوض شود، به‌روز می‌شود.

🌍 حلقه کامل حرکت ARCHO

وقتی در فصل بعد به Gazebo برسیم، این حلقه کامل می‌شود: فرمان /cmd_vel به ros2_control می‌رود، چرخ‌های Gazebo می‌چرخند، موقعیت جدید Jointها روی /joint_states منتشر می‌شود، robot_state_publisher آن را می‌گیرد و درخت TF را به‌روزرسانی می‌کند، و در نهایت RViz همان چرخش را روی مدل نمایش می‌دهد.

Launch ساده برای دیدن این Node در عمل (بدون هیچ چیز دیگری):

from launch import LaunchDescription
from launch.substitutions import Command
from launch_ros.actions import Node
from launch_ros.parameter_descriptions import ParameterValue
from ament_index_python.packages import get_package_share_directory
import os


def generate_launch_description():
    pkg_path = get_package_share_directory('archo_description')
    xacro_file = os.path.join(pkg_path, 'urdf', 'archo.urdf.xacro')
    robot_description = ParameterValue(
        Command(['xacro ', xacro_file]), value_type=str)

    return LaunchDescription([
        Node(
            package='robot_state_publisher',
            executable='robot_state_publisher',
            parameters=[{'robot_description': robot_description}],
        ),
    ])

۵.۵زنجیره ناوبری: map → odom → base_link

درخت TF که تا اینجا دیدیم فقط بدنه ثابت ربات را پوشش می‌دهد. اما برای اینکه Nav2 (که در فصل‌های بعد سراغش می‌رویم) بتواند کار کند، به یک زنجیره بزرگ‌تر نیاز داریم:

map → odom → base_link → laser_link
حلقه زنجیرهچه کسی مسئول انتشار آن است
map → odomSLAM یا AMCL (فصل‌های ۱۰ و ۱۱)
odom → base_linkOdometry یا robot_localization (فصل ۹)
base_link → laser_linkrobot_state_publisher (همین فصل)
⚠️ یک قانون سخت‌گیرانه TF2

هیچ دو Node نباید هم‌زمان یک Transform یکسان را منتشر کنند. اگر مثلاً هم SLAM و هم یک Node دیگر بخواهند map → odom را منتشر کنند، TF2 دچار تناقض می‌شود و رفتار غیرقابل پیش‌بینی پیش می‌آید. هر حلقه از زنجیره، دقیقاً یک ناشر مشخص دارد.

چرا این زنجیره سه‌تکه‌ای طراحی شده، نه یک Transform مستقیم از map به base_link؟ چون هرکدام نرخ و قابلیت اطمینان متفاوتی دارند: Odometry سریع و نرم است اما به‌مرور خطا (Drift) جمع می‌کند؛ SLAM/AMCL کندتر است اما هر چند وقت یک‌بار آن خطا را با مقایسه با نقشه تصحیح می‌کند. جدا نگه‌داشتن این دو لایه، هم دقت و هم پایداری را با هم ممکن می‌کند.

۵.۶بازرسی TF از خط فرمان

برای دیدن رابطه بین دو Frame مشخص:

dev@archo:~$ ros2 run tf2_ros tf2_echo odom base_link At time 1732000012.4 - Translation: [0.842, 0.113, 0.000] - Rotation: in Quaternion [0.000, 0.000, 0.071, 0.997]

و برای دیدن کل درخت TF به‌صورت یک نمودار تصویری:

ros2 run tf2_tools view_frames

این دستور یک فایل PDF می‌سازد که کل درخت — از map تا کوچک‌ترین Frame حسگر — را نشان می‌دهد، همراه با اینکه هر Transform با چه نرخی منتشر می‌شود و آخرین بار کِی به‌روز شده.

تمرین آسان

بعد از اجرای robot_state_publisher با URDF فصل قبل، tf2_echo base_link laser_link را اجرا کن. مقدار Translation باید با origin که در Joint laser_joint نوشتی یکی باشد — چرا؟

۵.۷خطاهای رایج TF

خطاعلت رایجراه‌حل
Lookup would require extrapolationTimestampها هماهنگ نیستند یا TF دیر منتشر می‌شودبررسی نرخ انتشار TF و ساعت سیستم
TF در شبیه‌سازی کار نمی‌کند اما با ساعت واقعی درست استParameter use_sim_time تنظیم نشدهuse_sim_time: true روی همه Nodeهای مرتبط با Gazebo
دو Transform یکسان از دو منبعدو Node هم‌زمان یک حلقه زنجیره را منتشر می‌کنندفقط یک ناشر برای هر حلقه نگه دار
Frame اشتباه در RVizFixed Frame اشتباه انتخاب شدهFixed Frame را روی odom یا map تنظیم کن، نه base_link
🧠 چرا use_sim_time مهم است

وقتی Gazebo اجرا می‌شود، زمان شبیه‌سازی ممکن است کندتر یا سریع‌تر از زمان واقعی پیش برود (یادت هست، Real-Time Factor؟ همان که در فصل بعد با Gazebo می‌بینیم). اگر یک Node به ساعت واقعی سیستم متکی باشد درحالی‌که TF بر اساس ساعت شبیه‌سازی منتشر می‌شود، محاسبات TF2 دائماً با خطای extrapolation مواجه می‌شوند. تنظیم use_sim_time: true به همه Nodeها می‌گوید از همان ساعت شبیه‌سازی استفاده کنند.

۵.۸جمع‌بندی فصل پنجم

حالا ARCHO فقط یک بدنه ایستا نیست — یک سیستم مختصات زنده دارد. می‌دانیم robot_state_publisher چطور URDF و /joint_states را ترکیب می‌کند تا درخت TF بسازد، چرا زنجیره ناوبری به سه حلقه جدا تقسیم می‌شود، و چطور با tf2_echo و view_frames هر بخش از این سیستم را بازرسی کنیم.

✅ نقطه بازبینی یادگیری
  • می‌توانم تفاوت Frame و Transform را توضیح دهم.
  • می‌دانم چرا Static و Dynamic Transform با نرخ‌های متفاوت منتشر می‌شوند.
  • می‌توانم بگویم robot_state_publisher دقیقاً از چه دو ورودی، TF می‌سازد.
  • می‌دانم زنجیره map → odom → base_link → laser_link را چه Nodeهایی پر می‌کنند.
  • می‌توانم با tf2_echo و view_frames یک درخت TF را بازرسی کنم.
  • می‌دانم use_sim_time چیست و چرا در شبیه‌سازی حیاتی است.

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

پروژه ARCHO اکنون می‌تواند با robot_state_publisher درخت TF بدنه‌اش را زنده منتشر کند — پیش‌نیازی که فصل بعد، وقتی ARCHO را وارد دنیای فیزیکی Gazebo می‌کنیم، بلافاصله به آن نیاز داریم.

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

در فصل ششم اول با RViz — داشبورد دیداری ROS 2 — همین درخت TF و مدل ربات را برای اولین بار «می‌بینیم»؛ سپس در فصل هفتم وارد Gazebo می‌شویم، جایی که گرانش، اصطکاک و برخورد واقعاً روی ARCHO اثر می‌گذارند.

واژه‌نامه فصل پنجم

TF2
کتابخانه ROS 2 برای دنبال‌کردن موقعیت نسبی Frameهای مختلف ربات در طول زمان.
Frame
یک نقطه مرجع در فضا با جهت مشخص، مثل base_link یا laser_link.
Transform
رابطه هندسی (جابه‌جایی و چرخش) بین دو Frame.
robot_state_publisher
Nodeای که URDF و /joint_states را ترکیب می‌کند تا درخت TF زنده بسازد.
/joint_states
Topic استانداردی که زاویه یا موقعیت لحظه‌ای هر Joint متحرک را منتشر می‌کند.
use_sim_time
Parameterی که به یک Node می‌گوید از ساعت شبیه‌سازی به‌جای ساعت واقعی سیستم استفاده کند.

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