多機器人系統設計

2026-07-18
  • ros2
  • multi-robot
  • namespace
  • tf-prefix
  • domain-id

問題定義

前面所有章節寫的範例,隱含的假設都是「系統裡只有一台機器人」——talkerbase_link/chatter 這些名稱在單機情境下不會有問題,但如果要同時運作兩台一模一樣的機器人(例如倉儲場景裡多台協同搬運的機器人),直接把同一套程式碼跑兩次,會發現兩台機器人的主題名稱、節點名稱、座標系名稱完全相同,彼此的資料會互相覆蓋、訂閱錯誤的來源,或是 TF 樹裡出現名稱衝突。這一節處理的是這類命名空間隔離問題。

核心概念說明

命名空間:讓同一套程式碼可以重複使用,各自獨立運作

ROS2 的節點、主題、服務都可以套上一個**命名空間(namespace)**前綴,讓原本寫死的名稱在執行期間自動加上區隔。例如同一份 talker 節點程式碼,分別用 robot1robot2 命名空間啟動兩次,節點名稱會自動變成 /robot1/talker/robot2/talker,主題也會變成 /robot1/chatter/robot2/chatter——程式碼本身完全不用修改,只在啟動時透過參數指定命名空間即可達成隔離。

TF Prefix:座標系需要另外處理的原因

主題跟節點的命名空間隔離是 ROS2 內建機制自動處理的,但 TF 座標系名稱不會自動套用命名空間前綴——這是新手很容易踩的坑:以為套了命名空間之後,base_link 就會自動變成 /robot1/base_link,實際上 TF 訊息裡的 frame_id 是純文字字串,不受命名空間機制影響,需要在 robot_state_publisher(第 4.3 節介紹過的節點)啟動時額外指定 TF prefix 參數,才能讓廣播出去的座標系名稱正確加上前綴,變成 robot1/base_link。如果兩台機器人的 TF 沒有做這個處理,兩者的 base_link 座標系名稱完全相同,TF 樹會出現名稱衝突,第二台機器人廣播的轉換會直接覆蓋或干擾第一台的資料。

ROS_DOMAIN_ID:網路層級的完全隔離

命名空間跟 TF prefix 解決的是「同一個網路裡,名稱不要衝突」的問題,但如果兩組機器人系統完全不需要互相知道對方存在(例如兩間不同教室各自的機器人示範,剛好用了同一個區域網路),更徹底的做法是用不同的 ROS_DOMAIN_ID(第 1 章提過的網路隔離機制),讓底層 DDS 完全看不到彼此,連「發現對方存在」這件事都不會發生,比命名空間隔離更進一步。

實作範例:用 launch 檔案啟動兩台獨立運作的機器人

延續第 3.4 節介紹的 launch 檔案機制,用 GroupAction 搭配 PushRosNamespace 把同一套節點群組,用不同命名空間各自啟動兩次。

檔案位置:~/ros2_ws/src/my_package/launch/multi_robot.launch.py

python
from launch import LaunchDescription
from launch.actions import GroupAction
from launch_ros.actions import Node, PushRosNamespace


def generate_launch_description():
    def make_robot_group(robot_name: str, x_offset: float):
        return GroupAction([
            PushRosNamespace(robot_name),
            Node(
                package="robot_state_publisher",
                executable="robot_state_publisher",
                name="robot_state_publisher",
                parameters=[{
                    "robot_description": open(
                        "/tmp/dual_arm_expanded.urdf"
                    ).read(),
                    "frame_prefix": f"{robot_name}/",
                }],
            ),
            Node(
                package="my_package",
                executable="talker",
                name="talker",
            ),
        ])

    return LaunchDescription([
        make_robot_group("robot1", x_offset=0.0),
        make_robot_group("robot2", x_offset=2.0),
    ])

frame_prefix 參數就是上面提到的 TF prefix 機制,PushRosNamespace 則負責讓群組裡所有節點的名稱與主題自動套上命名空間前綴。

執行

bash
ros2 launch my_package multi_robot.launch.py

預期輸出

bash
ros2 node list
text
/robot1/robot_state_publisher
/robot1/talker
/robot2/robot_state_publisher
/robot2/talker
bash
ros2 topic list
text
/robot1/chatter
/robot2/chatter
/tf
/tf_static

留意 /tf/tf_static 本身沒有被命名空間隔離(TF 主題通常設計成全域共享,讓所有節點都能查詢完整的 TF 樹),但裡面實際廣播的座標系名稱,會因為 frame_prefix 的設定而分別是 robot1/base_linkrobot2/base_link,兩者互不衝突:

bash
ros2 run tf2_ros tf2_echo robot1/base_link robot2/base_link
text
At time 0.0
- Translation: [2.000, 0.000, 0.000]
- Rotation: in Quaternion [0.000, 0.000, 0.000, 1.000]

這個結果證實了兩台機器人的座標系確實各自獨立存在於同一棵 TF 樹裡,且能正確查詢彼此之間的相對關係——這對協同任務(例如兩台機器人需要知道彼此的相對位置來避免碰撞)是必要的基礎。

常見錯誤與除錯技巧

錯誤一:套了命名空間之後,RViz2 裡的 RobotModel 顯示完全找不到 TF

text
No transform from [robot1/upper_arm] to frame [base_link]

原因:這正是上面提到的常見陷阱——命名空間讓主題與節點名稱自動加上前綴,但如果忘記同步設定 frame_prefixrobot_state_publisher 廣播出去的座標系名稱仍然是沒有前綴的 base_link,但 RViz2 的 Fixed Frame 如果設定成套用了前綴的 robot1/base_link,兩者對不上就會找不到 TF。

排除方式:確認 robot_state_publisher 的啟動參數裡有正確設定 frame_prefix,並且 RViz2 的 Fixed Frame 設定跟實際廣播出去的座標系名稱(可以用 ros2 run tf2_ros tf2_echoros2 topic echo /tf 確認)完全一致。

錯誤二:兩台機器人共用同一份地圖時,其中一台的定位結果明顯錯誤

原因:如果地圖本身沒有做命名空間隔離(例如兩台機器人都訂閱同一個全域的 /map 主題,這是合理且常見的設計——地圖通常應該是共享的),但負責定位的節點(例如 AMCL 或第 6.2 節介紹的 SLAM 定位模式)如果命名空間設定有誤,可能會不小心把 robot2 的感測器資料,拿去跟 robot1 的 TF 座標系做比對,導致定位結果錯亂。

排除方式:明確區分哪些資源應該是全域共享(地圖),哪些必須是每台機器人各自獨立(TF、感測器資料、定位結果),確認每個節點訂閱與發布的主題、使用的座標系前綴,是否符合這個「哪些共享、哪些獨立」的設計意圖,而不是簡單地把所有東西都套上或都不套命名空間。

小結

命名空間讓同一套節點程式碼可以重複部署成多個獨立運作的實例,TF 座標系需要額外用 frame_prefix 處理(不會自動跟隨命名空間),ROS_DOMAIN_ID 則提供更徹底的網路層級隔離。多機器人系統設計的核心,是清楚區分哪些資源該是全域共享(地圖),哪些必須各自獨立(TF、機器人本體狀態)。到這裡,第 9 章系統設計與最佳實踐(Lifecycle Node、進階 QoS、測試框架、多機器人設計)已經涵蓋了把前面章節的技術,組織成可維護、可測試、可擴展系統的核心考量——最後一章要處理的是怎麼把這樣的系統實際部署到正式環境。

延伸閱讀

常見問題

faq_01.log
命名空間隔離跟 ROS_DOMAIN_ID 隔離,應該用哪一種?
兩者解決不同層級的問題,通常會搭配使用而不是二選一。命名空間隔離的是『同一個 DDS domain 裡,不同機器人各自的主題名稱不要互相衝突』,機器人之間仍然可以看到彼此、能夠互相通訊(例如協同任務需要互相知道對方位置);ROS_DOMAIN_ID 則是網路層級的完全隔離,不同 domain 的節點完全看不到彼此,適合『這些機器人系統完全不需要互相知道對方存在』的場景,例如同一棟大樓裡好幾個獨立的機器人示範站,彼此不該互相干擾。
faq_02.log
多機器人之間如果需要共享地圖,應該怎麼設計?
常見做法是指定其中一台機器人(或獨立的伺服器節點)維護『權威版本』的地圖,其他機器人透過訂閱取得,而不是每台機器人各自獨立建圖後想辦法合併——多份獨立建立的地圖要事後精準合併是個困難的問題,牽涉到座標系對齊、量測誤差疊加等麻煩。實務上更常見的架構是幾台機器人共享感測資料,餵給同一個集中的建圖流程,或由其中一台機器人先建好地圖,其他機器人載入使用。
faq_03.log
TF prefix 會不會讓所有跟 TF 相關的程式碼都要跟著改?
如果程式碼裡把座標系名稱寫死成字串常數(例如直接寫 base_link 而不是用參數或設定檔),確實需要跟著改,這也是為什麼建議座標系名稱從一開始就設計成可設定的參數,而不是寫死在程式碼裡,這樣多機器人場景下只需要調整啟動參數,不需要修改程式碼本身。