Skip to content

Commit 249d5ec

Browse files
committed
Implement goal cancel requests
1 parent b06c14c commit 249d5ec

File tree

3 files changed

+165
-3
lines changed

3 files changed

+165
-3
lines changed

rclrs/src/action.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub enum GoalResponse {
4848
}
4949

5050
/// The response returned by an [`ActionServer`]'s cancel callback when a goal is requested to be cancelled.
51+
#[derive(PartialEq, Eq)]
5152
pub enum CancelResponse {
5253
/// The server will not try to cancel the goal.
5354
Reject = 1,

rclrs/src/action/server.rs

Lines changed: 155 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,10 @@ where
275275

276276
self.send_goal_response(request_id, true)?;
277277

278-
self.goal_handles.lock().unwrap().insert(uuid, Arc::clone(&goal_handle));
278+
self.goal_handles
279+
.lock()
280+
.unwrap()
281+
.insert(uuid, Arc::clone(&goal_handle));
279282

280283
if response == GoalResponse::AcceptAndExecute {
281284
goal_handle.execute()?;
@@ -289,8 +292,157 @@ where
289292
Ok(())
290293
}
291294

295+
fn take_cancel_request(&self) -> Result<(action_msgs__srv__CancelGoal_Request, rmw_request_id_t), RclrsError> {
296+
let mut request_id = rmw_request_id_t {
297+
writer_guid: [0; 16],
298+
sequence_number: 0,
299+
};
300+
// SAFETY: No preconditions
301+
let mut request_rmw = unsafe { rcl_action_get_zero_initialized_cancel_request() };
302+
let handle = &*self.handle.lock();
303+
unsafe {
304+
// SAFETY: The action server is locked by the handle. The request_id is a
305+
// zero-initialized rmw_request_id_t, and the request_rmw is a zero-initialized
306+
// action_msgs__srv__CancelGoal_Request.
307+
rcl_action_take_cancel_request(
308+
handle,
309+
&mut request_id,
310+
&mut request_rmw as *mut _ as *mut _,
311+
)
312+
}
313+
.ok()?;
314+
315+
Ok((request_rmw, request_id))
316+
}
317+
318+
fn send_cancel_response(
319+
&self,
320+
mut request_id: rmw_request_id_t,
321+
response_rmw: &mut action_msgs__srv__CancelGoal_Response,
322+
) -> Result<(), RclrsError> {
323+
let handle = &*self.handle.lock();
324+
let result = unsafe {
325+
// SAFETY: The action server handle is locked and so synchronized with other functions.
326+
// The request_id and response are both uniquely owned or borrowed, and so neither will
327+
// mutate during this function call.
328+
rcl_action_send_cancel_response(
329+
handle,
330+
&mut request_id,
331+
response_rmw as *mut _ as *mut _,
332+
)
333+
}
334+
.ok();
335+
match result {
336+
Ok(()) => Ok(()),
337+
Err(RclrsError::RclError {
338+
code: RclReturnCode::Timeout,
339+
..
340+
}) => {
341+
// TODO(nwn): Log an error and continue.
342+
// (See https://github.com/ros2/rclcpp/pull/2215 for reasoning.)
343+
Ok(())
344+
}
345+
_ => result,
346+
}
347+
}
348+
292349
fn execute_cancel_request(&self) -> Result<(), RclrsError> {
293-
todo!()
350+
let (request, request_id) = match self.take_cancel_request() {
351+
Ok(res) => res,
352+
Err(RclrsError::RclError {
353+
code: RclReturnCode::ServiceTakeFailed,
354+
..
355+
}) => {
356+
// Spurious wakeup – this may happen even when a waitset indicated that this
357+
// action was ready, so it shouldn't be an error.
358+
return Ok(());
359+
}
360+
Err(err) => return Err(err),
361+
};
362+
363+
let mut response_rmw = {
364+
// SAFETY: No preconditions
365+
let mut response_rmw = unsafe { rcl_action_get_zero_initialized_cancel_response() };
366+
unsafe {
367+
// SAFETY: The action server is locked by the handle. The request was initialized
368+
// by rcl_action, and the response is a zero-initialized
369+
// rcl_action_cancel_response_t.
370+
rcl_action_process_cancel_request(
371+
&*self.handle.lock(),
372+
&request,
373+
&mut response_rmw as *mut _,
374+
)
375+
}
376+
.ok()?;
377+
378+
DropGuard::new(response_rmw, |mut response_rmw| unsafe {
379+
// SAFETY: The response was initialized by rcl_action_process_cancel_request().
380+
// Later modifications only truncate the size of the array and shift elements,
381+
// without modifying the data pointer or capacity.
382+
rcl_action_cancel_response_fini(&mut response_rmw);
383+
})
384+
};
385+
386+
let num_candidates = response_rmw.msg.goals_canceling.size;
387+
let mut num_accepted = 0;
388+
for idx in 0..response_rmw.msg.goals_canceling.size {
389+
let goal_info = unsafe {
390+
// SAFETY: The array pointed to by response_rmw.msg.goals_canceling.data is
391+
// guaranteed to contain at least response_rmw.msg.goals_canceling.size members.
392+
&*response_rmw.msg.goals_canceling.data.add(idx)
393+
};
394+
let goal_uuid = GoalUuid(goal_info.goal_id.uuid);
395+
396+
let response = {
397+
if let Some(goal_handle) = self.goal_handles.lock().unwrap().get(&goal_uuid) {
398+
let response: CancelResponse = todo!("Call self.cancel_callback(goal_handle)");
399+
if response == CancelResponse::Accept {
400+
// Still reject the request if the goal is no longer cancellable.
401+
if goal_handle.cancel().is_ok() {
402+
CancelResponse::Accept
403+
} else {
404+
CancelResponse::Reject
405+
}
406+
} else {
407+
CancelResponse::Reject
408+
}
409+
} else {
410+
CancelResponse::Reject
411+
}
412+
};
413+
414+
if response == CancelResponse::Accept {
415+
// Shift the accepted entry back to the first rejected slot, if necessary.
416+
if num_accepted < idx {
417+
let goal_info_slot = unsafe {
418+
// SAFETY: The array pointed to by response_rmw.msg.goals_canceling.data is
419+
// guaranteed to contain at least response_rmw.msg.goals_canceling.size
420+
// members. Since `num_accepted` is strictly less than `idx`, it is a
421+
// distinct element of the array, so there is no mutable aliasing.
422+
&mut *response_rmw.msg.goals_canceling.data.add(num_accepted)
423+
};
424+
}
425+
num_accepted += 1;
426+
}
427+
}
428+
response_rmw.msg.goals_canceling.size = num_accepted;
429+
430+
// If the user rejects all individual cancel requests, consider the entire request as
431+
// having been rejected.
432+
if num_accepted == 0 && num_candidates > 0 {
433+
// TODO(nwn): Include action_msgs__srv__CancelGoal_Response__ERROR_REJECTED in the rcl
434+
// bindings.
435+
response_rmw.msg.return_code = 1;
436+
}
437+
438+
// If any goal states changed, publish a status update.
439+
if num_accepted > 0 {
440+
self.publish_status()?;
441+
}
442+
443+
self.send_cancel_response(request_id, &mut response_rmw.msg)?;
444+
445+
Ok(())
294446
}
295447

296448
fn execute_result_request(&self) -> Result<(), RclrsError> {
@@ -319,7 +471,7 @@ where
319471
let uuid = GoalUuid(expired_goal.goal_id.uuid);
320472
self.goal_handles.lock().unwrap().remove(&uuid);
321473
} else {
322-
break
474+
break;
323475
}
324476
}
325477

rclrs/src/action/server_goal_handle.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ where
9191
unsafe { rcl_action_update_goal_state(*rcl_handle, event).ok() }
9292
}
9393

94+
/// Indicate that the goal is being cancelled.
95+
///
96+
/// This is called when a cancel request for the goal has been accepted.
97+
///
98+
/// Returns an error if the goal is in any state other than accepted or executing.
99+
pub(crate) fn cancel(&self) -> Result<(), RclrsError> {
100+
self.update_state(rcl_action_goal_event_t::GOAL_EVENT_CANCEL_GOAL)
101+
}
102+
94103
/// Indicate that the goal could not be reached and has been aborted.
95104
///
96105
/// Only call this if the goal is executing but cannot be completed. This is a terminal state,

0 commit comments

Comments
 (0)