Skip to content

[WIP] Action support #410

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 52 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
05464a8
Added skeleton examples for action client and server
esteve Nov 28, 2022
8137733
Added action template
esteve Nov 28, 2022
fb90bec
Added basic create_action_client function
esteve Nov 28, 2022
b03c7a1
Fix linter
esteve Nov 29, 2022
b1957d4
Split action client and server examples
esteve Jul 17, 2023
28ece7a
checkin
esteve Jul 17, 2023
4d27f28
checkin
esteve Jul 17, 2023
e84ae6a
checkin
esteve Aug 10, 2023
eb4026b
fix visibility
esteve Aug 10, 2023
b308bac
Instantiate a new ActionClient
esteve Nov 8, 2023
e5dd17d
checkin
esteve Jul 17, 2023
6d41636
checkin
esteve Jul 17, 2023
8abbe13
checkin
esteve Nov 17, 2023
5cb9610
checkin
esteve Nov 17, 2023
9d06c46
Fix rclrs to compile after rebase
nwn Jun 6, 2024
d3222f8
Sketch out action server construction and destruction
nwn Jun 6, 2024
e7476f9
Sketch out action client as well
nwn Jun 6, 2024
788dc52
Pass rcl_clock_t from Node to ActionServer
nwn Jun 6, 2024
2e0a1b8
Split action servers and clients into separate modules
nwn Jun 6, 2024
e4f572c
Move ServerGoalHandle to separate module
nwn Jun 6, 2024
d7ece4b
Begin implementing ActionServerGoalHandle functions
nwn Jun 6, 2024
33a90d5
Make GoalUuid into a newtype
nwn Jun 8, 2024
47cda20
Document the ServerGoalHandle struct
nwn Jun 8, 2024
83d2470
Add documentation and clean up
nwn Jun 8, 2024
ae62fb1
Take goal, cancel, and accepted callbacks in ActionServer
nwn Jun 11, 2024
f197f24
Store action clients and servers in the Node
nwn Jul 6, 2024
98eef19
Add action client/server entities to wait set
nwn Jul 7, 2024
f6d11a7
Handle action server/client readiness in WaitSet and executor
nwn Jul 7, 2024
f48a590
Add rcl_action error codes
nwn Jul 13, 2024
fb8e703
[WIP] Start defining server/client execute functions
nwn Jul 13, 2024
3083cc4
Use rcl-allocated goal handle pointer in ServerGoalHandle
nwn Jul 20, 2024
908859f
Split execute_goal_request() out into three functions
nwn Jul 20, 2024
2b5a6b5
Partial implementation of ActionServer::publish_status()
nwn Jul 25, 2024
2c8cd97
Add DropGuard convenience wrapper
nwn Aug 7, 2024
5cce85f
Complete implementation of ActionServer::publish_status()
nwn Aug 7, 2024
555b267
Move goal acceptance logic back into execute_goal_request()
nwn Aug 7, 2024
6da2e69
Integrate RMW message methods into ActionImpl
nwn Aug 7, 2024
31e38ce
Add UUID->GoalHandle hash-map to action server
nwn Aug 7, 2024
8d820f9
Add rosidl_runtime_rs::ActionImpl::create_feedback_message()
nwn Aug 9, 2024
b238d67
Add ActionServer::publish_feedback() method
nwn Aug 9, 2024
de5b094
Implement goal expiration in action server
nwn Aug 9, 2024
2fd278f
Implement goal cancel requests
nwn Aug 10, 2024
cbf4ff6
Implement action result requests in the action server
nwn Aug 16, 2024
24cc611
Fix formatting in example
nwn Aug 23, 2024
04b8c58
Hook up ServerGoalHandle callbacks into the ActionServer
nwn Aug 23, 2024
1d6cc38
Switch to create_result_response() in rclrs
nwn Aug 23, 2024
94b517c
Hook up goal termination methods for goal handles
nwn Aug 23, 2024
53ebbbe
Implement client-side trait methods for action messages
nwn Sep 28, 2024
3e8d325
Fix compilation after rebase
nwn Jun 7, 2025
fb06971
Fix linker error
nwn Jun 7, 2025
dbdebd1
Switch to shared state for action client/server
nwn Jun 7, 2025
8973176
Use option struct for create_action_{client,server} arguments
nwn Jun 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Move goal acceptance logic back into execute_goal_request()
This trims the send_goal_response() function down to only be a wrapper
around the rcl_action equivalent. In addition to improving logical
separation, this also simplifies control flow when handling rejection.
  • Loading branch information
nwn committed Jun 7, 2025
commit 555b267cf15a83ad8d52fe779a7caa81d65c8721
175 changes: 99 additions & 76 deletions rclrs/src/action/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,100 +175,115 @@ where
&mut request_id,
&mut request_rmw as *mut RmwRequest<T> as *mut _,
)
}.ok()?;
}
.ok()?;

Ok((request_rmw, request_id))
}

fn send_goal_response(&self, response: GoalResponse, request: &<<T::SendGoalService as Service>::Request as Message>::RmwMsg, mut request_id: rmw_request_id_t) -> Result<(), RclrsError> {
let accepted = response != GoalResponse::Reject;
fn send_goal_response(
&self,
mut request_id: rmw_request_id_t,
accepted: bool,
) -> Result<(), RclrsError> {
type RmwResponse<T> = <<<T as rosidl_runtime_rs::ActionImpl>::SendGoalService as Service>::Response as Message>::RmwMsg;
let mut response_rmw = RmwResponse::<T>::default();
// TODO(nwn): Set the `accepted` field through a trait, similarly to how we extracted the UUID.
// response_rmw.accepted = accepted;
let handle = &*self.handle.lock();
let result = unsafe {
// SAFETY: The action server handle is locked and so synchronized with other
// functions. The request_id and response message are uniquely owned, and so will
// not mutate during this function call.
// Also, when appropriate, `rcl_action_accept_new_goal()` has been called beforehand,
// as specified in the `rcl_action` docs.
rcl_action_send_goal_response(
handle,
&mut request_id,
&mut response_rmw as *mut RmwResponse<T> as *mut _,
)
}
.ok();
match result {
Ok(()) => Ok(()),
Err(RclrsError::RclError {
code: RclReturnCode::Timeout,
..
}) => {
// TODO(nwn): Log an error and continue.
// (See https://github.com/ros2/rclcpp/pull/2215 for reasoning.)
Ok(())
}
_ => result,
}
}

fn execute_goal_request(&self) -> Result<(), RclrsError> {
let (request, request_id) = match self.take_goal_request() {
Ok(res) => res,
Err(RclrsError::RclError {
code: RclReturnCode::ServiceTakeFailed,
..
}) => {
// Spurious wakeup – this may happen even when a waitset indicated that this
// action was ready, so it shouldn't be an error.
return Ok(());
}
Err(err) => return Err(err),
};

let mut uuid = GoalUuid::default();
rosidl_runtime_rs::ExtractUuid::extract_uuid(&request, &mut uuid.0);

let response: GoalResponse = {
todo!("Optionally convert request to an idiomatic type for the user's callback.");
todo!("Call self.goal_callback(uuid, request)");
};

// Don't continue if the goal was rejected by the user.
if response == GoalResponse::Reject {
return self.send_goal_response(request_id, false);
}

// SAFETY: No preconditions
let mut goal_info = unsafe { rcl_action_get_zero_initialized_goal_info() };
// Populate the goal UUID; the other fields will be populated by rcl_action later on.
// TODO(nwn): Check this claim.
rosidl_runtime_rs::ExtractUuid::extract_uuid(request, &mut goal_info.goal_id.uuid);
goal_info.goal_id.uuid = uuid.0;

let goal_handle = if accepted {
let goal_handle = {
let server_handle = &mut *self.handle.lock();
let goal_handle_ptr = unsafe {
// SAFETY: The action server handle is locked and so synchronized with other
// functions. The request_id and response message are uniquely owned, and so will
// not mutate during this function call. The returned goal handle pointer should be
// valid unless it is null.
rcl_action_accept_new_goal(
server_handle,
&goal_info,
)
rcl_action_accept_new_goal(server_handle, &goal_info)
};
if goal_handle_ptr.is_null() {
// Other than rcl_get_error_string(), there's no indication what happened.
panic!("Failed to accept goal");
} else {
Some(ServerGoalHandle::<T>::new(goal_handle_ptr, todo!(""), GoalUuid(goal_info.goal_id.uuid)))
ServerGoalHandle::<T>::new(
goal_handle_ptr,
todo!(""),
GoalUuid(goal_info.goal_id.uuid),
)
}
} else {
None
};

{
type RmwResponse<T> = <<<T as rosidl_runtime_rs::ActionImpl>::SendGoalService as Service>::Response as Message>::RmwMsg;
let mut response_rmw = RmwResponse::<T>::default();
// TODO(nwn): Set the `accepted` field through a trait, similarly to how we extracted the UUID.
// response_rmw.accepted = accepted;
let handle = &*self.handle.lock();
unsafe {
// SAFETY: The action server handle is locked and so synchronized with other
// functions. The request_id and response message are uniquely owned, and so will
// not mutate during this function call.
// Also, `rcl_action_accept_new_goal()` has been called beforehand, as specified in
// the `rcl_action` docs.
rcl_action_send_goal_response(
handle,
&mut request_id,
&mut response_rmw as *mut RmwResponse<T> as *mut _,
)
}.ok()?; // TODO(nwn): Suppress RclReturnCode::Timeout?
}

if let Some(goal_handle) = goal_handle {
// Goal was accepted

// TODO: Add a UUID->goal_handle entry to a server goal map.
self.send_goal_response(request_id, true)?;

if response == GoalResponse::AcceptAndExecute {
goal_handle.execute()?;
}

// TODO: Call publish_status()
// TODO: Add a UUID->goal_handle entry to a server goal map.

// TODO: Call the goal_accepted callback
if response == GoalResponse::AcceptAndExecute {
goal_handle.execute()?;
}

Ok(())
}
self.publish_status()?;

fn execute_goal_request(&self) -> Result<(), RclrsError> {
let (request, request_id) = match self.take_goal_request() {
Ok(res) => res,
Err(RclrsError::RclError { code: RclReturnCode::ServiceTakeFailed, .. }) => {
// Spurious wakeup – this may happen even when a waitset indicated that this
// action was ready, so it shouldn't be an error.
return Ok(());
},
Err(err) => return Err(err),
};

let response: GoalResponse =
{
let mut uuid = GoalUuid::default();
rosidl_runtime_rs::ExtractUuid::extract_uuid(&request, &mut uuid.0);

todo!("Optionally convert request to an idiomatic type for the user's callback.");
todo!("Call self.goal_callback(uuid, request)");
};

self.send_goal_response(response, &request, request_id)?;
// TODO: Call the user's goal_accepted callback.
todo!("Call self.accepted_callback(goal_handle)");

Ok(())
}
Expand All @@ -286,26 +301,34 @@ where
}

fn publish_status(&self) -> Result<(), RclrsError> {
let mut goal_statuses = DropGuard::new(unsafe {
// SAFETY: No preconditions
rcl_action_get_zero_initialized_goal_status_array()
}, |mut goal_status| unsafe {
// SAFETY: The goal_status array is either zero-initialized and empty or populated by
// `rcl_action_get_goal_status_array`. In either case, it can be safely finalized.
rcl_action_goal_status_array_fini(&mut goal_status);
});
let mut goal_statuses = DropGuard::new(
unsafe {
// SAFETY: No preconditions
rcl_action_get_zero_initialized_goal_status_array()
},
|mut goal_statuses| unsafe {
// SAFETY: The goal_status array is either zero-initialized and empty or populated by
// `rcl_action_get_goal_status_array`. In either case, it can be safely finalized.
rcl_action_goal_status_array_fini(&mut goal_statuses);
},
);

unsafe {
// SAFETY: The action server is locked through the handle and goal_statuses is
// zero-initialized.
rcl_action_get_goal_status_array(&*self.handle.lock(), &mut *goal_statuses)
}.ok()?;
}
.ok()?;

unsafe {
// SAFETY: The action server is locked through the handle and goal_statuses.msg is a
// valid `action_msgs__msg__GoalStatusArray` by construction.
rcl_action_publish_status(&*self.handle.lock(), &goal_statuses.msg as *const _ as *const std::ffi::c_void)
}.ok()
rcl_action_publish_status(
&*self.handle.lock(),
&goal_statuses.msg as *const _ as *const std::ffi::c_void,
)
}
.ok()
}
}

Expand Down