Skip to content

Commit 8cc8959

Browse files
committed
Port draft action implementation from ros2-rust#410
Signed-off-by: Michael X. Grey <[email protected]>
1 parent 14f20b5 commit 8cc8959

File tree

11 files changed

+1503
-0
lines changed

11 files changed

+1503
-0
lines changed

rclrs/src/action.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
pub(crate) mod client;
2+
pub(crate) mod server;
3+
mod server_goal_handle;
4+
5+
use crate::rcl_bindings::RCL_ACTION_UUID_SIZE;
6+
use std::fmt;
7+
8+
pub use client::{ActionClient, ActionClientBase, ActionClientOptions, ActionClientState};
9+
pub use server::{ActionServer, ActionServerBase, ActionServerOptions, ActionServerState};
10+
pub use server_goal_handle::ServerGoalHandle;
11+
12+
/// A unique identifier for a goal request.
13+
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
14+
pub struct GoalUuid(pub [u8; RCL_ACTION_UUID_SIZE]);
15+
16+
impl fmt::Display for GoalUuid {
17+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
18+
write!(f, "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
19+
self.0[0],
20+
self.0[1],
21+
self.0[2],
22+
self.0[3],
23+
self.0[4],
24+
self.0[5],
25+
self.0[6],
26+
self.0[7],
27+
self.0[8],
28+
self.0[9],
29+
self.0[10],
30+
self.0[11],
31+
self.0[12],
32+
self.0[13],
33+
self.0[14],
34+
self.0[15],
35+
)
36+
}
37+
}
38+
39+
/// The response returned by an [`ActionServer`]'s goal callback when a goal request is received.
40+
#[derive(PartialEq, Eq)]
41+
pub enum GoalResponse {
42+
/// The goal is rejected and will not be executed.
43+
Reject = 1,
44+
/// The server accepts the goal and will begin executing it immediately.
45+
AcceptAndExecute = 2,
46+
/// The server accepts the goal and will begin executing it later.
47+
AcceptAndDefer = 3,
48+
}
49+
50+
/// The response returned by an [`ActionServer`]'s cancel callback when a goal is requested to be cancelled.
51+
#[derive(PartialEq, Eq)]
52+
pub enum CancelResponse {
53+
/// The server will not try to cancel the goal.
54+
Reject = 1,
55+
/// The server will try to cancel the goal.
56+
Accept = 2,
57+
}

rclrs/src/action/client.rs

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
use crate::{
2+
error::ToResult, rcl_bindings::*, wait::WaitableNumEntities, Node, NodeHandle, QoSProfile,
3+
RclrsError, ENTITY_LIFECYCLE_MUTEX,
4+
};
5+
use std::{
6+
borrow::Borrow,
7+
ffi::CString,
8+
marker::PhantomData,
9+
sync::{atomic::AtomicBool, Arc, Mutex, MutexGuard},
10+
};
11+
12+
// SAFETY: The functions accessing this type, including drop(), shouldn't care about the thread
13+
// they are running in. Therefore, this type can be safely sent to another thread.
14+
unsafe impl Send for rcl_action_client_t {}
15+
16+
/// Manage the lifecycle of an `rcl_action_client_t`, including managing its dependencies
17+
/// on `rcl_node_t` and `rcl_context_t` by ensuring that these dependencies are
18+
/// [dropped after][1] the `rcl_action_client_t`.
19+
///
20+
/// [1]: <https://doc.rust-lang.org/reference/destructors.html>
21+
pub struct ActionClientHandle {
22+
rcl_action_client: Mutex<rcl_action_client_t>,
23+
node_handle: Arc<NodeHandle>,
24+
pub(crate) in_use_by_wait_set: Arc<AtomicBool>,
25+
}
26+
27+
impl ActionClientHandle {
28+
pub(crate) fn lock(&self) -> MutexGuard<rcl_action_client_t> {
29+
self.rcl_action_client.lock().unwrap()
30+
}
31+
}
32+
33+
impl Drop for ActionClientHandle {
34+
fn drop(&mut self) {
35+
let rcl_action_client = self.rcl_action_client.get_mut().unwrap();
36+
let mut rcl_node = self.node_handle.rcl_node.lock().unwrap();
37+
let _lifecycle_lock = ENTITY_LIFECYCLE_MUTEX.lock().unwrap();
38+
// SAFETY: The entity lifecycle mutex is locked to protect against the risk of
39+
// global variables in the rmw implementation being unsafely modified during cleanup.
40+
unsafe {
41+
rcl_action_client_fini(rcl_action_client, &mut *rcl_node);
42+
}
43+
}
44+
}
45+
46+
/// Trait to be implemented by concrete ActionClient structs.
47+
///
48+
/// See [`ActionClient<T>`] for an example
49+
pub trait ActionClientBase: Send + Sync {
50+
/// Internal function to get a reference to the `rcl` handle.
51+
fn handle(&self) -> &ActionClientHandle;
52+
/// Returns the number of underlying entities for the action client.
53+
fn num_entities(&self) -> &WaitableNumEntities;
54+
/// Tries to run the callback for the given readiness mode.
55+
fn execute(&self, mode: ReadyMode) -> Result<(), RclrsError>;
56+
}
57+
58+
pub(crate) enum ReadyMode {
59+
Feedback,
60+
Status,
61+
GoalResponse,
62+
CancelResponse,
63+
ResultResponse,
64+
}
65+
66+
///
67+
/// Main class responsible for sending goals to a ROS action server.
68+
///
69+
/// Create a client using [`Node::create_action_client`][1].
70+
///
71+
/// Receiving feedback and results requires the node's executor to [spin][2].
72+
///
73+
/// [1]: crate::NodeState::create_action_client
74+
/// [2]: crate::spin
75+
pub type ActionClient<ActionT> = Arc<ActionClientState<ActionT>>;
76+
77+
/// The inner state of an [`ActionClient`].
78+
///
79+
/// This is public so that you can choose to create a [`Weak`][1] reference to it
80+
/// if you want to be able to refer to an [`ActionClient`] in a non-owning way. It is
81+
/// generally recommended to manage the `ActionClientState` inside of an [`Arc`],
82+
/// and [`ActionClient`] is provided as a convenience alias for that.
83+
///
84+
/// The public API of the [`ActionClient`] type is implemented via `ActionClientState`.
85+
///
86+
/// [1]: std::sync::Weak
87+
pub struct ActionClientState<ActionT>
88+
where
89+
ActionT: rosidl_runtime_rs::Action,
90+
{
91+
_marker: PhantomData<fn() -> ActionT>,
92+
pub(crate) handle: Arc<ActionClientHandle>,
93+
num_entities: WaitableNumEntities,
94+
/// Ensure the parent node remains alive as long as the subscription is held.
95+
/// This implementation will change in the future.
96+
#[allow(unused)]
97+
node: Node,
98+
}
99+
100+
impl<T> ActionClientState<T>
101+
where
102+
T: rosidl_runtime_rs::Action,
103+
{
104+
/// Creates a new action client.
105+
pub(crate) fn new<'a>(
106+
node: &Node,
107+
options: impl Into<ActionClientOptions<'a>>,
108+
) -> Result<Self, RclrsError>
109+
where
110+
T: rosidl_runtime_rs::Action,
111+
{
112+
let options = options.into();
113+
// SAFETY: Getting a zero-initialized value is always safe.
114+
let mut rcl_action_client = unsafe { rcl_action_get_zero_initialized_client() };
115+
let type_support = T::get_type_support() as *const rosidl_action_type_support_t;
116+
let action_name_c_string =
117+
CString::new(options.action_name).map_err(|err| RclrsError::StringContainsNul {
118+
err,
119+
s: options.action_name.into(),
120+
})?;
121+
122+
// SAFETY: No preconditions for this function.
123+
let action_client_options = unsafe { rcl_action_client_get_default_options() };
124+
125+
{
126+
let mut rcl_node = node.handle.rcl_node.lock().unwrap();
127+
let _lifecycle_lock = ENTITY_LIFECYCLE_MUTEX.lock().unwrap();
128+
129+
// SAFETY:
130+
// * The rcl_action_client was zero-initialized as expected by this function.
131+
// * The rcl_node is kept alive by the NodeHandle because it is a dependency of the action client.
132+
// * The action name and the options are copied by this function, so they can be dropped
133+
// afterwards.
134+
// * The entity lifecycle mutex is locked to protect against the risk of global
135+
// variables in the rmw implementation being unsafely modified during initialization.
136+
unsafe {
137+
rcl_action_client_init(
138+
&mut rcl_action_client,
139+
&mut *rcl_node,
140+
type_support,
141+
action_name_c_string.as_ptr(),
142+
&action_client_options,
143+
)
144+
.ok()?;
145+
}
146+
}
147+
148+
let handle = Arc::new(ActionClientHandle {
149+
rcl_action_client: Mutex::new(rcl_action_client),
150+
node_handle: Arc::clone(&node.handle),
151+
in_use_by_wait_set: Arc::new(AtomicBool::new(false)),
152+
});
153+
154+
let mut num_entities = WaitableNumEntities::default();
155+
unsafe {
156+
rcl_action_client_wait_set_get_num_entities(
157+
&*handle.lock(),
158+
&mut num_entities.num_subscriptions,
159+
&mut num_entities.num_guard_conditions,
160+
&mut num_entities.num_timers,
161+
&mut num_entities.num_clients,
162+
&mut num_entities.num_services,
163+
)
164+
.ok()?;
165+
}
166+
167+
Ok(Self {
168+
_marker: Default::default(),
169+
handle,
170+
num_entities,
171+
node: Arc::clone(node),
172+
})
173+
}
174+
175+
fn execute_feedback(&self) -> Result<(), RclrsError> {
176+
todo!()
177+
}
178+
179+
fn execute_status(&self) -> Result<(), RclrsError> {
180+
todo!()
181+
}
182+
183+
fn execute_goal_response(&self) -> Result<(), RclrsError> {
184+
todo!()
185+
}
186+
187+
fn execute_cancel_response(&self) -> Result<(), RclrsError> {
188+
todo!()
189+
}
190+
191+
fn execute_result_response(&self) -> Result<(), RclrsError> {
192+
todo!()
193+
}
194+
}
195+
196+
impl<T> ActionClientBase for ActionClientState<T>
197+
where
198+
T: rosidl_runtime_rs::Action,
199+
{
200+
fn handle(&self) -> &ActionClientHandle {
201+
&self.handle
202+
}
203+
204+
fn num_entities(&self) -> &WaitableNumEntities {
205+
&self.num_entities
206+
}
207+
208+
fn execute(&self, mode: ReadyMode) -> Result<(), RclrsError> {
209+
match mode {
210+
ReadyMode::Feedback => self.execute_feedback(),
211+
ReadyMode::Status => self.execute_status(),
212+
ReadyMode::GoalResponse => self.execute_goal_response(),
213+
ReadyMode::CancelResponse => self.execute_cancel_response(),
214+
ReadyMode::ResultResponse => self.execute_result_response(),
215+
}
216+
}
217+
}
218+
219+
/// `ActionClientOptions` are used by [`Node::create_action_client`][1] to initialize an
220+
/// [`ActionClient`].
221+
///
222+
/// [1]: crate::Node::create_action_client
223+
#[derive(Debug, Clone)]
224+
#[non_exhaustive]
225+
pub struct ActionClientOptions<'a> {
226+
/// The name of the action that this client will send requests to
227+
pub action_name: &'a str,
228+
/// The quality of service profile for the goal service
229+
pub goal_service_qos: QoSProfile,
230+
/// The quality of service profile for the result service
231+
pub result_service_qos: QoSProfile,
232+
/// The quality of service profile for the cancel service
233+
pub cancel_service_qos: QoSProfile,
234+
/// The quality of service profile for the feedback topic
235+
pub feedback_topic_qos: QoSProfile,
236+
/// The quality of service profile for the status topic
237+
pub status_topic_qos: QoSProfile,
238+
}
239+
240+
impl<'a> ActionClientOptions<'a> {
241+
/// Initialize a new [`ActionClientOptions`] with default settings.
242+
pub fn new(action_name: &'a str) -> Self {
243+
Self {
244+
action_name,
245+
goal_service_qos: QoSProfile::services_default(),
246+
result_service_qos: QoSProfile::services_default(),
247+
cancel_service_qos: QoSProfile::services_default(),
248+
feedback_topic_qos: QoSProfile::topics_default(),
249+
status_topic_qos: QoSProfile::action_status_default(),
250+
}
251+
}
252+
}
253+
254+
impl<'a, T: Borrow<str> + ?Sized + 'a> From<&'a T> for ActionClientOptions<'a> {
255+
fn from(value: &'a T) -> Self {
256+
Self::new(value.borrow())
257+
}
258+
}

0 commit comments

Comments
 (0)