در فصل قبل، ARCHO یک بدنه گرفت: چرخها، یک چرخ هرزگرد، محل نصب LiDAR و IMU. اما یک سؤال ساده هنوز بیجواب مانده: وقتی LiDAR میگوید «یک مانع در فاصله ۲ متری من است»، این «من» دقیقاً کجاست؟ و آن ۲ متر نسبت به چه نقطهای اندازهگیری شده — مرکز ربات، لبه بدنه، یا خودِ LiDAR که چند سانتیمتر جلوتر از مرکز نصب شده؟
این دقیقاً همان مشکلی است که TF2 (نسخه دوم کتابخانه Transform در ROS) حل میکند: یک سیستم استاندارد برای دنبالکردن اینکه هر قطعه ربات، در هر لحظه، کجا نسبت به بقیه قطعات و نسبت به دنیای اطراف قرار دارد.
فرض کن در یک ساختمان اداری هستی و میخواهی به کسی آدرس بدهی. میتوانی بگویی «طبقه سوم، اتاق ۱۲» (نسبت به ساختمان)، یا «دو قدم جلوتر از آسانسور» (نسبت به یک نقطه محلی). هر دو آدرس درستاند، فقط نسبت به مرجعهای متفاوت. TF2 دقیقاً همین کار را برای ربات انجام میدهد: به هر بخش میگوید موقعیتش را نسبت به «والد» خودش تعریف کند، و بعد بهطور خودکار محاسبه میکند که هر نقطه نسبت به هر نقطه دیگر کجاست.
یک Frame (قاب مختصات) یک نقطه مرجع در فضا با جهت مشخص است — مثلاً
base_link یا laser_link. یک Transform رابطه هندسی
(جابهجایی + چرخش) بین دو Frame است. مجموعه همه Transformهای یک ربات یک درخت
تشکیل میدهد: هر Frame دقیقاً یک والد دارد، اما میتواند چند فرزند داشته باشد.
درخت TF فعلی ARCHO — دقیقاً همان چیزی که در فصل قبل با URDF ساختیم — این شکل است:
وقتی میپرسیم «LiDAR نسبت به کجای زمین است؟»، TF2 مسیر laser_link → base_link → base_footprint
را طی میکند و Transformها را زنجیره میکند تا جواب نهایی را بسازد. تو هیچوقت مجبور نیستی این محاسبه
را خودت دستی انجام دهی — این دقیقاً کاری است که TF2 پشت صحنه برایت میکند.
در فصل چهارم دو نوع Joint دیدیم: fixed برای اتصالات ثابت (مثل LiDAR روی بدنه)، و continuous برای اتصالات متحرک (مثل چرخها). TF2 دقیقاً همین تمایز را نگه میدارد:
| نوع Transform | مثال در ARCHO | چطور منتشر میشود |
|---|---|---|
| Static | base_link → laser_link | یکبار، در ابتدای اجرا؛ هیچوقت تغییر نمیکند |
| Dynamic | base_link → left_wheel_link | هر لحظه که چرخ میچرخد، دوباره منتشر میشود |
| Dynamic (ناوبری) | odom → base_link | با حرکت ربات، پیوسته بهروزرسانی میشود |
Static Transformها معمولاً با ابزار static_transform_publisher یا مستقیم از URDF منتشر
میشوند و منبع پردازشی تقریباً صفر دارند، چون فقط یکبار فرستاده میشوند و روی گیرنده کش (Cache)
میشوند. Dynamic Transformها با نرخ بالا (معمولاً چندین بار در ثانیه) منتشر میشوند، چون وضعیتشان
دائماً تغییر میکند.
حالا سؤال عملی: چه کسی واقعاً این Transformها را از روی فایل Xacro فصل قبل میسازد و منتشر میکند؟ پاسخ Node استانداردی به نام robot_state_publisher است.
این Node دو چیز را با هم ترکیب میکند: ساختار ثابت ربات (از URDF — اینکه کدام Link به کدام Joint وصل
است) و زاویه لحظهای هر Joint (از Topic /joint_states — مثلاً «چرخ چپ الان ۴۵ درجه
چرخیده»). حاصل این ترکیب، یک درخت TF زنده است که هر بار زاویه چرخها عوض شود، بهروز میشود.
وقتی در فصل بعد به 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}],
),
])
درخت TF که تا اینجا دیدیم فقط بدنه ثابت ربات را پوشش میدهد. اما برای اینکه Nav2 (که در فصلهای بعد سراغش میرویم) بتواند کار کند، به یک زنجیره بزرگتر نیاز داریم:
map → odom → base_link → laser_link
| حلقه زنجیره | چه کسی مسئول انتشار آن است |
|---|---|
map → odom | SLAM یا AMCL (فصلهای ۱۰ و ۱۱) |
odom → base_link | Odometry یا robot_localization (فصل ۹) |
base_link → laser_link | robot_state_publisher (همین فصل) |
هیچ دو Node نباید همزمان یک Transform یکسان را منتشر کنند. اگر مثلاً هم SLAM و هم یک Node دیگر
بخواهند map → odom را منتشر کنند، TF2 دچار تناقض میشود و رفتار غیرقابل پیشبینی پیش
میآید. هر حلقه از زنجیره، دقیقاً یک ناشر مشخص دارد.
چرا این زنجیره سهتکهای طراحی شده، نه یک Transform مستقیم از map به base_link؟
چون هرکدام نرخ و قابلیت اطمینان متفاوتی دارند: Odometry سریع و نرم است اما بهمرور خطا (Drift) جمع
میکند؛ SLAM/AMCL کندتر است اما هر چند وقت یکبار آن خطا را با مقایسه با نقشه تصحیح میکند. جدا نگهداشتن
این دو لایه، هم دقت و هم پایداری را با هم ممکن میکند.
برای دیدن رابطه بین دو Frame مشخص:
و برای دیدن کل درخت 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 نوشتی
یکی باشد — چرا؟
| خطا | علت رایج | راهحل |
|---|---|---|
Lookup would require extrapolation | Timestampها هماهنگ نیستند یا TF دیر منتشر میشود | بررسی نرخ انتشار TF و ساعت سیستم |
| TF در شبیهسازی کار نمیکند اما با ساعت واقعی درست است | Parameter use_sim_time تنظیم نشده | use_sim_time: true روی همه Nodeهای مرتبط با Gazebo |
| دو Transform یکسان از دو منبع | دو Node همزمان یک حلقه زنجیره را منتشر میکنند | فقط یک ناشر برای هر حلقه نگه دار |
| Frame اشتباه در RViz | Fixed Frame اشتباه انتخاب شده | Fixed Frame را روی odom یا map تنظیم کن، نه base_link |
وقتی 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 هر بخش از این سیستم را
بازرسی کنیم.
پروژه ARCHO اکنون میتواند با robot_state_publisher درخت TF بدنهاش را زنده منتشر کند — پیشنیازی که فصل بعد، وقتی ARCHO را وارد دنیای فیزیکی Gazebo میکنیم، بلافاصله به آن نیاز داریم.
در فصل ششم اول با RViz — داشبورد دیداری ROS 2 — همین درخت TF و مدل ربات را برای اولین بار «میبینیم»؛ سپس در فصل هفتم وارد Gazebo میشویم، جایی که گرانش، اصطکاک و برخورد واقعاً روی ARCHO اثر میگذارند.