National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 80 24°C PM2.5 21
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 06

action

要跑很久的工作:進度回報、取消與結果
Interaction 30 minute(s)

Learning Objectives

  • 說明要跑很久的工作為什麼用 action,不用 service
  • 說出 goal、feedback、result、cancel 的角色
  • 用 ActionServer、ActionClient 寫伺服器與用戶端
  • 說明 execute 回呼阻塞時,同一個節點的訂閱為什麼收不到,以及怎麼解決
  • 正確處理取消:先讓機器人停下來,再回報已取消

「走到教室門口」不能用 service

叫機器人走到教室門口要花 30 秒。如果用 service:

  • 用戶端這 30 秒什麼都不知道:走到哪了、卡住了嗎、還是機器人根本沒收到?
  • 走到一半有人擋路,想喊停也沒辦法,因為只有「問」和「答」兩個動作。

action 就是為了這種工作設計的。官方的說法是「長時間的遠端呼叫,有回饋,也可以取消」。它分成四個部分:

部分 方向 例子
goal(目標) 用戶端 → 伺服器 走到 (3, 1)
feedback(回饋) 伺服器 → 用戶端,可以很多次 還剩 2.4 公尺
result(結果) 伺服器 → 用戶端,一次 到了/失敗
cancel(取消) 用戶端 → 伺服器 不要走了

定義檔長這樣,用兩條 --- 分成三段(goal、result、feedback):

int32 order
---
int32[] sequence
---
int32[] sequence

官方的例子是算 Fibonacci 數列,這裡先用它看流程。伺服器故意每一步停一下,模擬要跑很久的工作。

互動練習:官方的 Fibonacci action

看「輸出」裡三種訊息的先後:Goal accepted、好幾次 Feedback、最後一個 Result。這是模擬環境的輸出,時間是虛擬的。

用戶端的程式全是回呼:send_goal_async 不會等,接受了才呼叫 goal_response_callback,結果出來才呼叫 get_result_callback。

一步一步看 action 的順序

這次用同一個節點同時當伺服器與用戶端,也不用 sleep,專心看順序:先拿到「接受了」,再用它要結果。注意最後的輸出:回饋和結果走不同的通道,不保證誰先到。這個範例伺服器一下子就做完,回饋反而比結果晚被處理;上面的練習伺服器每一步都停一下,所以回饋都先到。

自己寫一個「往前走 N 公尺」

現在做一個真的會動的 action:收到距離,往前走,回報剩餘距離,走到就停。

先借用 nav2_msgs/action/NavigateToPose 這個訊息型別:goal 裡的 pose 有 x,可以放距離;feedback 有 distance_remaining。這樣就不用先建自訂的 action 型別(真實專案通常會在自己的 package 裡寫一個 .action 檔)。真正的導航堆疊要到第 19 課才用。

看起來最直接的寫法是:訂閱 /odom 更新 self.x,在 execute 裡一邊發速度指令、一邊 time.sleep(0.1)、一邊檢查 self.x。這樣寫有問題,先執行看看。

互動練習:為什麼機器人一直走

用戶端要求走 2 公尺,所以機器人應該從 x=1 走到 x=3(畫面上的綠色圓圈)就停。

修好它要改 drive_server.py 三個地方(import 已經寫好了):

group = ReentrantCallbackGroup()
self.create_subscription(Odometry, '/odom', self.on_odom, 10, callback_group=group)
ActionServer(self, NavigateToPose, 'drive_forward', self.execute, callback_group=group)
rclpy.spin(DriveServer(), executor=MultiThreadedExecutor())

這是官方範例 minimal_action_server 的寫法。

多執行緒與回呼群組

剛才照官方範例改了兩件事:

  • MultiThreadedExecutor:用多條執行緒跑回呼,同時可以有好幾個回呼在進行。預設的單執行緒執行器一次只跑一個。
  • 回呼群組(callback group):決定哪些回呼可以同時跑。每個節點預設只有一個互斥群組,同一群組裡的回呼不會同時跑。ReentrantCallbackGroup 表示這個群組裡的回呼可以同時跑。

多執行緒也有代價:多個回呼可能同時碰到同一個變數。這裡 on_odom 寫、execute 讀一個浮點數問題不大,如果是好幾個欄位要一起更新,就要加鎖,否則可能讀到改到一半的資料。

另一個選擇:把工作拆開。 第 3 課的做法在這裡也成立,規則是能短就短;真的要等很久的回呼才需要這一節的工具。rclpy 也支援 async def 的回呼,用 await 把執行權讓出來,官方範例的 execute_callback 就是這樣宣告的。

(真實環境和模擬環境都依這套規則運作,但模擬環境裡的回呼其實是輪流跑的:只有在 time.sleep 或 await 的時候才會換人,所以不會真的碰到同時改資料的問題。真實環境會。)

取消:要對方同意,也要真的停下來

用戶端呼叫 goal_handle.cancel_goal_async() 只是請求取消,伺服器要做三件事才算處理好:

  1. 同意:ActionServer(..., cancel_callback=...) 註冊的函數回傳 CancelResponse.ACCEPT。沒有註冊的話,rclpy 的預設行為是一律拒絕。
  2. 注意到:execute 的迴圈要自己檢查 goal_handle.is_cancel_requested,系統不會把你的迴圈中斷掉。
  3. 收尾:讓機器人停下來,呼叫 goal_handle.canceled(),回傳一個結果。

第 3 步的順序不是形式。cmd_vel 指令發出去之後,機器人會照最後一筆指令繼續走(模擬世界就是這樣;很多真實的底盤驅動會在一段時間沒收到指令後自己停,但不能假設)。所以取消的時候要主動送一筆速度為 0 的指令。有人站在機器人前面按取消,他要的是機器人停下來,不是系統狀態變成「已取消」。

同樣的邏輯也適用於拒絕 goal:goal_callback 可以在開始之前就拒絕不合理的目標(例如距離是負的、超出地圖),回傳 GoalResponse.REJECT。

互動練習:走到一半喊停

用戶端要求走 3 公尺,2 秒之後送出取消。伺服器已經用多執行緒執行器與可重入群組了。

機器人大約走 0.6 公尺之後應該就停下來,也就是停在畫面上的綠色圓圈裡。

確認一下

1.

機器人手臂要花 20 秒把杯子拿過來,過程中要顯示進度,也要能中止。應該用什麼?

2.

ActionServer 沒有傳 cancel_callback。用戶端呼叫 cancel_goal_async() 會怎樣?

3.

execute 裡有 while 迴圈配 time.sleep,並讀取訂閱回呼更新的 self.x。節點用預設的 rclpy.spin。會發生什麼?

4.

下面哪一個是處理取消的正確順序?

5.

為什麼用戶端的程式全部寫成回呼(send_goal_async 接 add_done_callback),不要寫成一行一行等?

第 1 次作答

重點整理

  • action 給要跑很久的工作用:goal、多次 feedback、一個 result,還能 cancel。
  • 伺服器回傳之前要宣告 succeed、abort 或 canceled。
  • execute 阻塞時,單執行緒執行器輪不到同一個節點的其他回呼。需要在 execute 中途讀感測器時,用 MultiThreadedExecutor 加 ReentrantCallbackGroup。
  • 預設會拒絕取消;要接受得註冊 cancel_callback,並在 execute 裡檢查 is_cancel_requested。
  • 取消或失敗時,先讓機器人停下來,再回報狀態。

到這裡 ROS 2 的三種溝通方式都看過了。下一課處理一個實際問題:節點多了怎麼一次啟動,以及怎麼用 TF 管理一堆座標系。

(Fibonacci 範例改寫自 ROS 2 官方文件 Writing an action server and client(Python),CC BY 4.0,有修改。)

這一課完成了!
Next lesson:launch 與 TF
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。