Skip to main content

ros2_client/action/
client.rs

1#[allow(unused_imports)]
2use log::{debug, error, info, warn};
3use rustdds::dds::{ReadResult, WriteResult};
4pub use action_msgs::{CancelGoalRequest, CancelGoalResponse, GoalId, GoalInfo, GoalStatusEnum};
5use builtin_interfaces::Time;
6use futures::{
7  //pin_mut,
8  stream::{FusedStream, StreamExt},
9  Future,
10};
11
12use crate::{
13  action_msgs, builtin_interfaces,
14  message::Message,
15  names::Name,
16  service::{request_id::RmwRequestId, AService, CallServiceError, Client},
17  unique_identifier_msgs, Subscription,
18};
19use super::{
20  ActionTypes, FeedbackMessage, GetResultRequest, GetResultResponse, SendGoalRequest,
21  SendGoalResponse,
22};
23
24/// A client for ROS 2 Actions. Supports both sync and async operation.
25pub struct ActionClient<A>
26where
27  A: ActionTypes,
28  A::GoalType: Message + Clone,
29  A::ResultType: Message + Clone,
30  A::FeedbackType: Message,
31{
32  pub(crate) my_goal_client: Client<AService<SendGoalRequest<A::GoalType>, SendGoalResponse>>,
33
34  pub(crate) my_cancel_client:
35    Client<AService<action_msgs::CancelGoalRequest, action_msgs::CancelGoalResponse>>,
36
37  pub(crate) my_result_client: Client<AService<GetResultRequest, GetResultResponse<A::ResultType>>>,
38
39  pub(crate) my_feedback_subscription: Subscription<FeedbackMessage<A::FeedbackType>>,
40
41  pub(crate) my_status_subscription: Subscription<action_msgs::GoalStatusArray>,
42
43  pub(crate) my_action_name: Name,
44}
45
46impl<A> ActionClient<A>
47where
48  A: ActionTypes,
49  A::GoalType: Message + Clone,
50  A::ResultType: Message + Clone,
51  A::FeedbackType: Message,
52{
53  pub fn name(&self) -> &Name {
54    &self.my_action_name
55  }
56
57  pub fn goal_client(&self) -> &Client<AService<SendGoalRequest<A::GoalType>, SendGoalResponse>> {
58    &self.my_goal_client
59  }
60  pub fn cancel_client(
61    &self,
62  ) -> &Client<AService<action_msgs::CancelGoalRequest, action_msgs::CancelGoalResponse>> {
63    &self.my_cancel_client
64  }
65  pub fn result_client(
66    &self,
67  ) -> &Client<AService<GetResultRequest, GetResultResponse<A::ResultType>>> {
68    &self.my_result_client
69  }
70  pub fn feedback_subscription(&self) -> &Subscription<FeedbackMessage<A::FeedbackType>> {
71    &self.my_feedback_subscription
72  }
73  pub fn status_subscription(&self) -> &Subscription<action_msgs::GoalStatusArray> {
74    &self.my_status_subscription
75  }
76
77  /// Returns and id of the Request and id for the Goal.
78  /// Request id can be used to recognize correct response from Action Server.
79  /// Goal id is later used to communicate Goal status and result.
80  pub fn send_goal(&self, goal: A::GoalType) -> WriteResult<(RmwRequestId, GoalId), ()>
81  where
82    <A as ActionTypes>::GoalType: 'static,
83  {
84    let goal_id = unique_identifier_msgs::UUID::new_random();
85    self
86      .my_goal_client
87      .send_request(SendGoalRequest { goal_id, goal })
88      .map(|req_id| (req_id, goal_id))
89  }
90
91  /// Receive a response for the specified goal request, or None if response is
92  /// not yet available
93  pub fn receive_goal_response(&self, req_id: RmwRequestId) -> ReadResult<Option<SendGoalResponse>>
94  where
95    <A as ActionTypes>::GoalType: 'static,
96  {
97    loop {
98      match self.my_goal_client.receive_response() {
99        Err(e) => break Err(e),
100        Ok(None) => break Ok(None), // not yet
101        Ok(Some((incoming_req_id, resp))) if incoming_req_id == req_id =>
102        // received the expected answer
103        {
104          break Ok(Some(resp))
105        }
106        Ok(Some((incoming_req_id, _resp))) => {
107          // got someone else's answer. Try again.
108          info!("Goal Response not for us: {incoming_req_id:?} != {req_id:?}");
109          continue;
110        }
111      }
112    }
113    // We loop here to drain all the answers received so far.
114    // The mio .poll() only does not trigger again for the next item, if it has
115    // been received already.
116  }
117
118  pub async fn async_send_goal(
119    &self,
120    goal: A::GoalType,
121  ) -> Result<(GoalId, SendGoalResponse), CallServiceError<()>>
122  where
123    <A as ActionTypes>::GoalType: 'static,
124  {
125    let goal_id = unique_identifier_msgs::UUID::new_random();
126    let send_goal_response = self
127      .my_goal_client
128      .async_call_service(SendGoalRequest { goal_id, goal })
129      .await?;
130    Ok((goal_id, send_goal_response))
131  }
132
133  // From ROS2 docs:
134  // https://docs.ros2.org/foxy/api/action_msgs/srv/CancelGoal.html
135  //
136  // Cancel one or more goals with the following policy:
137  // - If the goal ID is zero and timestamp is zero, cancel all goals.
138  // - If the goal ID is zero and timestamp is not zero, cancel all goals accepted
139  //   at or before the timestamp.
140  // - If the goal ID is not zero and timestamp is zero, cancel the goal with the
141  //   given ID regardless of the time it was accepted.
142  // - If the goal ID is not zero and timestamp is not zero, cancel the goal with
143  //   the given ID and all goals accepted at or before the timestamp.
144
145  fn cancel_goal_raw(&self, goal_id: GoalId, timestamp: Time) -> WriteResult<RmwRequestId, ()> {
146    let goal_info = GoalInfo {
147      goal_id,
148      stamp: timestamp,
149    };
150    self
151      .my_cancel_client
152      .send_request(CancelGoalRequest { goal_info })
153  }
154
155  pub fn cancel_goal(&self, goal_id: GoalId) -> WriteResult<RmwRequestId, ()> {
156    self.cancel_goal_raw(goal_id, Time::ZERO)
157  }
158
159  pub fn cancel_all_goals_before(&self, timestamp: Time) -> WriteResult<RmwRequestId, ()> {
160    self.cancel_goal_raw(GoalId::ZERO, timestamp)
161  }
162
163  pub fn cancel_all_goals(&self) -> WriteResult<RmwRequestId, ()> {
164    self.cancel_goal_raw(GoalId::ZERO, Time::ZERO)
165  }
166
167  pub fn receive_cancel_response(
168    &self,
169    cancel_request_id: RmwRequestId,
170  ) -> ReadResult<Option<CancelGoalResponse>> {
171    loop {
172      match self.my_cancel_client.receive_response() {
173        Err(e) => break Err(e),
174        Ok(None) => break Ok(None), // not yet
175        Ok(Some((incoming_req_id, resp))) if incoming_req_id == cancel_request_id => {
176          break Ok(Some(resp))
177        } // received expected answer
178        Ok(Some(_)) => continue,    // got someone else's answer. Try again.
179      }
180    }
181  }
182
183  pub fn async_cancel_goal(
184    &self,
185    goal_id: GoalId,
186    timestamp: Time,
187  ) -> impl Future<Output = Result<CancelGoalResponse, CallServiceError<()>>> + '_ {
188    let goal_info = GoalInfo {
189      goal_id,
190      stamp: timestamp,
191    };
192    self
193      .my_cancel_client
194      .async_call_service(CancelGoalRequest { goal_info })
195  }
196
197  pub fn request_result(&self, goal_id: GoalId) -> WriteResult<RmwRequestId, ()>
198  where
199    <A as ActionTypes>::ResultType: 'static,
200  {
201    self
202      .my_result_client
203      .send_request(GetResultRequest { goal_id })
204  }
205
206  pub fn receive_result(
207    &self,
208    result_request_id: RmwRequestId,
209  ) -> ReadResult<Option<(GoalStatusEnum, A::ResultType)>>
210  where
211    <A as ActionTypes>::ResultType: 'static,
212  {
213    loop {
214      match self.my_result_client.receive_response() {
215        Err(e) => break Err(e),
216        Ok(None) => break Ok(None), // not yet
217        Ok(Some((incoming_req_id, GetResultResponse { status, result })))
218          if incoming_req_id == result_request_id =>
219        {
220          break Ok(Some((status, result)))
221        } // received expected answer
222        Ok(Some(_)) => continue,    // got someone else's answer. Try again.
223      }
224    }
225  }
226
227  /// Asynchronously request goal result.
228  /// Result should be requested as soon as a goal is accepted.
229  /// Result ia actually received only when Server informs that the goal has
230  /// either Succeeded, or has been Canceled or Aborted.
231  pub async fn async_request_result(
232    &self,
233    goal_id: GoalId,
234  ) -> Result<(GoalStatusEnum, A::ResultType), CallServiceError<()>>
235  where
236    <A as ActionTypes>::ResultType: 'static,
237  {
238    let GetResultResponse { status, result } = self
239      .my_result_client
240      .async_call_service(GetResultRequest { goal_id })
241      .await?;
242    Ok((status, result))
243  }
244
245  pub fn receive_feedback(&self, goal_id: GoalId) -> ReadResult<Option<A::FeedbackType>>
246  where
247    <A as ActionTypes>::FeedbackType: 'static,
248  {
249    loop {
250      match self.my_feedback_subscription.take() {
251        Err(e) => break Err(e),
252        Ok(None) => break Ok(None),
253        Ok(Some((fb_msg, _msg_info))) if fb_msg.goal_id == goal_id => {
254          break Ok(Some(fb_msg.feedback))
255        }
256        Ok(Some((fb_msg, _msg_info))) => {
257          // feedback on some other goal
258          debug!(
259            "Feedback on another goal {:?} != {:?}",
260            fb_msg.goal_id, goal_id
261          )
262        }
263      }
264    }
265  }
266
267  /// Receive asynchronous feedback stream of goal progress.
268  pub fn feedback_stream(
269    &self,
270    goal_id: GoalId,
271  ) -> impl FusedStream<Item = ReadResult<A::FeedbackType>> + '_
272  where
273    <A as ActionTypes>::FeedbackType: 'static,
274  {
275    let expected_goal_id = goal_id; // rename
276    self
277      .my_feedback_subscription
278      .async_stream()
279      .filter_map(move |result| async move {
280        match result {
281          Err(e) => Some(Err(e)),
282          Ok((FeedbackMessage { goal_id, feedback }, _msg_info)) => {
283            if goal_id == expected_goal_id {
284              Some(Ok(feedback))
285            } else {
286              debug!("Feedback for some other {goal_id:?}.");
287              None
288            }
289          }
290        }
291      })
292  }
293
294  /// Note: This does not take GoalId and will therefore report status of all
295  /// Goals.
296  pub fn receive_status(&self) -> ReadResult<Option<action_msgs::GoalStatusArray>> {
297    self
298      .my_status_subscription
299      .take()
300      .map(|r| r.map(|(gsa, _msg_info)| gsa))
301  }
302
303  pub async fn async_receive_status(&self) -> ReadResult<action_msgs::GoalStatusArray> {
304    let (m, _msg_info) = self.my_status_subscription.async_take().await?;
305    Ok(m)
306  }
307
308  /// Async Stream of status updates
309  /// Action server send updates containing status of all goals, hence an array.
310  pub fn all_statuses_stream(
311    &self,
312  ) -> impl FusedStream<Item = ReadResult<action_msgs::GoalStatusArray>> + '_ {
313    self
314      .my_status_subscription
315      .async_stream()
316      .map(|result| result.map(|(gsa, _mi)| gsa))
317  }
318
319  pub fn status_stream(
320    &self,
321    goal_id: GoalId,
322  ) -> impl FusedStream<Item = ReadResult<action_msgs::GoalStatus>> + '_ {
323    self
324      .all_statuses_stream()
325      .filter_map(move |result| async move {
326        match result {
327          Err(e) => Some(Err(e)),
328          Ok(gsa) => gsa
329            .status_list
330            .into_iter()
331            .find(|gs| gs.goal_info.goal_id == goal_id)
332            .map(Ok),
333        }
334      })
335  }
336} // impl ActionClient