diff --git a/examples/message_demo/src/message_demo.rs b/examples/message_demo/src/message_demo.rs index d1fcea1d3..31b791aeb 100644 --- a/examples/message_demo/src/message_demo.rs +++ b/examples/message_demo/src/message_demo.rs @@ -1,8 +1,10 @@ -use std::{convert::TryInto, env, sync::Arc}; +use std::convert::TryInto; use anyhow::{Error, Result}; use rosidl_runtime_rs::{seq, BoundedSequence, Message, Sequence}; +use rclrs::*; + fn check_default_values() { let msg = rclrs_example_msgs::msg::rmw::VariousTypes::default(); assert!(msg.bool_member); @@ -138,38 +140,36 @@ fn demonstrate_sequences() { fn demonstrate_pubsub() -> Result<(), Error> { println!("================== Interoperability demo =================="); // Demonstrate interoperability between idiomatic and RMW-native message types - let context = rclrs::Context::new(env::args())?; - let node = rclrs::create_node(&context, "message_demo")?; + let mut executor = Context::default_from_env()?.create_basic_executor(); + let node = executor.create_node("message_demo")?; - let idiomatic_publisher = node.create_publisher::( - "topic", - rclrs::QOS_PROFILE_DEFAULT, - )?; + let idiomatic_publisher = node + .create_publisher::("topic", QOS_PROFILE_DEFAULT)?; let direct_publisher = node.create_publisher::( "topic", - rclrs::QOS_PROFILE_DEFAULT, + QOS_PROFILE_DEFAULT, )?; let _idiomatic_subscription = node .create_subscription::( "topic", - rclrs::QOS_PROFILE_DEFAULT, + QOS_PROFILE_DEFAULT, move |_msg: rclrs_example_msgs::msg::VariousTypes| println!("Got idiomatic message!"), )?; let _direct_subscription = node .create_subscription::( "topic", - rclrs::QOS_PROFILE_DEFAULT, + QOS_PROFILE_DEFAULT, move |_msg: rclrs_example_msgs::msg::rmw::VariousTypes| { println!("Got RMW-native message!") }, )?; println!("Sending idiomatic message."); idiomatic_publisher.publish(rclrs_example_msgs::msg::VariousTypes::default())?; - rclrs::spin_once(Arc::clone(&node), None)?; + executor.spin(SpinOptions::spin_once()).first_error()?; println!("Sending RMW-native message."); direct_publisher.publish(rclrs_example_msgs::msg::rmw::VariousTypes::default())?; - rclrs::spin_once(Arc::clone(&node), None)?; + executor.spin(SpinOptions::spin_once()).first_error()?; Ok(()) } diff --git a/examples/minimal_client_service/src/minimal_client.rs b/examples/minimal_client_service/src/minimal_client.rs index 915541d54..a8651b4a5 100644 --- a/examples/minimal_client_service/src/minimal_client.rs +++ b/examples/minimal_client_service/src/minimal_client.rs @@ -1,11 +1,10 @@ -use std::env; - use anyhow::{Error, Result}; +use rclrs::*; fn main() -> Result<(), Error> { - let context = rclrs::Context::new(env::args())?; + let mut executor = Context::default_from_env()?.create_basic_executor(); - let node = rclrs::create_node(&context, "minimal_client")?; + let node = executor.create_node("minimal_client")?; let client = node.create_client::("add_two_ints")?; @@ -30,5 +29,8 @@ fn main() -> Result<(), Error> { std::thread::sleep(std::time::Duration::from_millis(500)); println!("Waiting for response"); - rclrs::spin(node).map_err(|err| err.into()) + executor + .spin(SpinOptions::default()) + .first_error() + .map_err(|err| err.into()) } diff --git a/examples/minimal_client_service/src/minimal_client_async.rs b/examples/minimal_client_service/src/minimal_client_async.rs index 0eeb87f4d..c31f2e26e 100644 --- a/examples/minimal_client_service/src/minimal_client_async.rs +++ b/examples/minimal_client_service/src/minimal_client_async.rs @@ -1,12 +1,11 @@ -use std::env; - use anyhow::{Error, Result}; +use rclrs::*; #[tokio::main] async fn main() -> Result<(), Error> { - let context = rclrs::Context::new(env::args())?; + let mut executor = Context::default_from_env()?.create_basic_executor(); - let node = rclrs::create_node(&context, "minimal_client")?; + let node = executor.create_node("minimal_client")?; let client = node.create_client::("add_two_ints")?; @@ -22,7 +21,7 @@ async fn main() -> Result<(), Error> { println!("Waiting for response"); - let rclrs_spin = tokio::task::spawn_blocking(move || rclrs::spin(node)); + let rclrs_spin = tokio::task::spawn_blocking(move || executor.spin(SpinOptions::default())); let response = future.await?; println!( diff --git a/examples/minimal_client_service/src/minimal_service.rs b/examples/minimal_client_service/src/minimal_service.rs index b4149c817..0fe681dbf 100644 --- a/examples/minimal_client_service/src/minimal_service.rs +++ b/examples/minimal_client_service/src/minimal_service.rs @@ -1,6 +1,5 @@ -use std::env; - use anyhow::{Error, Result}; +use rclrs::*; fn handle_service( _request_header: &rclrs::rmw_request_id_t, @@ -13,13 +12,16 @@ fn handle_service( } fn main() -> Result<(), Error> { - let context = rclrs::Context::new(env::args())?; + let mut executor = Context::default_from_env()?.create_basic_executor(); - let node = rclrs::create_node(&context, "minimal_service")?; + let node = executor.create_node("minimal_service")?; let _server = node .create_service::("add_two_ints", handle_service)?; println!("Starting server"); - rclrs::spin(node).map_err(|err| err.into()) + executor + .spin(SpinOptions::default()) + .first_error() + .map_err(|err| err.into()) } diff --git a/examples/minimal_pub_sub/src/minimal_publisher.rs b/examples/minimal_pub_sub/src/minimal_publisher.rs index 720086917..e74f4e05c 100644 --- a/examples/minimal_pub_sub/src/minimal_publisher.rs +++ b/examples/minimal_pub_sub/src/minimal_publisher.rs @@ -1,14 +1,13 @@ -use std::env; - use anyhow::{Error, Result}; +use rclrs::*; fn main() -> Result<(), Error> { - let context = rclrs::Context::new(env::args())?; + let context = Context::default_from_env()?; + let executor = context.create_basic_executor(); - let node = rclrs::create_node(&context, "minimal_publisher")?; + let node = executor.create_node("minimal_publisher")?; - let publisher = - node.create_publisher::("topic", rclrs::QOS_PROFILE_DEFAULT)?; + let publisher = node.create_publisher::("topic", QOS_PROFILE_DEFAULT)?; let mut message = std_msgs::msg::String::default(); diff --git a/examples/minimal_pub_sub/src/minimal_subscriber.rs b/examples/minimal_pub_sub/src/minimal_subscriber.rs index ebc5fc194..f835bb2ba 100644 --- a/examples/minimal_pub_sub/src/minimal_subscriber.rs +++ b/examples/minimal_pub_sub/src/minimal_subscriber.rs @@ -1,17 +1,17 @@ -use std::env; - use anyhow::{Error, Result}; +use rclrs::*; fn main() -> Result<(), Error> { - let context = rclrs::Context::new(env::args())?; + let context = Context::default_from_env()?; + let mut executor = context.create_basic_executor(); - let node = rclrs::create_node(&context, "minimal_subscriber")?; + let node = executor.create_node("minimal_subscriber")?; let mut num_messages: usize = 0; let _subscription = node.create_subscription::( "topic", - rclrs::QOS_PROFILE_DEFAULT, + QOS_PROFILE_DEFAULT, move |msg: std_msgs::msg::String| { num_messages += 1; println!("I heard: '{}'", msg.data); @@ -19,5 +19,8 @@ fn main() -> Result<(), Error> { }, )?; - rclrs::spin(node).map_err(|err| err.into()) + executor + .spin(SpinOptions::default()) + .first_error() + .map_err(|err| err.into()) } diff --git a/examples/minimal_pub_sub/src/minimal_two_nodes.rs b/examples/minimal_pub_sub/src/minimal_two_nodes.rs index fb03574a2..72eb55129 100644 --- a/examples/minimal_pub_sub/src/minimal_two_nodes.rs +++ b/examples/minimal_pub_sub/src/minimal_two_nodes.rs @@ -1,23 +1,20 @@ -use std::{ - env, - sync::{ - atomic::{AtomicU32, Ordering}, - Arc, Mutex, - }, +use rclrs::*; +use std::sync::{ + atomic::{AtomicU32, Ordering}, + Arc, Mutex, }; use anyhow::{Error, Result}; struct MinimalSubscriber { num_messages: AtomicU32, - node: Arc, - subscription: Mutex>>>, + node: Arc, + subscription: Mutex>>>, } impl MinimalSubscriber { - pub fn new(name: &str, topic: &str) -> Result, rclrs::RclrsError> { - let context = rclrs::Context::new(env::args())?; - let node = rclrs::create_node(&context, name)?; + pub fn new(executor: &Executor, name: &str, topic: &str) -> Result, RclrsError> { + let node = executor.create_node(name)?; let minimal_subscriber = Arc::new(MinimalSubscriber { num_messages: 0.into(), node, @@ -29,7 +26,7 @@ impl MinimalSubscriber { .node .create_subscription::( topic, - rclrs::QOS_PROFILE_DEFAULT, + QOS_PROFILE_DEFAULT, move |msg: std_msgs::msg::String| { minimal_subscriber_aux.callback(msg); }, @@ -50,16 +47,18 @@ impl MinimalSubscriber { } fn main() -> Result<(), Error> { - let publisher_context = rclrs::Context::new(env::args())?; - let publisher_node = rclrs::create_node(&publisher_context, "minimal_publisher")?; + let mut executor = Context::default_from_env()?.create_basic_executor(); + let publisher_node = executor.create_node("minimal_publisher")?; - let subscriber_node_one = MinimalSubscriber::new("minimal_subscriber_one", "topic")?; - let subscriber_node_two = MinimalSubscriber::new("minimal_subscriber_two", "topic")?; + let _subscriber_node_one = + MinimalSubscriber::new(&executor, "minimal_subscriber_one", "topic")?; + let _subscriber_node_two = + MinimalSubscriber::new(&executor, "minimal_subscriber_two", "topic")?; - let publisher = publisher_node - .create_publisher::("topic", rclrs::QOS_PROFILE_DEFAULT)?; + let publisher = + publisher_node.create_publisher::("topic", QOS_PROFILE_DEFAULT)?; - std::thread::spawn(move || -> Result<(), rclrs::RclrsError> { + std::thread::spawn(move || -> Result<(), RclrsError> { let mut message = std_msgs::msg::String::default(); let mut publish_count: u32 = 1; loop { @@ -71,11 +70,8 @@ fn main() -> Result<(), Error> { } }); - let executor = rclrs::SingleThreadedExecutor::new(); - - executor.add_node(&publisher_node)?; - executor.add_node(&subscriber_node_one.node)?; - executor.add_node(&subscriber_node_two.node)?; - - executor.spin().map_err(|err| err.into()) + executor + .spin(SpinOptions::default()) + .first_error() + .map_err(|err| err.into()) } diff --git a/examples/minimal_pub_sub/src/zero_copy_publisher.rs b/examples/minimal_pub_sub/src/zero_copy_publisher.rs index 5e73b5de7..e950ab8d1 100644 --- a/examples/minimal_pub_sub/src/zero_copy_publisher.rs +++ b/examples/minimal_pub_sub/src/zero_copy_publisher.rs @@ -1,14 +1,14 @@ -use std::env; - use anyhow::{Error, Result}; +use rclrs::*; fn main() -> Result<(), Error> { - let context = rclrs::Context::new(env::args())?; + let context = Context::default_from_env()?; + let executor = context.create_basic_executor(); - let node = rclrs::create_node(&context, "minimal_publisher")?; + let node = executor.create_node("minimal_publisher")?; let publisher = - node.create_publisher::("topic", rclrs::QOS_PROFILE_DEFAULT)?; + node.create_publisher::("topic", QOS_PROFILE_DEFAULT)?; let mut publish_count: u32 = 1; diff --git a/examples/minimal_pub_sub/src/zero_copy_subscriber.rs b/examples/minimal_pub_sub/src/zero_copy_subscriber.rs index 9551dba0e..47d87f11c 100644 --- a/examples/minimal_pub_sub/src/zero_copy_subscriber.rs +++ b/examples/minimal_pub_sub/src/zero_copy_subscriber.rs @@ -1,23 +1,25 @@ -use std::env; - use anyhow::{Error, Result}; +use rclrs::*; fn main() -> Result<(), Error> { - let context = rclrs::Context::new(env::args())?; + let mut executor = Context::default_from_env()?.create_basic_executor(); - let node = rclrs::create_node(&context, "minimal_subscriber")?; + let node = executor.create_node("minimal_subscriber")?; let mut num_messages: usize = 0; let _subscription = node.create_subscription::( "topic", - rclrs::QOS_PROFILE_DEFAULT, - move |msg: rclrs::ReadOnlyLoanedMessage<'_, std_msgs::msg::UInt32>| { + QOS_PROFILE_DEFAULT, + move |msg: ReadOnlyLoanedMessage<'_, std_msgs::msg::UInt32>| { num_messages += 1; println!("I heard: '{}'", msg.data); println!("(Got {} messages so far)", num_messages); }, )?; - rclrs::spin(node).map_err(|err| err.into()) + executor + .spin(SpinOptions::default()) + .first_error() + .map_err(|err| err.into()) } diff --git a/examples/rust_pubsub/src/simple_publisher.rs b/examples/rust_pubsub/src/simple_publisher.rs index 98d0e0f74..ae0739e19 100644 --- a/examples/rust_pubsub/src/simple_publisher.rs +++ b/examples/rust_pubsub/src/simple_publisher.rs @@ -1,36 +1,37 @@ -use rclrs::{create_node, Context, Node, Publisher, RclrsError, QOS_PROFILE_DEFAULT}; +use rclrs::*; use std::{sync::Arc, thread, time::Duration}; use std_msgs::msg::String as StringMsg; -struct SimplePublisherNode { - node: Arc, - _publisher: Arc>, + +struct SimplePublisher { + publisher: Arc>, } -impl SimplePublisherNode { - fn new(context: &Context) -> Result { - let node = create_node(context, "simple_publisher").unwrap(); - let _publisher = node + +impl SimplePublisher { + fn new(executor: &Executor) -> Result { + let node = executor.create_node("simple_publisher").unwrap(); + let publisher = node .create_publisher("publish_hello", QOS_PROFILE_DEFAULT) .unwrap(); - Ok(Self { node, _publisher }) + Ok(Self { publisher }) } fn publish_data(&self, increment: i32) -> Result { let msg: StringMsg = StringMsg { data: format!("Hello World {}", increment), }; - self._publisher.publish(msg).unwrap(); + self.publisher.publish(msg).unwrap(); Ok(increment + 1_i32) } } fn main() -> Result<(), RclrsError> { - let context = Context::new(std::env::args()).unwrap(); - let publisher = Arc::new(SimplePublisherNode::new(&context).unwrap()); + let mut executor = Context::default_from_env().unwrap().create_basic_executor(); + let publisher = Arc::new(SimplePublisher::new(&executor).unwrap()); let publisher_other_thread = Arc::clone(&publisher); let mut count: i32 = 0; thread::spawn(move || loop { thread::sleep(Duration::from_millis(1000)); count = publisher_other_thread.publish_data(count).unwrap(); }); - rclrs::spin(publisher.node.clone()) + executor.spin(SpinOptions::default()).first_error() } diff --git a/examples/rust_pubsub/src/simple_subscriber.rs b/examples/rust_pubsub/src/simple_subscriber.rs index a0d02bb4c..b0d26dea0 100644 --- a/examples/rust_pubsub/src/simple_subscriber.rs +++ b/examples/rust_pubsub/src/simple_subscriber.rs @@ -1,19 +1,19 @@ -use rclrs::{create_node, Context, Node, RclrsError, Subscription, QOS_PROFILE_DEFAULT}; +use rclrs::*; use std::{ - env, sync::{Arc, Mutex}, thread, time::Duration, }; use std_msgs::msg::String as StringMsg; + pub struct SimpleSubscriptionNode { - node: Arc, _subscriber: Arc>, data: Arc>>, } + impl SimpleSubscriptionNode { - fn new(context: &Context) -> Result { - let node = create_node(context, "simple_subscription").unwrap(); + fn new(executor: &Executor) -> Result { + let node = executor.create_node("simple_subscription").unwrap(); let data: Arc>> = Arc::new(Mutex::new(None)); let data_mut: Arc>> = Arc::clone(&data); let _subscriber = node @@ -25,11 +25,7 @@ impl SimpleSubscriptionNode { }, ) .unwrap(); - Ok(Self { - node, - _subscriber, - data, - }) + Ok(Self { _subscriber, data }) } fn data_callback(&self) -> Result<(), RclrsError> { if let Some(data) = self.data.lock().unwrap().as_ref() { @@ -41,12 +37,12 @@ impl SimpleSubscriptionNode { } } fn main() -> Result<(), RclrsError> { - let context = Context::new(env::args()).unwrap(); - let subscription = Arc::new(SimpleSubscriptionNode::new(&context).unwrap()); + let mut executor = Context::default_from_env().unwrap().create_basic_executor(); + let subscription = Arc::new(SimpleSubscriptionNode::new(&executor).unwrap()); let subscription_other_thread = Arc::clone(&subscription); thread::spawn(move || loop { thread::sleep(Duration::from_millis(1000)); subscription_other_thread.data_callback().unwrap() }); - rclrs::spin(subscription.node.clone()) + executor.spin(SpinOptions::default()).first_error() } diff --git a/rclrs/src/context.rs b/rclrs/src/context.rs index 524169bb2..1aeeaf104 100644 --- a/rclrs/src/context.rs +++ b/rclrs/src/context.rs @@ -78,34 +78,40 @@ pub(crate) struct ContextHandle { logging: Arc, } +impl Default for Context { + fn default() -> Self { + // SAFETY: It should always be valid to instantiate a context with no + // arguments, no parameters, no options, etc. + Self::new([], InitOptions::default()).expect("Failed to instantiate a default context") + } +} + impl Context { /// Creates a new context. /// - /// Usually this would be called with `std::env::args()`, analogously to `rclcpp::init()`. - /// See also the official "Passing ROS arguments to nodes via the command-line" tutorial. + /// * `args` - A sequence of strings that resembles command line arguments + /// that users can pass into a ROS executable. See [the official tutorial][1] + /// to know what these arguments may look like. To simply pass in the arguments + /// that the user has provided from the command line, call [`Self::from_env`] + /// or [`Self::default_from_env`] instead. /// - /// Creating a context will fail if the args contain invalid ROS arguments. + /// * `options` - Additional options that your application can use to override + /// settings that would otherwise be determined by the environment. /// - /// # Example - /// ``` - /// # use rclrs::Context; - /// assert!(Context::new([]).is_ok()); - /// let invalid_remapping = ["--ros-args", "-r", ":=:*/]"].map(String::from); - /// assert!(Context::new(invalid_remapping).is_err()); - /// ``` - pub fn new(args: impl IntoIterator) -> Result { - Self::new_with_options(args, InitOptions::new()) - } - - /// Same as [`Context::new`] except you can additionally provide initialization options. + /// Creating a context will fail if `args` contains invalid ROS arguments. /// /// # Example /// ``` /// use rclrs::{Context, InitOptions}; - /// let context = Context::new_with_options([], InitOptions::new().with_domain_id(Some(5))).unwrap(); + /// let context = Context::new( + /// std::env::args(), + /// InitOptions::new().with_domain_id(Some(5)), + /// ).unwrap(); /// assert_eq!(context.domain_id(), 5); - /// ```` - pub fn new_with_options( + /// ``` + /// + /// [1]: https://docs.ros.org/en/rolling/How-To-Guides/Node-arguments.html + pub fn new( args: impl IntoIterator, options: InitOptions, ) -> Result { @@ -165,6 +171,18 @@ impl Context { }) } + /// Same as [`Self::new`] but [`std::env::args`] is automatically passed in + /// for `args`. + pub fn from_env(options: InitOptions) -> Result { + Self::new(std::env::args(), options) + } + + /// Same as [`Self::from_env`] but the default [`InitOptions`] is passed in + /// for `options`. + pub fn default_from_env() -> Result { + Self::new(std::env::args(), InitOptions::default()) + } + /// Returns the ROS domain ID that the context is using. /// /// The domain ID controls which nodes can send messages to each other, see the [ROS 2 concept article][1]. @@ -265,14 +283,14 @@ mod tests { #[test] fn test_create_context() -> Result<(), RclrsError> { // If the context fails to be created, this will cause a panic - let _ = Context::new(vec![])?; + let _ = Context::new(vec![], InitOptions::default())?; Ok(()) } #[test] fn test_context_ok() -> Result<(), RclrsError> { // If the context fails to be created, this will cause a panic - let created_context = Context::new(vec![]).unwrap(); + let created_context = Context::new(vec![], InitOptions::default()).unwrap(); assert!(created_context.ok()); Ok(()) diff --git a/rclrs/src/error.rs b/rclrs/src/error.rs index 3eba2549f..527a4d3a6 100644 --- a/rclrs/src/error.rs +++ b/rclrs/src/error.rs @@ -34,6 +34,33 @@ pub enum RclrsError { AlreadyAddedToWaitSet, } +impl RclrsError { + /// Returns true if the error was due to a timeout, otherwise returns false. + pub fn is_timeout(&self) -> bool { + matches!( + self, + RclrsError::RclError { + code: RclReturnCode::Timeout, + .. + } + ) + } + + /// Returns true if the error was because a subscription, service, or client + /// take failed, otherwise returns false. + pub fn is_take_failed(&self) -> bool { + matches!( + self, + RclrsError::RclError { + code: RclReturnCode::SubscriptionTakeFailed + | RclReturnCode::ServiceTakeFailed + | RclReturnCode::ClientTakeFailed, + .. + } + ) + } +} + impl Display for RclrsError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -352,3 +379,80 @@ impl ToResult for rcl_ret_t { to_rclrs_result(*self) } } + +/// A helper trait to disregard timeouts as not an error. +pub trait RclrsErrorFilter { + /// Get the first error available, or Ok(()) if there are no errors. + fn first_error(self) -> Result<(), RclrsError>; + + /// If the result was a timeout error, change it to `Ok(())`. + fn timeout_ok(self) -> Self; + + /// If a subscription, service, or client take failed, change the result + /// to be `Ok(())`. + fn take_failed_ok(self) -> Self; + + /// Some error types just indicate an early termination but do not indicate + /// that anything in the system has misbehaved. This filters out anything + /// that is part of the normal operation of rcl. + fn ignore_non_errors(self) -> Self + where + Self: Sized, + { + self.timeout_ok().take_failed_ok() + } +} + +impl RclrsErrorFilter for Result<(), RclrsError> { + fn first_error(self) -> Result<(), RclrsError> { + self + } + + fn timeout_ok(self) -> Result<(), RclrsError> { + match self { + Ok(()) => Ok(()), + Err(err) => { + if err.is_timeout() { + Ok(()) + } else { + Err(err) + } + } + } + } + + fn take_failed_ok(self) -> Result<(), RclrsError> { + match self { + Err(err) => { + if err.is_take_failed() { + // Spurious wakeup - this may happen even when a waitset indicated that + // work was ready, so we won't report it as an error + Ok(()) + } else { + Err(err) + } + } + other => other, + } + } +} + +impl RclrsErrorFilter for Vec { + fn first_error(mut self) -> Result<(), RclrsError> { + if self.is_empty() { + return Ok(()); + } + + Err(self.remove(0)) + } + + fn timeout_ok(mut self) -> Self { + self.retain(|err| !err.is_timeout()); + self + } + + fn take_failed_ok(mut self) -> Self { + self.retain(|err| !err.is_take_failed()); + self + } +} diff --git a/rclrs/src/executor.rs b/rclrs/src/executor.rs index 37c43a68e..437be1cc1 100644 --- a/rclrs/src/executor.rs +++ b/rclrs/src/executor.rs @@ -1,45 +1,60 @@ -use crate::{rcl_bindings::rcl_context_is_valid, Node, RclReturnCode, RclrsError, WaitSet}; +use crate::{ + rcl_bindings::rcl_context_is_valid, Context, ContextHandle, IntoNodeOptions, Node, RclrsError, + WaitSet, +}; use std::{ sync::{Arc, Mutex, Weak}, time::Duration, }; /// Single-threaded executor implementation. -pub struct SingleThreadedExecutor { +pub struct Executor { + context: Arc, nodes_mtx: Mutex>>, } -impl Default for SingleThreadedExecutor { - fn default() -> Self { - Self::new() +impl Executor { + /// Create a [`Node`] that will run on this Executor. + pub fn create_node<'a>( + &'a self, + options: impl IntoNodeOptions<'a>, + ) -> Result, RclrsError> { + let options = options.into_node_options(); + let node = options.build(&self.context)?; + self.nodes_mtx.lock().unwrap().push(Arc::downgrade(&node)); + Ok(node) } -} -impl SingleThreadedExecutor { - /// Creates a new executor. - pub fn new() -> Self { - SingleThreadedExecutor { - nodes_mtx: Mutex::new(Vec::new()), - } - } + /// Spin the Executor. The current thread will be blocked until the Executor + /// stops spinning. + /// + /// [`SpinOptions`] can be used to automatically stop the spinning when + /// certain conditions are met. Use `SpinOptions::default()` to allow the + /// Executor to keep spinning indefinitely. + pub fn spin(&mut self, options: SpinOptions) -> Vec { + loop { + if self.nodes_mtx.lock().unwrap().is_empty() { + // Nothing to spin for, so just quit here + return Vec::new(); + } - /// Add a node to the executor. - pub fn add_node(&self, node: &Arc) -> Result<(), RclrsError> { - { self.nodes_mtx.lock().unwrap() }.push(Arc::downgrade(node)); - Ok(()) - } + if let Err(err) = self.spin_once(options.timeout) { + return vec![err]; + } - /// Remove a node from the executor. - pub fn remove_node(&self, node: Arc) -> Result<(), RclrsError> { - { self.nodes_mtx.lock().unwrap() } - .retain(|n| !n.upgrade().map(|n| Arc::ptr_eq(&n, &node)).unwrap_or(false)); - Ok(()) + if options.only_next_available_work { + // We were only suppposed to spin once, so quit here + return Vec::new(); + } + + std::thread::yield_now(); + } } /// Polls the nodes for new messages and executes the corresponding callbacks. /// /// This function additionally checks that the context is still valid. - pub fn spin_once(&self, timeout: Option) -> Result<(), RclrsError> { + fn spin_once(&self, timeout: Option) -> Result<(), RclrsError> { for node in { self.nodes_mtx.lock().unwrap() } .iter() .filter_map(Weak::upgrade) @@ -66,19 +81,63 @@ impl SingleThreadedExecutor { Ok(()) } - /// Convenience function for calling [`SingleThreadedExecutor::spin_once`] in a loop. - pub fn spin(&self) -> Result<(), RclrsError> { - while !{ self.nodes_mtx.lock().unwrap() }.is_empty() { - match self.spin_once(None) { - Ok(_) - | Err(RclrsError::RclError { - code: RclReturnCode::Timeout, - .. - }) => std::thread::yield_now(), - error => return error, - } + /// Used by [`Context`] to create the `Executor`. Users cannot call this + /// function. + pub(crate) fn new(context: Arc) -> Self { + Self { + context, + nodes_mtx: Mutex::new(Vec::new()), } + } +} - Ok(()) +/// A bundle of optional conditions that a user may want to impose on how long +/// an executor spins for. +/// +/// By default the executor will be allowed to spin indefinitely. +#[non_exhaustive] +#[derive(Default)] +pub struct SpinOptions { + /// Only perform the next available work. This is similar to spin_once in + /// rclcpp and rclpy. + /// + /// To only process work that is immediately available without waiting at all, + /// set a timeout of zero. + pub only_next_available_work: bool, + /// Stop waiting after this duration of time has passed. Use `Some(0)` to not + /// wait any amount of time. Use `None` to wait an infinite amount of time. + pub timeout: Option, +} + +impl SpinOptions { + /// Use default spin options. + pub fn new() -> Self { + Self::default() + } + + /// Behave like spin_once in rclcpp and rclpy. + pub fn spin_once() -> Self { + Self { + only_next_available_work: true, + ..Default::default() + } + } + + /// Stop spinning once this durtion of time is reached. + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self + } +} + +/// This trait allows [`Context`] to create a basic executor. +pub trait CreateBasicExecutor { + /// Create a basic executor associated with this [`Context`]. + fn create_basic_executor(&self) -> Executor; +} + +impl CreateBasicExecutor for Context { + fn create_basic_executor(&self) -> Executor { + Executor::new(Arc::clone(&self.handle)) } } diff --git a/rclrs/src/lib.rs b/rclrs/src/lib.rs index 3a22c6da8..73d478191 100644 --- a/rclrs/src/lib.rs +++ b/rclrs/src/lib.rs @@ -31,8 +31,6 @@ mod rcl_bindings; #[cfg(feature = "dyn_msg")] pub mod dynamic_message; -use std::{sync::Arc, time::Duration}; - pub use arguments::*; pub use client::*; pub use clock::*; @@ -50,66 +48,3 @@ pub use subscription::*; pub use time::*; use time_source::*; pub use wait::*; - -/// Polls the node for new messages and executes the corresponding callbacks. -/// -/// See [`WaitSet::wait`] for the meaning of the `timeout` parameter. -/// -/// This may under some circumstances return -/// [`SubscriptionTakeFailed`][1], [`ClientTakeFailed`][1], [`ServiceTakeFailed`][1] when the wait -/// set spuriously wakes up. -/// This can usually be ignored. -/// -/// [1]: crate::RclReturnCode -pub fn spin_once(node: Arc, timeout: Option) -> Result<(), RclrsError> { - let executor = SingleThreadedExecutor::new(); - executor.add_node(&node)?; - executor.spin_once(timeout) -} - -/// Convenience function for calling [`spin_once`] in a loop. -pub fn spin(node: Arc) -> Result<(), RclrsError> { - let executor = SingleThreadedExecutor::new(); - executor.add_node(&node)?; - executor.spin() -} - -/// Creates a new node in the empty namespace. -/// -/// Convenience function equivalent to [`Node::new`][1]. -/// Please see that function's documentation. -/// -/// [1]: crate::Node::new -/// -/// # Example -/// ``` -/// # use rclrs::{Context, RclrsError}; -/// let ctx = Context::new([])?; -/// let node = rclrs::create_node(&ctx, "my_node"); -/// assert!(node.is_ok()); -/// # Ok::<(), RclrsError>(()) -/// ``` -pub fn create_node(context: &Context, node_name: &str) -> Result, RclrsError> { - Node::new(context, node_name) -} - -/// Creates a [`NodeBuilder`]. -/// -/// Convenience function equivalent to [`NodeBuilder::new()`][1] and [`Node::builder()`][2]. -/// Please see that function's documentation. -/// -/// [1]: crate::NodeBuilder::new -/// [2]: crate::Node::builder -/// -/// # Example -/// ``` -/// # use rclrs::{Context, RclrsError}; -/// let context = Context::new([])?; -/// let node_builder = rclrs::create_node_builder(&context, "my_node"); -/// let node = node_builder.build()?; -/// assert_eq!(node.name(), "my_node"); -/// # Ok::<(), RclrsError>(()) -/// ``` -pub fn create_node_builder(context: &Context, node_name: &str) -> NodeBuilder { - Node::builder(context, node_name) -} diff --git a/rclrs/src/logging.rs b/rclrs/src/logging.rs index 5143ae35c..cbc30114f 100644 --- a/rclrs/src/logging.rs +++ b/rclrs/src/logging.rs @@ -25,13 +25,13 @@ pub use logger::*; /// # Examples /// /// ``` -/// use rclrs::{log, ToLogParams}; +/// use rclrs::*; /// use std::sync::Mutex; /// use std::time::Duration; /// use std::env; /// -/// let context = rclrs::Context::new(env::args()).unwrap(); -/// let node = rclrs::Node::new(&context, "test_node").unwrap(); +/// let executor = rclrs::Context::default().create_basic_executor(); +/// let node = executor.create_node("test_node").unwrap(); /// /// log!(node.debug(), "Simple debug message"); /// let some_variable = 43; @@ -473,7 +473,10 @@ macro_rules! function { #[cfg(test)] mod tests { use crate::{log_handler::*, test_helpers::*, *}; - use std::sync::Mutex; + use std::{ + sync::{Arc, Mutex}, + time::Duration, + }; #[test] fn test_logging_macros() -> Result<(), RclrsError> { diff --git a/rclrs/src/node.rs b/rclrs/src/node.rs index b51b59817..0adefadd6 100644 --- a/rclrs/src/node.rs +++ b/rclrs/src/node.rs @@ -1,5 +1,5 @@ -mod builder; mod graph; +mod node_options; use std::{ cmp::PartialEq, ffi::CStr, @@ -11,12 +11,12 @@ use std::{ use rosidl_runtime_rs::Message; -pub use self::{builder::*, graph::*}; +pub use self::{graph::*, node_options::*}; use crate::{ - rcl_bindings::*, Client, ClientBase, Clock, Context, ContextHandle, GuardCondition, LogParams, - Logger, ParameterBuilder, ParameterInterface, ParameterVariant, Parameters, Publisher, - QoSProfile, RclrsError, Service, ServiceBase, Subscription, SubscriptionBase, - SubscriptionCallback, TimeSource, ToLogParams, ENTITY_LIFECYCLE_MUTEX, + rcl_bindings::*, Client, ClientBase, Clock, ContextHandle, GuardCondition, LogParams, Logger, + ParameterBuilder, ParameterInterface, ParameterVariant, Parameters, Publisher, QoSProfile, + RclrsError, Service, ServiceBase, Subscription, SubscriptionBase, SubscriptionCallback, + TimeSource, ToLogParams, ENTITY_LIFECYCLE_MUTEX, }; // SAFETY: The functions accessing this type, including drop(), shouldn't care about the thread @@ -28,9 +28,13 @@ unsafe impl Send for rcl_node_t {} /// Nodes are a core concept in ROS 2. Refer to the official ["Understanding ROS 2 nodes"][1] /// tutorial for an introduction. /// -/// Ownership of the node is shared with all [`Publisher`]s and [`Subscription`]s created from it. -/// That means that even after the node itself is dropped, it will continue to exist and be -/// displayed by e.g. `ros2 topic` as long as its publishers and subscriptions are not dropped. +/// Ownership of the node is shared with all the primitives such as [`Publisher`]s and [`Subscription`]s +/// that are created from it. That means that even after the `Node` itself is dropped, it will continue +/// to exist and be displayed by e.g. `ros2 topic` as long as any one of its primitives is not dropped. +/// +/// # Creating +/// Use [`Executor::create_node`][7] to create a new node. Pass in [`NodeOptions`] to set all the different +/// options for node creation, or just pass in a string for the node's name if the default options are okay. /// /// # Naming /// A node has a *name* and a *namespace*. @@ -48,16 +52,19 @@ unsafe impl Send for rcl_node_t {} /// In that sense, the parameters to the node creation functions are only the _default_ namespace and /// name. /// See also the [official tutorial][1] on the command line arguments for ROS nodes, and the -/// [`Node::namespace()`] and [`Node::name()`] functions for examples. +/// [`Node::namespace()`][3] and [`Node::name()`][4] functions for examples. /// /// ## Rules for valid names /// The rules for valid node names and node namespaces are explained in -/// [`NodeBuilder::new()`][3] and [`NodeBuilder::namespace()`][4]. +/// [`NodeOptions::new()`][5] and [`NodeOptions::namespace()`][6]. /// /// [1]: https://docs.ros.org/en/rolling/Tutorials/Understanding-ROS2-Nodes.html /// [2]: https://docs.ros.org/en/rolling/How-To-Guides/Node-arguments.html -/// [3]: crate::NodeBuilder::new -/// [4]: crate::NodeBuilder::namespace +/// [3]: Node::namespace +/// [4]: Node::name +/// [5]: crate::NodeOptions::new +/// [6]: crate::NodeOptions::namespace +/// [7]: crate::Executor::create_node pub struct Node { pub(crate) clients_mtx: Mutex>>, pub(crate) guard_conditions_mtx: Mutex>>, @@ -65,8 +72,8 @@ pub struct Node { pub(crate) subscriptions_mtx: Mutex>>, time_source: TimeSource, parameter: ParameterInterface, - pub(crate) handle: Arc, logger: Logger, + pub(crate) handle: Arc, } /// This struct manages the lifetime of an `rcl_node_t`, and accounts for its @@ -131,14 +138,6 @@ impl fmt::Debug for Node { } impl Node { - /// Creates a new node in the empty namespace. - /// - /// See [`NodeBuilder::new()`] for documentation. - #[allow(clippy::new_ret_no_self)] - pub fn new(context: &Context, node_name: &str) -> Result, RclrsError> { - Self::builder(context, node_name).build() - } - /// Returns the clock associated with this node. pub fn get_clock(&self) -> Clock { self.time_source.get_clock() @@ -151,15 +150,15 @@ impl Node { /// /// # Example /// ``` - /// # use rclrs::{Context, RclrsError}; + /// # use rclrs::*; /// // Without remapping - /// let context = Context::new([])?; - /// let node = rclrs::create_node(&context, "my_node")?; + /// let executor = Context::default().create_basic_executor(); + /// let node = executor.create_node("my_node")?; /// assert_eq!(node.name(), "my_node"); /// // With remapping /// let remapping = ["--ros-args", "-r", "__node:=your_node"].map(String::from); - /// let context_r = Context::new(remapping)?; - /// let node_r = rclrs::create_node(&context_r, "my_node")?; + /// let executor_r = Context::new(remapping, InitOptions::default())?.create_basic_executor(); + /// let node_r = executor_r.create_node("my_node")?; /// assert_eq!(node_r.name(), "your_node"); /// # Ok::<(), RclrsError>(()) /// ``` @@ -174,18 +173,18 @@ impl Node { /// /// # Example /// ``` - /// # use rclrs::{Context, RclrsError}; + /// # use rclrs::*; /// // Without remapping - /// let context = Context::new([])?; - /// let node = - /// rclrs::create_node_builder(&context, "my_node") - /// .namespace("/my/namespace") - /// .build()?; + /// let executor = Context::default().create_basic_executor(); + /// let node = executor.create_node( + /// "my_node" + /// .namespace("/my/namespace") + /// )?; /// assert_eq!(node.namespace(), "/my/namespace"); /// // With remapping /// let remapping = ["--ros-args", "-r", "__ns:=/your_namespace"].map(String::from); - /// let context_r = Context::new(remapping)?; - /// let node_r = rclrs::create_node(&context_r, "my_node")?; + /// let executor_r = Context::new(remapping, InitOptions::default())?.create_basic_executor(); + /// let node_r = executor_r.create_node("my_node")?; /// assert_eq!(node_r.namespace(), "/your_namespace"); /// # Ok::<(), RclrsError>(()) /// ``` @@ -200,12 +199,12 @@ impl Node { /// /// # Example /// ``` - /// # use rclrs::{Context, RclrsError}; - /// let context = Context::new([])?; - /// let node = - /// rclrs::create_node_builder(&context, "my_node") - /// .namespace("/my/namespace") - /// .build()?; + /// # use rclrs::*; + /// let executor = Context::default().create_basic_executor(); + /// let node = executor.create_node( + /// "my_node" + /// .namespace("/my/namespace") + /// )?; /// assert_eq!(node.fully_qualified_name(), "/my/namespace/my_node"); /// # Ok::<(), RclrsError>(()) /// ``` @@ -238,12 +237,11 @@ impl Node { /// Creates a [`GuardCondition`][1] with no callback. /// /// A weak pointer to the `GuardCondition` is stored within this node. - /// When this node is added to a wait set (e.g. when calling `spin_once`[2] - /// with this node as an argument), the guard condition can be used to - /// interrupt the wait. + /// When this node is added to a wait set (e.g. when its executor is [spinning][2]), + /// the guard condition can be used to interrupt the wait. /// /// [1]: crate::GuardCondition - /// [2]: crate::spin_once + /// [2]: crate::Executor::spin pub fn create_guard_condition(&self) -> Arc { let guard_condition = Arc::new(GuardCondition::new_with_context_handle( Arc::clone(&self.handle.context_handle), @@ -257,12 +255,11 @@ impl Node { /// Creates a [`GuardCondition`][1] with a callback. /// /// A weak pointer to the `GuardCondition` is stored within this node. - /// When this node is added to a wait set (e.g. when calling `spin_once`[2] - /// with this node as an argument), the guard condition can be used to - /// interrupt the wait. + /// When this node is added to a wait set (e.g. when its executor is [spinning][2]), + /// the guard condition can be used to interrupt the wait. /// /// [1]: crate::GuardCondition - /// [2]: crate::spin_once + /// [2]: crate::Executor::spin pub fn create_guard_condition_with_callback(&mut self, callback: F) -> Arc where F: Fn() + Send + Sync + 'static, @@ -279,7 +276,6 @@ impl Node { /// Creates a [`Publisher`][1]. /// /// [1]: crate::Publisher - // TODO: make publisher's lifetime depend on node's lifetime pub fn create_publisher( &self, topic: &str, @@ -295,7 +291,6 @@ impl Node { /// Creates a [`Service`][1]. /// /// [1]: crate::Service - // TODO: make service's lifetime depend on node's lifetime pub fn create_service( &self, topic: &str, @@ -318,7 +313,6 @@ impl Node { /// Creates a [`Subscription`][1]. /// /// [1]: crate::Subscription - // TODO: make subscription's lifetime depend on node's lifetime pub fn create_subscription( &self, topic: &str, @@ -372,24 +366,24 @@ impl Node { /// Returns the ROS domain ID that the node is using. /// /// The domain ID controls which nodes can send messages to each other, see the [ROS 2 concept article][1]. - /// It can be set through the `ROS_DOMAIN_ID` environment variable. + /// It can be set through the `ROS_DOMAIN_ID` environment variable or by + /// passing custom [`NodeOptions`] into [`Context::new`][2] or [`Context::from_env`][3]. /// /// [1]: https://docs.ros.org/en/rolling/Concepts/About-Domain-ID.html + /// [2]: crate::Context::new + /// [3]: crate::Context::from_env /// /// # Example /// ``` - /// # use rclrs::{Context, RclrsError}; + /// # use rclrs::*; /// // Set default ROS domain ID to 10 here /// std::env::set_var("ROS_DOMAIN_ID", "10"); - /// let context = Context::new([])?; - /// let node = rclrs::create_node(&context, "domain_id_node")?; + /// let executor = Context::default().create_basic_executor(); + /// let node = executor.create_node("domain_id_node")?; /// let domain_id = node.domain_id(); /// assert_eq!(domain_id, 10); /// # Ok::<(), RclrsError>(()) /// ``` - // TODO: If node option is supported, - // add description about this function is for getting actual domain_id - // and about override of domain_id via node option pub fn domain_id(&self) -> usize { let rcl_node = self.handle.rcl_node.lock().unwrap(); let mut domain_id: usize = 0; @@ -409,9 +403,9 @@ impl Node { /// /// # Example /// ``` - /// # use rclrs::{Context, ParameterRange, RclrsError}; - /// let context = Context::new([])?; - /// let node = rclrs::create_node(&context, "domain_id_node")?; + /// # use rclrs::*; + /// let executor = Context::default().create_basic_executor(); + /// let node = executor.create_node("domain_id_node")?; /// // Set it to a range of 0-100, with a step of 2 /// let range = ParameterRange { /// lower: Some(0), @@ -447,25 +441,6 @@ impl Node { } } - /// Creates a [`NodeBuilder`][1] with the given name. - /// - /// Convenience function equivalent to [`NodeBuilder::new()`][2]. - /// - /// [1]: crate::NodeBuilder - /// [2]: crate::NodeBuilder::new - /// - /// # Example - /// ``` - /// # use rclrs::{Context, Node, RclrsError}; - /// let context = Context::new([])?; - /// let node = Node::builder(&context, "my_node").build()?; - /// assert_eq!(node.name(), "my_node"); - /// # Ok::<(), RclrsError>(()) - /// ``` - pub fn builder(context: &Context, node_name: &str) -> NodeBuilder { - NodeBuilder::new(context, node_name) - } - /// Get the logger associated with this Node. pub fn logger(&self) -> &Logger { &self.logger diff --git a/rclrs/src/node/graph.rs b/rclrs/src/node/graph.rs index 639a38e38..b1535bf64 100644 --- a/rclrs/src/node/graph.rs +++ b/rclrs/src/node/graph.rs @@ -454,7 +454,7 @@ fn convert_names_and_types( #[cfg(test)] mod tests { use super::*; - use crate::{Context, InitOptions}; + use crate::*; #[test] fn test_graph_empty() { @@ -482,11 +482,11 @@ mod tests { .map(|value: usize| if value != 99 { 99 } else { 98 }) .unwrap_or(99); - let context = - Context::new_with_options([], InitOptions::new().with_domain_id(Some(domain_id))) - .unwrap(); + let executor = Context::new([], InitOptions::new().with_domain_id(Some(domain_id))) + .unwrap() + .create_basic_executor(); let node_name = "test_publisher_names_and_types"; - let node = Node::new(&context, node_name).unwrap(); + let node = executor.create_node(node_name).unwrap(); let check_rosout = |topics: HashMap>| { // rosout shows up in humble and iron, even if the graph is empty @@ -558,9 +558,9 @@ mod tests { #[test] fn test_node_names() { - let context = Context::new([]).unwrap(); + let executor = Context::default().create_basic_executor(); let node_name = "test_node_names"; - let node = Node::new(&context, node_name).unwrap(); + let node = executor.create_node(node_name).unwrap(); let names_and_namespaces = node.get_node_names().unwrap(); @@ -574,9 +574,9 @@ mod tests { #[test] fn test_node_names_with_enclaves() { - let context = Context::new([]).unwrap(); + let executor = Context::default().create_basic_executor(); let node_name = "test_node_names_with_enclaves"; - let node = Node::new(&context, node_name).unwrap(); + let node = executor.create_node(node_name).unwrap(); let names_and_namespaces = node.get_node_names_with_enclaves().unwrap(); diff --git a/rclrs/src/node/builder.rs b/rclrs/src/node/node_options.rs similarity index 64% rename from rclrs/src/node/builder.rs rename to rclrs/src/node/node_options.rs index 1e7a9fc63..9f5a41581 100644 --- a/rclrs/src/node/builder.rs +++ b/rclrs/src/node/node_options.rs @@ -1,110 +1,21 @@ use std::{ + borrow::Borrow, ffi::{CStr, CString}, sync::{atomic::AtomicBool, Arc, Mutex}, }; use crate::{ - rcl_bindings::*, ClockType, Context, ContextHandle, Logger, Node, NodeHandle, - ParameterInterface, QoSProfile, RclrsError, TimeSource, ToResult, ENTITY_LIFECYCLE_MUTEX, - QOS_PROFILE_CLOCK, + rcl_bindings::*, ClockType, ContextHandle, Logger, Node, NodeHandle, ParameterInterface, + QoSProfile, RclrsError, TimeSource, ToResult, ENTITY_LIFECYCLE_MUTEX, QOS_PROFILE_CLOCK, }; -/// A builder for creating a [`Node`][1]. +/// This trait helps to build [`NodeOptions`] which can be passed into +/// [`Executor::create_node`][1]. /// -/// The builder pattern allows selectively setting some fields, and leaving all others at their default values. -/// This struct instance can be created via [`Node::builder()`][2]. -/// -/// The default values for optional fields are: -/// - `namespace: "/"` -/// - `use_global_arguments: true` -/// - `arguments: []` -/// - `enable_rosout: true` -/// - `start_parameter_services: true` -/// - `clock_type: ClockType::RosTime` -/// - `clock_qos: QOS_PROFILE_CLOCK` -/// -/// # Example -/// ``` -/// # use rclrs::{Context, NodeBuilder, Node, RclrsError}; -/// let context = Context::new([])?; -/// // Building a node in a single expression -/// let node = NodeBuilder::new(&context, "foo_node").namespace("/bar").build()?; -/// assert_eq!(node.name(), "foo_node"); -/// assert_eq!(node.namespace(), "/bar"); -/// // Building a node via Node::builder() -/// let node = Node::builder(&context, "bar_node").build()?; -/// assert_eq!(node.name(), "bar_node"); -/// // Building a node step-by-step -/// let mut builder = Node::builder(&context, "goose"); -/// builder = builder.namespace("/duck/duck"); -/// let node = builder.build()?; -/// assert_eq!(node.fully_qualified_name(), "/duck/duck/goose"); -/// # Ok::<(), RclrsError>(()) -/// ``` -/// -/// [1]: crate::Node -/// [2]: crate::Node::builder -pub struct NodeBuilder { - context: Arc, - name: String, - namespace: String, - use_global_arguments: bool, - arguments: Vec, - enable_rosout: bool, - start_parameter_services: bool, - clock_type: ClockType, - clock_qos: QoSProfile, -} - -impl NodeBuilder { - /// Creates a builder for a node with the given name. - /// - /// See the [`Node` docs][1] for general information on node names. - /// - /// # Rules for valid node names - /// - /// The rules for a valid node name are checked by the [`rmw_validate_node_name()`][2] - /// function. They are: - /// - Must contain only the `a-z`, `A-Z`, `0-9`, and `_` characters - /// - Must not be empty and not be longer than `RMW_NODE_NAME_MAX_NAME_LENGTH` - /// - Must not start with a number - /// - /// Note that node name validation is delayed until [`NodeBuilder::build()`][3]. - /// - /// # Example - /// ``` - /// # use rclrs::{Context, NodeBuilder, RclrsError, RclReturnCode}; - /// let context = Context::new([])?; - /// // This is a valid node name - /// assert!(NodeBuilder::new(&context, "my_node").build().is_ok()); - /// // This is another valid node name (although not a good one) - /// assert!(NodeBuilder::new(&context, "_______").build().is_ok()); - /// // This is an invalid node name - /// assert!(matches!( - /// NodeBuilder::new(&context, "röböt") - /// .build() - /// .unwrap_err(), - /// RclrsError::RclError { code: RclReturnCode::NodeInvalidName, .. } - /// )); - /// # Ok::<(), RclrsError>(()) - /// ``` - /// - /// [1]: crate::Node#naming - /// [2]: https://docs.ros2.org/latest/api/rmw/validate__node__name_8h.html#a5690a285aed9735f89ef11950b6e39e3 - /// [3]: NodeBuilder::build - pub fn new(context: &Context, name: &str) -> NodeBuilder { - NodeBuilder { - context: Arc::clone(&context.handle), - name: name.to_string(), - namespace: "/".to_string(), - use_global_arguments: true, - arguments: vec![], - enable_rosout: true, - start_parameter_services: true, - clock_type: ClockType::RosTime, - clock_qos: QOS_PROFILE_CLOCK, - } - } +/// [1]: crate::Executor::create_node +pub trait IntoNodeOptions<'a>: Sized { + /// Conver the object into [`NodeOptions`] with default settings. + fn into_node_options(self) -> NodeOptions<'a>; /// Sets the node namespace. /// @@ -123,29 +34,29 @@ impl NodeBuilder { /// - Must not contain two or more `/` characters in a row /// - Must not have a `/` character at the end, except if `/` is the full namespace /// - /// Note that namespace validation is delayed until [`NodeBuilder::build()`][4]. + /// Note that namespace validation is delayed until [`Executor::create_node`][4]. /// /// # Example /// ``` - /// # use rclrs::{Context, Node, RclrsError, RclReturnCode}; - /// let context = Context::new([])?; + /// # use rclrs::*; + /// let executor = Context::default().create_basic_executor(); /// // This is a valid namespace - /// let builder_ok_ns = Node::builder(&context, "my_node").namespace("/some/nested/namespace"); - /// assert!(builder_ok_ns.build().is_ok()); + /// let options_ok_ns = "my_node".namespace("/some/nested/namespace"); + /// assert!(executor.create_node(options_ok_ns).is_ok()); /// // This is an invalid namespace /// assert!(matches!( - /// Node::builder(&context, "my_node") + /// executor.create_node( + /// "my_node" /// .namespace("/10_percent_luck/20_percent_skill") - /// .build() - /// .unwrap_err(), + /// ).unwrap_err(), /// RclrsError::RclError { code: RclReturnCode::NodeInvalidNamespace, .. } /// )); /// // A missing forward slash at the beginning is automatically added /// assert_eq!( - /// Node::builder(&context, "my_node") + /// executor.create_node( + /// "my_node" /// .namespace("foo") - /// .build()? - /// .namespace(), + /// )?.namespace(), /// "/foo" /// ); /// # Ok::<(), RclrsError>(()) @@ -154,10 +65,11 @@ impl NodeBuilder { /// [1]: crate::Node#naming /// [2]: http://design.ros2.org/articles/topic_and_service_names.html /// [3]: https://docs.ros2.org/latest/api/rmw/validate__namespace_8h.html#a043f17d240cf13df01321b19a469ee49 - /// [4]: NodeBuilder::build - pub fn namespace(mut self, namespace: &str) -> Self { - self.namespace = namespace.to_string(); - self + /// [4]: crate::Executor::create_node + fn namespace(self, namespace: &'a str) -> NodeOptions<'a> { + let mut options = self.into_node_options(); + options.namespace = namespace; + options } /// Enables or disables using global arguments. @@ -166,29 +78,30 @@ impl NodeBuilder { /// /// # Example /// ``` - /// # use rclrs::{Context, Node, NodeBuilder, RclrsError}; + /// # use rclrs::*; /// let context_args = ["--ros-args", "--remap", "__node:=your_node"] /// .map(String::from); - /// let context = Context::new(context_args)?; + /// let executor = Context::new(context_args, InitOptions::default())?.create_basic_executor(); /// // Ignore the global arguments: - /// let node_without_global_args = - /// rclrs::create_node_builder(&context, "my_node") - /// .use_global_arguments(false) - /// .build()?; + /// let node_without_global_args = executor.create_node( + /// "my_node" + /// .use_global_arguments(false) + /// )?; /// assert_eq!(node_without_global_args.name(), "my_node"); /// // Do not ignore the global arguments: - /// let node_with_global_args = - /// rclrs::create_node_builder(&context, "my_other_node") - /// .use_global_arguments(true) - /// .build()?; + /// let node_with_global_args = executor.create_node( + /// "my_other_node" + /// .use_global_arguments(true) + /// )?; /// assert_eq!(node_with_global_args.name(), "your_node"); /// # Ok::<(), RclrsError>(()) /// ``` /// /// [1]: crate::Context::new - pub fn use_global_arguments(mut self, enable: bool) -> Self { - self.use_global_arguments = enable; - self + fn use_global_arguments(self, enable: bool) -> NodeOptions<'a> { + let mut options = self.into_node_options(); + options.use_global_arguments = enable; + options } /// Sets node-specific command line arguments. @@ -201,27 +114,31 @@ impl NodeBuilder { /// /// # Example /// ``` - /// # use rclrs::{Context, Node, NodeBuilder, RclrsError}; + /// # use rclrs::*; /// // Usually, this would change the name of "my_node" to "context_args_node": /// let context_args = ["--ros-args", "--remap", "my_node:__node:=context_args_node"] /// .map(String::from); - /// let context = Context::new(context_args)?; + /// let executor = Context::new(context_args, InitOptions::default())?.create_basic_executor(); /// // But the node arguments will change it to "node_args_node": /// let node_args = ["--ros-args", "--remap", "my_node:__node:=node_args_node"] /// .map(String::from); - /// let node = - /// rclrs::create_node_builder(&context, "my_node") - /// .arguments(node_args) - /// .build()?; + /// let node = executor.create_node( + /// "my_node" + /// .arguments(node_args) + /// )?; /// assert_eq!(node.name(), "node_args_node"); /// # Ok::<(), RclrsError>(()) /// ``` /// /// [1]: crate::Context::new /// [2]: https://design.ros2.org/articles/ros_command_line_arguments.html - pub fn arguments(mut self, arguments: impl IntoIterator) -> Self { - self.arguments = arguments.into_iter().collect(); - self + fn arguments(self, arguments: Args) -> NodeOptions<'a> + where + Args::Item: ToString, + { + let mut options = self.into_node_options(); + options.arguments = arguments.into_iter().map(|item| item.to_string()).collect(); + options } /// Enables or disables logging to rosout. @@ -230,57 +147,161 @@ impl NodeBuilder { /// standard output. /// /// This option is currently unused in `rclrs`. - pub fn enable_rosout(mut self, enable: bool) -> Self { - self.enable_rosout = enable; - self + fn enable_rosout(self, enable: bool) -> NodeOptions<'a> { + let mut options = self.into_node_options(); + options.enable_rosout = enable; + options } /// Enables or disables parameter services. /// /// Parameter services can be used to allow external nodes to list, get and set /// parameters for this node. - pub fn start_parameter_services(mut self, start: bool) -> Self { - self.start_parameter_services = start; - self + fn start_parameter_services(self, start: bool) -> NodeOptions<'a> { + let mut options = self.into_node_options(); + options.start_parameter_services = start; + options } /// Sets the node's clock type. - pub fn clock_type(mut self, clock_type: ClockType) -> Self { - self.clock_type = clock_type; - self + fn clock_type(self, clock_type: ClockType) -> NodeOptions<'a> { + let mut options = self.into_node_options(); + options.clock_type = clock_type; + options } /// Sets the QoSProfile for the clock subscription. - pub fn clock_qos(mut self, clock_qos: QoSProfile) -> Self { - self.clock_qos = clock_qos; - self + fn clock_qos(self, clock_qos: QoSProfile) -> NodeOptions<'a> { + let mut options = self.into_node_options(); + options.clock_qos = clock_qos; + options } +} - /// Builds the node instance. +/// A set of options for creating a [`Node`][1]. +/// +/// The builder pattern, implemented through [`IntoNodeOptions`], allows +/// selectively setting some fields, and leaving all others at their default values. +/// +/// The default values for optional fields are: +/// - `namespace: "/"` +/// - `use_global_arguments: true` +/// - `arguments: []` +/// - `enable_rosout: true` +/// - `start_parameter_services: true` +/// - `clock_type: ClockType::RosTime` +/// - `clock_qos: QOS_PROFILE_CLOCK` +/// +/// # Example +/// ``` +/// # use rclrs::*; +/// let executor = Context::default().create_basic_executor(); +/// +/// // Building a node with default options +/// let node = executor.create_node("foo_node"); +/// +/// // Building a node with a namespace +/// let node = executor.create_node("bar_node".namespace("/bar"))?; +/// assert_eq!(node.name(), "bar_node"); +/// assert_eq!(node.namespace(), "/bar"); +/// +/// // Building a node with a namespace and no parameter services +/// let node = executor.create_node( +/// "baz" +/// .namespace("qux") +/// .start_parameter_services(false) +/// )?; +/// +/// // Building node options step-by-step +/// let mut options = NodeOptions::new("goose"); +/// options = options.namespace("/duck/duck"); +/// options = options.clock_type(ClockType::SteadyTime); +/// +/// let node = executor.create_node(options)?; +/// assert_eq!(node.fully_qualified_name(), "/duck/duck/goose"); +/// # Ok::<(), RclrsError>(()) +/// ``` +/// +/// [1]: crate::Node +pub struct NodeOptions<'a> { + name: &'a str, + namespace: &'a str, + use_global_arguments: bool, + arguments: Vec, + enable_rosout: bool, + start_parameter_services: bool, + clock_type: ClockType, + clock_qos: QoSProfile, +} + +impl<'a> NodeOptions<'a> { + /// Creates a builder for a node with the given name. /// - /// Node name and namespace validation is performed in this method. + /// See the [`Node` docs][1] for general information on node names. /// - /// For example usage, see the [`NodeBuilder`][1] docs. + /// # Rules for valid node names /// - /// [1]: crate::NodeBuilder - pub fn build(&self) -> Result, RclrsError> { - let node_name = - CString::new(self.name.as_str()).map_err(|err| RclrsError::StringContainsNul { - err, - s: self.name.clone(), - })?; + /// The rules for a valid node name are checked by the [`rmw_validate_node_name()`][2] + /// function. They are: + /// - Must contain only the `a-z`, `A-Z`, `0-9`, and `_` characters + /// - Must not be empty and not be longer than `RMW_NODE_NAME_MAX_NAME_LENGTH` + /// - Must not start with a number + /// + /// Note that node name validation is delayed until [`Executor::create_node`][3]. + /// + /// # Example + /// ``` + /// # use rclrs::*; + /// let executor = Context::default().create_basic_executor(); + /// // This is a valid node name + /// assert!(executor.create_node(NodeOptions::new("my_node")).is_ok()); + /// // This is another valid node name (although not a good one) + /// assert!(executor.create_node(NodeOptions::new("_______")).is_ok()); + /// // This is an invalid node name + /// assert!(matches!( + /// executor.create_node(NodeOptions::new("röböt")).unwrap_err(), + /// RclrsError::RclError { code: RclReturnCode::NodeInvalidName, .. } + /// )); + /// # Ok::<(), RclrsError>(()) + /// ``` + /// + /// [1]: crate::Node#naming + /// [2]: https://docs.ros2.org/latest/api/rmw/validate__node__name_8h.html#a5690a285aed9735f89ef11950b6e39e3 + /// [3]: crate::Executor::create_node + pub fn new(name: &'a str) -> NodeOptions<'a> { + NodeOptions { + name, + namespace: "/", + use_global_arguments: true, + arguments: vec![], + enable_rosout: true, + start_parameter_services: true, + clock_type: ClockType::RosTime, + clock_qos: QOS_PROFILE_CLOCK, + } + } + + /// Builds the node instance. + /// + /// Only used internally. Downstream users should call + /// [`Executor::create_node`]. + pub(crate) fn build(self, context: &Arc) -> Result, RclrsError> { + let node_name = CString::new(self.name).map_err(|err| RclrsError::StringContainsNul { + err, + s: self.name.to_owned(), + })?; let node_namespace = - CString::new(self.namespace.as_str()).map_err(|err| RclrsError::StringContainsNul { + CString::new(self.namespace).map_err(|err| RclrsError::StringContainsNul { err, - s: self.namespace.clone(), + s: self.namespace.to_owned(), })?; let rcl_node_options = self.create_rcl_node_options()?; - let rcl_context = &mut *self.context.rcl_context.lock().unwrap(); + let rcl_context = &mut *context.rcl_context.lock().unwrap(); let handle = Arc::new(NodeHandle { // SAFETY: Getting a zero-initialized value is always safe. rcl_node: Mutex::new(unsafe { rcl_get_zero_initialized_node() }), - context_handle: Arc::clone(&self.context), + context_handle: Arc::clone(context), initialized: AtomicBool::new(false), }); @@ -335,22 +356,23 @@ impl NodeBuilder { }; let node = Arc::new(Node { - handle, - clients_mtx: Mutex::new(vec![]), - guard_conditions_mtx: Mutex::new(vec![]), - services_mtx: Mutex::new(vec![]), - subscriptions_mtx: Mutex::new(vec![]), + clients_mtx: Mutex::default(), + guard_conditions_mtx: Mutex::default(), + services_mtx: Mutex::default(), + subscriptions_mtx: Mutex::default(), time_source: TimeSource::builder(self.clock_type) .clock_qos(self.clock_qos) .build(), parameter, logger: Logger::new(logger_name)?, + handle, }); - node.time_source.attach_node(&node); + if self.start_parameter_services { node.parameter.create_services(&node)?; } + Ok(node) } @@ -395,6 +417,24 @@ impl NodeBuilder { } } +impl<'a> IntoNodeOptions<'a> for NodeOptions<'a> { + fn into_node_options(self) -> NodeOptions<'a> { + self + } +} + +impl<'a, T: Borrow> IntoNodeOptions<'a> for &'a T { + fn into_node_options(self) -> NodeOptions<'a> { + NodeOptions::new(self.borrow()) + } +} + +impl<'a> IntoNodeOptions<'a> for &'a str { + fn into_node_options(self) -> NodeOptions<'a> { + NodeOptions::new(self) + } +} + impl Drop for rcl_node_options_t { fn drop(&mut self) { // SAFETY: Do not finish this struct except here. diff --git a/rclrs/src/parameter.rs b/rclrs/src/parameter.rs index 2a0829eac..87ff192dc 100644 --- a/rclrs/src/parameter.rs +++ b/rclrs/src/parameter.rs @@ -874,18 +874,25 @@ impl ParameterInterface { #[cfg(test)] mod tests { use super::*; - use crate::{create_node, Context}; + use crate::*; #[test] fn test_parameter_override_errors() { // Create a new node with a few parameter overrides - let ctx = Context::new([ - String::from("--ros-args"), - String::from("-p"), - String::from("declared_int:=10"), - ]) - .unwrap(); - let node = create_node(&ctx, &format!("param_test_node_{}", line!())).unwrap(); + let executor = Context::new( + [ + String::from("--ros-args"), + String::from("-p"), + String::from("declared_int:=10"), + ], + InitOptions::default(), + ) + .unwrap() + .create_basic_executor(); + + let node = executor + .create_node(&format!("param_test_node_{}", line!())) + .unwrap(); // Declaring a parameter with a different type than what was overridden should return an // error @@ -931,19 +938,26 @@ mod tests { #[test] fn test_parameter_setting_declaring() { // Create a new node with a few parameter overrides - let ctx = Context::new([ - String::from("--ros-args"), - String::from("-p"), - String::from("declared_int:=10"), - String::from("-p"), - String::from("double_array:=[1.0, 2.0]"), - String::from("-p"), - String::from("optional_bool:=true"), - String::from("-p"), - String::from("non_declared_string:='param'"), - ]) - .unwrap(); - let node = create_node(&ctx, &format!("param_test_node_{}", line!())).unwrap(); + let executor = Context::new( + [ + String::from("--ros-args"), + String::from("-p"), + String::from("declared_int:=10"), + String::from("-p"), + String::from("double_array:=[1.0, 2.0]"), + String::from("-p"), + String::from("optional_bool:=true"), + String::from("-p"), + String::from("non_declared_string:='param'"), + ], + InitOptions::default(), + ) + .unwrap() + .create_basic_executor(); + + let node = executor + .create_node(&format!("param_test_node_{}", line!())) + .unwrap(); let overridden_int = node .declare_parameter("declared_int") @@ -1087,13 +1101,20 @@ mod tests { #[test] fn test_override_undeclared_set_priority() { - let ctx = Context::new([ - String::from("--ros-args"), - String::from("-p"), - String::from("declared_int:=10"), - ]) - .unwrap(); - let node = create_node(&ctx, &format!("param_test_node_{}", line!())).unwrap(); + let executor = Context::new( + [ + String::from("--ros-args"), + String::from("-p"), + String::from("declared_int:=10"), + ], + InitOptions::default(), + ) + .unwrap() + .create_basic_executor(); + + let node = executor + .create_node(&format!("param_test_node_{}", line!())) + .unwrap(); // If a parameter was set as an override and as an undeclared parameter, the undeclared // value should get priority node.use_undeclared_parameters() @@ -1109,13 +1130,20 @@ mod tests { #[test] fn test_parameter_scope_redeclaring() { - let ctx = Context::new([ - String::from("--ros-args"), - String::from("-p"), - String::from("declared_int:=10"), - ]) - .unwrap(); - let node = create_node(&ctx, &format!("param_test_node_{}", line!())).unwrap(); + let executor = Context::new( + [ + String::from("--ros-args"), + String::from("-p"), + String::from("declared_int:=10"), + ], + InitOptions::default(), + ) + .unwrap() + .create_basic_executor(); + + let node = executor + .create_node(&format!("param_test_node_{}", line!())) + .unwrap(); { // Setting a parameter with an override let param = node @@ -1160,8 +1188,10 @@ mod tests { #[test] fn test_parameter_ranges() { - let ctx = Context::new([]).unwrap(); - let node = create_node(&ctx, &format!("param_test_node_{}", line!())).unwrap(); + let node = Context::default() + .create_basic_executor() + .create_node(&format!("param_test_node_{}", line!())) + .unwrap(); // Setting invalid ranges should fail let range = ParameterRange { lower: Some(10), @@ -1288,8 +1318,10 @@ mod tests { #[test] fn test_readonly_parameters() { - let ctx = Context::new([]).unwrap(); - let node = create_node(&ctx, &format!("param_test_node_{}", line!())).unwrap(); + let node = Context::default() + .create_basic_executor() + .create_node(&format!("param_test_node_{}", line!())) + .unwrap(); let param = node .declare_parameter("int_param") .default(100) @@ -1315,8 +1347,10 @@ mod tests { #[test] fn test_preexisting_value_error() { - let ctx = Context::new([]).unwrap(); - let node = create_node(&ctx, &format!("param_test_node_{}", line!())).unwrap(); + let node = Context::default() + .create_basic_executor() + .create_node(&format!("param_test_node_{}", line!())) + .unwrap(); node.use_undeclared_parameters() .set("int_param", 100) .unwrap(); @@ -1368,8 +1402,10 @@ mod tests { #[test] fn test_optional_parameter_apis() { - let ctx = Context::new([]).unwrap(); - let node = create_node(&ctx, &format!("param_test_node_{}", line!())).unwrap(); + let node = Context::default() + .create_basic_executor() + .create_node(&format!("param_test_node_{}", line!())) + .unwrap(); node.declare_parameter::("int_param") .optional() .unwrap(); diff --git a/rclrs/src/parameter/service.rs b/rclrs/src/parameter/service.rs index 7c8ffe62d..465275f7f 100644 --- a/rclrs/src/parameter/service.rs +++ b/rclrs/src/parameter/service.rs @@ -312,11 +312,13 @@ mod tests { }, srv::rmw::*, }, - Context, MandatoryParameter, Node, NodeBuilder, ParameterRange, ParameterValue, RclrsError, - ReadOnlyParameter, + *, }; use rosidl_runtime_rs::{seq, Sequence}; - use std::sync::{Arc, RwLock}; + use std::{ + sync::{Arc, RwLock}, + time::Duration, + }; struct TestNode { node: Arc, @@ -326,9 +328,9 @@ mod tests { dynamic_param: MandatoryParameter, } - async fn try_until_timeout(f: F) -> Result<(), ()> + async fn try_until_timeout(mut f: F) -> Result<(), ()> where - F: FnOnce() -> bool + Copy, + F: FnMut() -> bool, { let mut retry_count = 0; while !f() { @@ -341,10 +343,10 @@ mod tests { Ok(()) } - fn construct_test_nodes(context: &Context, ns: &str) -> (TestNode, Arc) { - let node = NodeBuilder::new(context, "node") - .namespace(ns) - .build() + fn construct_test_nodes(ns: &str) -> (Executor, TestNode, Arc) { + let executor = Context::default().create_basic_executor(); + let node = executor + .create_node(NodeOptions::new("node").namespace(ns)) .unwrap(); let range = ParameterRange { lower: Some(0), @@ -375,12 +377,12 @@ mod tests { .mandatory() .unwrap(); - let client = NodeBuilder::new(context, "client") - .namespace(ns) - .build() + let client = executor + .create_node(NodeOptions::new("client").namespace(ns)) .unwrap(); ( + executor, TestNode { node, bool_param, @@ -394,8 +396,7 @@ mod tests { #[test] fn test_parameter_services_names_and_types() -> Result<(), RclrsError> { - let context = Context::new([]).unwrap(); - let (node, _client) = construct_test_nodes(&context, "names_types"); + let (_, node, _client) = construct_test_nodes("names_types"); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -429,8 +430,7 @@ mod tests { #[tokio::test] async fn test_list_parameters_service() -> Result<(), RclrsError> { - let context = Context::new([]).unwrap(); - let (node, client) = construct_test_nodes(&context, "list"); + let (mut executor, _test, client) = construct_test_nodes("list"); let list_client = client.create_client::("/list/node/list_parameters")?; try_until_timeout(|| list_client.service_is_ready().unwrap()) @@ -441,9 +441,13 @@ mod tests { let inner_done = done.clone(); let rclrs_spin = tokio::task::spawn(async move { - try_until_timeout(|| { - crate::spin_once(node.node.clone(), Some(std::time::Duration::ZERO)).ok(); - crate::spin_once(client.clone(), Some(std::time::Duration::ZERO)).ok(); + try_until_timeout(move || { + executor + .spin(SpinOptions::spin_once().timeout(Duration::ZERO)) + .timeout_ok() + .first_error() + .unwrap(); + *inner_done.read().unwrap() }) .await @@ -542,7 +546,6 @@ mod tests { move |response: ListParameters_Response| { *call_done.write().unwrap() = true; let names = response.result.names; - dbg!(&names); assert_eq!(names.len(), 2); assert_eq!(names[0].to_string(), "bool"); assert_eq!(names[1].to_string(), "use_sim_time"); @@ -564,8 +567,7 @@ mod tests { #[tokio::test] async fn test_get_set_parameters_service() -> Result<(), RclrsError> { - let context = Context::new([]).unwrap(); - let (node, client) = construct_test_nodes(&context, "get_set"); + let (mut executor, test, client) = construct_test_nodes("get_set"); let get_client = client.create_client::("/get_set/node/get_parameters")?; let set_client = client.create_client::("/get_set/node/set_parameters")?; let set_atomically_client = client @@ -581,18 +583,24 @@ mod tests { let done = Arc::new(RwLock::new(false)); - let inner_node = node.node.clone(); let inner_done = done.clone(); let rclrs_spin = tokio::task::spawn(async move { - try_until_timeout(|| { - crate::spin_once(inner_node.clone(), Some(std::time::Duration::ZERO)).ok(); - crate::spin_once(client.clone(), Some(std::time::Duration::ZERO)).ok(); + try_until_timeout(move || { + executor + .spin(SpinOptions::spin_once().timeout(Duration::ZERO)) + .timeout_ok() + .first_error() + .unwrap(); + *inner_done.read().unwrap() }) .await .unwrap(); }); + let _hold_node = test.node.clone(); + let _hold_client = client.clone(); + let res = tokio::task::spawn(async move { // Get an existing parameter let request = GetParameters_Request { @@ -711,7 +719,7 @@ mod tests { let client_finished = Arc::new(RwLock::new(false)); let call_done = client_finished.clone(); // Parameter is assigned a default of true at declaration time - assert!(node.bool_param.get()); + assert!(test.bool_param.get()); set_client .async_send_request_with_callback( &request, @@ -721,14 +729,14 @@ mod tests { // Setting a bool value set for a bool parameter assert!(response.results[0].successful); // Value was set to false, node parameter get should reflect this - assert!(!node.bool_param.get()); + assert!(!test.bool_param.get()); // Setting a parameter to the wrong type assert!(!response.results[1].successful); // Setting a read only parameter assert!(!response.results[2].successful); // Setting a dynamic parameter to a new type assert!(response.results[3].successful); - assert_eq!(node.dynamic_param.get(), ParameterValue::Bool(true)); + assert_eq!(test.dynamic_param.get(), ParameterValue::Bool(true)); // Setting a value out of range assert!(!response.results[4].successful); // Setting an invalid type @@ -743,7 +751,7 @@ mod tests { .unwrap(); // Set the node to use undeclared parameters and try to set one - node.node.use_undeclared_parameters(); + test.node.use_undeclared_parameters(); let request = SetParameters_Request { parameters: seq![undeclared_bool], }; @@ -758,7 +766,7 @@ mod tests { // Setting the undeclared parameter is now allowed assert!(response.results[0].successful); assert_eq!( - node.node.use_undeclared_parameters().get("undeclared_bool"), + test.node.use_undeclared_parameters().get("undeclared_bool"), Some(ParameterValue::Bool(true)) ); }, @@ -797,8 +805,7 @@ mod tests { #[tokio::test] async fn test_describe_get_types_parameters_service() -> Result<(), RclrsError> { - let context = Context::new([]).unwrap(); - let (node, client) = construct_test_nodes(&context, "describe"); + let (mut executor, _test, client) = construct_test_nodes("describe"); let describe_client = client.create_client::("/describe/node/describe_parameters")?; let get_types_client = @@ -814,11 +821,14 @@ mod tests { let done = Arc::new(RwLock::new(false)); let inner_done = done.clone(); - let inner_node = node.node.clone(); let rclrs_spin = tokio::task::spawn(async move { - try_until_timeout(|| { - crate::spin_once(inner_node.clone(), Some(std::time::Duration::ZERO)).ok(); - crate::spin_once(client.clone(), Some(std::time::Duration::ZERO)).ok(); + try_until_timeout(move || { + executor + .spin(SpinOptions::spin_once().timeout(Duration::ZERO)) + .timeout_ok() + .first_error() + .unwrap(); + *inner_done.read().unwrap() }) .await diff --git a/rclrs/src/parameter/value.rs b/rclrs/src/parameter/value.rs index 82fe31ebb..ff0c86c46 100644 --- a/rclrs/src/parameter/value.rs +++ b/rclrs/src/parameter/value.rs @@ -537,7 +537,7 @@ impl ParameterValue { #[cfg(test)] mod tests { use super::*; - use crate::{Context, RclrsError, ToResult}; + use crate::{Context, InitOptions, RclrsError, ToResult}; // TODO(luca) tests for all from / to ParameterVariant functions @@ -565,11 +565,14 @@ mod tests { ), ]; for pair in input_output_pairs { - let ctx = Context::new([ - String::from("--ros-args"), - String::from("-p"), - format!("foo:={}", pair.0), - ])?; + let ctx = Context::new( + [ + String::from("--ros-args"), + String::from("-p"), + format!("foo:={}", pair.0), + ], + InitOptions::default(), + )?; let mut rcl_params = std::ptr::null_mut(); unsafe { rcl_arguments_get_param_overrides( diff --git a/rclrs/src/publisher.rs b/rclrs/src/publisher.rs index b1cdd93b9..6a6ad30d7 100644 --- a/rclrs/src/publisher.rs +++ b/rclrs/src/publisher.rs @@ -50,9 +50,9 @@ impl Drop for PublisherHandle { /// The underlying RMW will decide on the concrete delivery mechanism (network stack, shared /// memory, or intraprocess). /// -/// Sending messages does not require calling [`spin`][1] on the publisher's node. +/// Sending messages does not require the node's executor to [spin][2]. /// -/// [1]: crate::spin +/// [2]: crate::Executor::spin pub struct Publisher where T: Message, diff --git a/rclrs/src/subscription.rs b/rclrs/src/subscription.rs index fbd518c21..645f9019c 100644 --- a/rclrs/src/subscription.rs +++ b/rclrs/src/subscription.rs @@ -66,7 +66,7 @@ pub trait SubscriptionBase: Send + Sync { /// /// There can be multiple subscriptions for the same topic, in different nodes or the same node. /// -/// Receiving messages requires calling [`spin_once`][1] or [`spin`][2] on the subscription's node. +/// Receiving messages requires the node's executor to [spin][2]. /// /// When a subscription is created, it may take some time to get "matched" with a corresponding /// publisher. @@ -74,8 +74,7 @@ pub trait SubscriptionBase: Send + Sync { /// The only available way to instantiate subscriptions is via [`Node::create_subscription()`][3], this /// is to ensure that [`Node`][4]s can track all the subscriptions that have been created. /// -/// [1]: crate::spin_once -/// [2]: crate::spin +/// [2]: crate::Executor::spin /// [3]: crate::Node::create_subscription /// [4]: crate::Node pub struct Subscription diff --git a/rclrs/src/test_helpers/graph_helpers.rs b/rclrs/src/test_helpers/graph_helpers.rs index 1e9b581ae..838ef49f9 100644 --- a/rclrs/src/test_helpers/graph_helpers.rs +++ b/rclrs/src/test_helpers/graph_helpers.rs @@ -1,4 +1,4 @@ -use crate::{Context, Node, NodeBuilder, RclrsError}; +use crate::*; use std::sync::Arc; pub(crate) struct TestGraph { @@ -7,13 +7,9 @@ pub(crate) struct TestGraph { } pub(crate) fn construct_test_graph(namespace: &str) -> Result { - let context = Context::new([])?; + let executor = Context::default().create_basic_executor(); Ok(TestGraph { - node1: NodeBuilder::new(&context, "graph_test_node_1") - .namespace(namespace) - .build()?, - node2: NodeBuilder::new(&context, "graph_test_node_2") - .namespace(namespace) - .build()?, + node1: executor.create_node("graph_test_node_1".namespace(namespace))?, + node2: executor.create_node("graph_test_node_2".namespace(namespace))?, }) } diff --git a/rclrs/src/time_source.rs b/rclrs/src/time_source.rs index 0be0c07ec..1b5f16500 100644 --- a/rclrs/src/time_source.rs +++ b/rclrs/src/time_source.rs @@ -145,28 +145,34 @@ impl TimeSource { #[cfg(test)] mod tests { - use crate::{create_node, Context}; + use crate::*; #[test] fn time_source_default_clock() { - let node = create_node( - &Context::new([]).unwrap(), - &format!("time_source_test_node_{}", line!()), - ) - .unwrap(); + let node = Context::default() + .create_basic_executor() + .create_node(&format!("time_source_test_node_{}", line!())) + .unwrap(); // Default clock should be above 0 (use_sim_time is default false) assert!(node.get_clock().now().nsec > 0); } #[test] fn time_source_sim_time() { - let ctx = Context::new([ - String::from("--ros-args"), - String::from("-p"), - String::from("use_sim_time:=true"), - ]) - .unwrap(); - let node = create_node(&ctx, &format!("time_source_test_node_{}", line!())).unwrap(); + let executor = Context::new( + [ + String::from("--ros-args"), + String::from("-p"), + String::from("use_sim_time:=true"), + ], + InitOptions::default(), + ) + .unwrap() + .create_basic_executor(); + + let node = executor + .create_node(&format!("time_source_test_node_{}", line!())) + .unwrap(); // Default sim time value should be 0 (no message received) assert_eq!(node.get_clock().now().nsec, 0); } diff --git a/rclrs/src/wait.rs b/rclrs/src/wait.rs index 243c9d857..c0e0c659d 100644 --- a/rclrs/src/wait.rs +++ b/rclrs/src/wait.rs @@ -427,7 +427,7 @@ mod tests { #[test] fn guard_condition_in_wait_set_readies() -> Result<(), RclrsError> { - let context = Context::new([])?; + let context = Context::default(); let guard_condition = Arc::new(GuardCondition::new(&context)); diff --git a/rclrs/src/wait/guard_condition.rs b/rclrs/src/wait/guard_condition.rs index 92a6acd00..d9d8b62d9 100644 --- a/rclrs/src/wait/guard_condition.rs +++ b/rclrs/src/wait/guard_condition.rs @@ -16,7 +16,7 @@ use crate::{rcl_bindings::*, Context, ContextHandle, RclrsError, ToResult}; /// # use rclrs::{Context, GuardCondition, WaitSet, RclrsError}; /// # use std::sync::{Arc, atomic::Ordering}; /// -/// let context = Context::new([])?; +/// let context = Context::default(); /// /// let atomic_bool = Arc::new(std::sync::atomic::AtomicBool::new(false)); /// let atomic_bool_for_closure = Arc::clone(&atomic_bool); @@ -162,7 +162,7 @@ mod tests { #[test] fn test_guard_condition() -> Result<(), RclrsError> { - let context = Context::new([])?; + let context = Context::default(); let atomic_bool = Arc::new(std::sync::atomic::AtomicBool::new(false)); let atomic_bool_for_closure = Arc::clone(&atomic_bool); @@ -180,7 +180,7 @@ mod tests { #[test] fn test_guard_condition_wait() -> Result<(), RclrsError> { - let context = Context::new([])?; + let context = Context::default(); let atomic_bool = Arc::new(std::sync::atomic::AtomicBool::new(false)); let atomic_bool_for_closure = Arc::clone(&atomic_bool);