Learning Objectives
- 判斷一個功能該用 topic 還是 service
- 寫 service 伺服器與用戶端,使用 wait_for_service、call_async、spin_until_future_complete
- 說明為什麼不能在回呼裡用 client.call 等回應
- 用參數調整節點的設定,並在 add_on_set_parameters_callback 裡拒絕不合理的值
有些事情不是持續的資料流
光達每秒掃好幾次,topic 很合適。但「把地圖存檔」「重設里程計」「請告訴我這個點能不能到」這些事情,是有人問一次、對方答一次,而且問的人想知道答案。用 topic 做的話,要自己配對「哪個回答對應哪個問題」,也不知道對方到底有沒有收到。
service 就是問一句答一句:用戶端(client)送請求(request),伺服器(server)算完回回應(response)。
| topic | service | |
|---|---|---|
| 形式 | 一直發,誰想聽就聽 | 問一次答一次 |
| 對象 | 多對多,發布者不知道誰在收 | 一個請求對應一個回應 |
| 適合 | 感測器、速度指令、狀態 | 查詢、切換設定、觸發很快做完的動作 |
| 不適合 | 需要知道對方是否完成 | 高頻率資料;要跑很久的工作 |
要跑很久的工作不要用 service(例如「走到教室門口」),因為用戶端看不到進度也不能取消,那是下一課 action 的工作。
一步一步看非同步呼叫
call_async 送出請求後馬上回來,手上拿到一個 future(一張還沒兌現的取貨單)。要讓執行器有機會處理,答案才會進來。這裡用 spin_until_future_complete:spin 到這個 future 完成為止。為了方便看,伺服器和用戶端放在同一個節點。
在回呼裡等回應會卡住
rclpy 還有一個同步的 client.call(request),看起來比較簡單:呼叫、等、拿到答案。官方原始碼的警告是:不要在回呼裡呼叫它,可能死結或逾時。
原因和第 3 課的「回呼排隊」一樣。回應要由執行器處理,但執行器正在跑你的回呼,而你的回呼在等回應:
你的回呼:等回應回來才結束
執行器: 等回呼結束,才能處理回應
兩邊互相等,永遠不會結束。機器人的控制迴圈就這樣停了,也沒有任何錯誤訊息。解法是用 call_async 加上 done callback,把「收到回應之後要做的事」寫成另一個回呼,這樣目前的回呼可以先結束:
future = self.cli.call_async(req)
future.add_done_callback(self.on_response)
真實環境裡這種卡住不會有任何提示;模擬環境為了教學,偵測到就會停下來說明。
參數:不改程式就能調整的設定
速度上限、感測器週期、PID 的係數,這些數字會因機器人與場地不同而變。寫死在程式裡的話,每調一次就要改程式、重新啟動。ROS 2 的參數(parameter)是節點自己帶的設定值:
self.declare_parameter('max_speed', 0.3) # 宣告,並給預設值
speed = self.get_parameter('max_speed').value # 讀
參數一定要先宣告才能用(否則丟出例外),型別由預設值決定,例如 0.3 就是浮點數。可以在啟動時給值,也可以在執行中從外面改:
ros2 run my_robot driver --ros-args -p max_speed:=0.2
ros2 param set /driver max_speed 0.15
外面改得到,也就可能改錯。 有人打成 5.5,或是另一個節點傳來算錯的值,機器人就可能在人群裡全速前進。所以要加一道檢查,add_on_set_parameters_callback 註冊的函數會在參數改變之前被呼叫,回傳不成功就拒絕,參數維持原值。
有兩個細節要注意:
get_parameter要在每次用到時讀(或在 post-set callback 裡更新),只在__init__讀一次就不會跟著改。- 這只是軟體上的第一道關。真實機器人的硬體與驅動層也應該有上限與緊急停止,不能只靠一個 Python 判斷。
確認一下
下面哪一個功能最適合用 service?
future = self.cli.call_async(req) 的下一行寫 print(future.result().sum),中間沒有 spin。會發生什麼?
一個計時器回呼裡寫 response = self.cli.call(req),伺服器在另一支程式、也正常運作。這個節點只用預設的單執行緒 spin。會怎樣?
on-set callback 回傳 successful=False 之後,參數的值是?
節點只在 init 寫 self.max_speed = self.get_parameter('max_speed').value,之後 tick 都用 self.max_speed。有人下 ros2 param set 改值會怎樣?
重點整理
- topic 是持續的資料流,service 是一問一答。要跑很久的工作留給 action。
- 用戶端先
wait_for_service,再call_async;答案要靠 spin 或 done callback 才拿得到。 - 不要在回呼裡用
client.call:執行器等回呼,回呼等回應,兩邊都不會結束。 - 參數先宣告才能用,可以在啟動時給值,也可以在執行中修改;型別固定。
- 影響安全的參數要在
add_on_set_parameters_callback裡檢查範圍,並且不能是唯一的防線。
下一課處理要跑很久、中途要回報進度、還可能被取消的工作:action。