Learning Objectives
- 說明把機器人程式拆成多個節點的好處與代價
- 用 create_publisher、create_subscription、create_timer 寫節點
- 說明發布者與訂閱者怎麼靠 topic 名稱配對,名稱不同會發生什麼
- 用 ros2 指令看節點與 topic 的狀態
- 寫一個訂閱再發布的轉接節點
- 說明回呼做太久為什麼會擋住同一個節點的其他工作
光達壞了,不該連馬達都停擺
如果光達驅動、地圖、路徑規劃、馬達控制全部寫在一支程式裡,光達驅動多一個 bug,整台機器人一起當掉;想換一顆不同牌子的光達,也要改到其他部分的程式。
ROS 2 的做法是拆成很多個節點(node),一個節點負責一件事,每個節點可以獨立啟動、重新啟動與替換。節點之間最常用的溝通方式是 topic:
/scan /cmd_vel
光達驅動 ──────────▶ 避障節點 ──────────▶ 馬達驅動
▲
路徑規劃 ───────────────┘
/plan
- 發布者(publisher)把訊息送到一個 topic;訂閱者(subscriber)收這個 topic 的訊息。
- 雙方只靠 topic 名稱與訊息型別配對。發布者不知道誰在收、有幾個人在收,甚至不知道有沒有人在收。
這叫鬆耦合:多加一個記錄資料的節點,光達驅動一行都不用改。代價是你不會在程式裡看到「誰接誰」,名字打錯也不會有錯誤訊息,只會安靜地收不到東西,下面就會實際碰到。
節點裡的三個基本元件
一個節點常用的東西就這三個:
self.pub = self.create_publisher(String, 'topic', 10) # 型別、名稱、佇列大小
self.create_subscription(String, 'topic', self.on_msg, 10) # 型別、名稱、回呼、佇列大小
self.create_timer(0.5, self.tick) # 週期(秒)、回呼
訊息是一個物件,先建立、填欄位,再 publish。訂閱和計時器都給一個回呼(callback):你不會自己呼叫它,資料來了或時間到了才會被呼叫。
那誰來呼叫?rclpy.spin(node)。它是一個迴圈:等到有事發生,就呼叫對應的回呼,做完再等下一件事。沒有 spin,計時器不會響、訊息也不會進來。 程式寫成這樣的好處是不必自己管「現在該做哪件事」,代價是要習慣程式的順序不是由上往下,而是由事件決定。
佇列大小(那個 10)是訊息來不及處理時最多留幾筆,下一課會看到它的影響。
一步一步看回呼的順序
為了方便看,這個節點自己發布、自己訂閱。注意 publish 那一行執行完,on_msg 並沒有立刻被呼叫:程式會先把 tick 走完,回到 spin,才輪到 on_msg。回呼是排隊一個一個做的。
用 ros2 指令看系統現在的樣子
節點之間的連結不寫在程式裡,所以要靠工具來看。真實環境最常用的是這幾個:
| 指令 | 回答什麼問題 |
|---|---|
ros2 node list |
現在有哪些節點 |
ros2 topic list -t |
有哪些 topic,各是什麼型別 |
ros2 topic echo /scan |
這個 topic 現在在傳什麼內容 |
ros2 topic hz /scan |
每秒幾筆,有沒有變慢 |
ros2 topic info /scan |
幾個發布者、幾個訂閱者 |
除錯的順序通常是:節點有沒有起來 → topic 名稱對不對 → 兩邊數量是不是 0 → 有沒有訊息在跑 → 頻率夠不夠。
下面 robot-lab 的「ros2 指令」面板可以按這些指令,內容由這次模擬的狀態算出,只是格式仿照真實指令,所以有些地方會不一樣。例如真實環境的 ros2 topic list 還會多出系統自帶的 /parameter_events 和 /rosout,模擬環境沒有這兩個。
回呼是排隊做的
預設情況下,一個節點的回呼是一個做完才換下一個,同時只有一個在跑。所以回呼應該短:收到資料、存起來、馬上結束。
如果某個回呼要花 0.5 秒,這 0.5 秒裡:
- 計時器到了也不會響,控制指令送得更慢
- 新的感測器資料在佇列裡排隊,你處理的永遠是舊資料
- 佇列滿了,最舊的資料直接被丟掉
機器人一邊走,這些延遲就是一段「閉著眼睛走」的距離。下面實際看。
確認一下
節點 A 發布到 /scan,節點 B 訂閱 scan(沒有開頭的斜線),兩個節點都沒有設定命名空間。B 收得到嗎?
下面這個節點建立了計時器,但 main 只寫了 node = Talker() 就結束,沒有呼叫 spin。會發生什麼?
你開了 ros2 topic echo /cmd_vel 卻什麼都沒有。下面哪一個是最合理的下一步?
一個節點訂閱 /scan(每 0.2 秒一筆),回呼要花 1 秒,同一個節點還有一個 0.1 秒的計時器發布速度指令。下面哪個描述最接近實際情況?
為什麼限速要做成獨立節點,不寫在規劃節點裡?
重點整理
- 節點一個負責一件事,可以獨立啟動與替換;topic 是節點之間最常用的溝通方式。
- 發布者與訂閱者只靠名稱與型別配對,彼此不知道對方。名稱不合不會報錯,只會收不到。
- 回呼由
rclpy.spin()呼叫。沒有 spin,計時器與訂閱都不會動。 - 預設一次只跑一個回呼,所以回呼要短;花時間的工作放到別的節點。
- 除錯先用
ros2 node list、topic list、topic info、topic echo、topic hz確認事實。
下一課看訊息本身,以及另一種「訂閱了卻收不到」的原因:QoS。