Learning Objectives
- 說明 launch 檔的用途,讀懂 Python launch 檔的結構
- 畫出 map、odom、base_link、laser 的 TF 樹,說明為什麼要分 map 和 odom
- 用 lookup_transform 查兩個座標系的關係,處理查不到的情況
- 用 StaticTransformBroadcaster 發布固定的轉換
- 把光達的點換到 odom 座標系
十幾個節點,不可能開十幾個終端機
一台能導航的機器人,光達、里程計、定位、規劃、控制加起來可能有十幾個節點,每個還有自己的參數。一個一個 ros2 run 既麻煩又容易漏掉。
launch 檔用一個檔案描述「這個系統要啟動哪些節點、各帶什麼參數」,一個指令全部帶起來。可以用 XML、YAML 或 Python 寫,官方三種都支援。下面是 Python 的樣子:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(package='my_robot', executable='limiter', name='limiter',
parameters=[{'max_speed': 0.15}]),
Node(package='my_robot', executable='planner', name='planner',
remappings=[('/cmd_vel_raw', '/planner/cmd_vel')]),
])
ros2 launch my_robot robot_launch.py # 一次啟動
ros2 run my_robot limiter # 只跑一個節點,除錯時常用
parameters在啟動時帶入參數,取代第 5 課的--ros-args -p。remappings把節點裡的 topic 名稱換成別的,不用改程式。這是節點能重複使用的關鍵。
這個 launch 檔瀏覽器裡不會執行,它要真實的 ROS 2 環境(第 20 課會說明)。下面的 robot-lab 用「同時跑兩支程式」加上「帶入參數」達到一樣的效果,你可以對照看。
TF:把所有座標系接成一棵樹
第 2 課算過:光達的點要換到地圖,要把 T_map_base 和 T_base_laser 相乘。真實機器人有更多座標系,有的會動(車身),有的不動(感測器的安裝位置),每個節點都自己算太容易出錯。
TF(tf2 函式庫)把這件事集中處理:
- 誰知道某兩個座標系的關係,誰就發布這個轉換(
/tf給會動的,/tf_static給固定的)。 - 誰需要,就查詢任意兩個座標系的關係,TF 沿著樹把中間的轉換乘起來。
每個座標系只有一個父座標系,所以整體是一棵樹。移動機器人的標準結構由 REP-105 定義:
map ──▶ odom ──▶ base_link ──▶ laser
| 座標系 | 意思 | 特性 |
|---|---|---|
base_link |
固定在機器人身上 | 跟著機器人走 |
odom |
里程計的世界 | 連續,數字不會跳;但誤差會一直累積,沒有上限 |
map |
地圖的世界 | 長期不漂移;但定位修正時數字可能突然跳一下 |
為什麼要同時有 map 和 odom? 因為兩種需求互相衝突。控制機器人走直線要連續的數字:位置突然跳 10 公分,控制器會以為自己偏了,猛然轉向。要知道自己在建築物哪個位置,就要不漂移的數字。所以兩個都留,各給適合的人用:避障、短距離控制看 odom;全域規劃看 map。
REP-105 還規定了一個細節:每個座標系只能有一個父座標系,所以定位演算法不發布 map→base_link(base_link 的父座標系已經是 odom),而是算出 map→odom 來修正。
定位修正時,哪個數字會跳
用矩陣把這條鏈算一次,看定位修正時發生什麼:odom 裡的數字沒有變,所以依賴它的控制器不受影響;map 裡的位置被修正了。
在程式裡查詢與發布
查詢要一個 Buffer(存資料)和一個 TransformListener(把 /tf 收進來):
self.tf_buffer = Buffer()
self.tf_listener = TransformListener(self.tf_buffer, self)
t = self.tf_buffer.lookup_transform('odom', 'laser', Time())
lookup_transform(target, source, time) 回傳的轉換,可以把 source 座標系裡的點換到 target。Time() 表示「最新的一筆」。參數順序很容易記反,反了不會報錯,只會得到反方向的轉換。
查詢一定要處理失敗。程式剛啟動時可能還沒收到 TF,發布的節點也可能晚點才起來,所以官方範例都用 try 包起來,查不到就等下一輪:
| 錯誤訊息的關鍵字 | 通常代表 | 怎麼查 |
|---|---|---|
does not exist |
這個座標系名稱沒有人發布過 | 名稱有沒有打錯;發布的節點有沒有啟動 |
not part of the same tree |
兩個座標系都有,但沒有接在同一棵樹上 | 中間少一段;常見是 map→odom 沒有人發布 |
| 時間相關(外插) | 你要的時間點超出資料範圍 | 感測器時間戳記與 TF 的時間差。模擬環境只留最新一筆,不會出現這個錯誤 |
真實環境可以用 ros2 run tf2_ros tf2_echo map base_link 印出轉換,用 ros2 run tf2_tools view_frames 把整棵樹畫成圖。
發布固定的轉換用 StaticTransformBroadcaster,它用第 4 課的 TRANSIENT_LOCAL,晚啟動的節點也拿得到;會動的用 TransformBroadcaster,要持續發布。兩者都是填一個 TransformStamped:header.frame_id 是父座標系、child_frame_id 是子座標系。
確認一下
依 REP-105,定位演算法想修正機器人在地圖上的位置,它應該發布哪個轉換?
機器人要沿著走廊貼牆走 2 公尺(短距離控制)。該用哪個座標系的位置?
lookup_transform 丟出「Could not find a connection … not part of the same tree」。最可能的原因是?
感測器的安裝位置為什麼發布在 /tf_static,不用計時器每秒發布到 /tf?
下面這段 launch 設定,哪個敘述正確?
Node(package='my_robot', executable='limiter', parameters=[{'max_speed': 0.15}])
重點整理
- launch 檔描述要啟動哪些節點與參數,
ros2 launch一次帶起來;ros2 run只跑一個。 - TF 把所有座標系接成一棵樹:
map → odom → base_link → laser(REP-105)。 odom連續但會漂移,適合短距離控制;map不漂移但會跳,適合全域定位。定位演算法發布的是map→odom。lookup_transform(target, source, Time())把 source 的點換到 target。一定要處理查不到的情況;參數順序反了不會報錯。does not exist查名稱,not part of the same tree查少了哪一段。
ROS 2 的基本工具到這裡告一段落。下一課開始看機器人怎麼動:左右輪速度怎麼變成移動,里程計的誤差又是從哪裡來的。
(launch 檔的寫法改寫自 ROS 2 官方文件 Creating a launch file,CC BY 4.0,有修改。)