From 239ab369c30879ff0247726f4db1e89b9d2391cb Mon Sep 17 00:00:00 2001 From: carter Date: Thu, 9 Jan 2025 11:03:26 -0700 Subject: [PATCH 1/2] Add ros trait --- CHANGELOG.md | 2 + roslibrust_common/src/lib.rs | 4 +- .../src/{topic_provider.rs => traits.rs} | 13 +++++++ roslibrust_mock/src/lib.rs | 39 +++++++++++++++++-- roslibrust_ros1/src/lib.rs | 21 +++++++++- roslibrust_rosbridge/src/lib.rs | 17 ++++++++ roslibrust_zenoh/src/lib.rs | 17 ++++++++ 7 files changed, 106 insertions(+), 7 deletions(-) rename roslibrust_common/src/{topic_provider.rs => traits.rs} (87%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2633a61..e3fee38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- A new `Ros` trait has been added that all backends implement. This trait behaves like a typical ROS1 node handle, and is easier to work with in generics than directly using TopicProvider or ServiceProvider. + ### Fixed ### Changed diff --git a/roslibrust_common/src/lib.rs b/roslibrust_common/src/lib.rs index 8b8d2f9..ed33a39 100644 --- a/roslibrust_common/src/lib.rs +++ b/roslibrust_common/src/lib.rs @@ -131,5 +131,5 @@ pub mod md5sum; /// Contains the generic traits represent a pubsub system and service system /// These traits will be implemented for specific backends to provides access to "ROS Like" functionality -pub mod topic_provider; -pub use topic_provider::*; // Bring topic provider traits into root namespace +pub mod traits; +pub use traits::*; // Bring topic provider traits into root namespace diff --git a/roslibrust_common/src/topic_provider.rs b/roslibrust_common/src/traits.rs similarity index 87% rename from roslibrust_common/src/topic_provider.rs rename to roslibrust_common/src/traits.rs index add41e8..bf75ec4 100644 --- a/roslibrust_common/src/topic_provider.rs +++ b/roslibrust_common/src/traits.rs @@ -90,6 +90,19 @@ pub trait ServiceProvider { F: ServiceFn; } +/// Represents all "standard" ROS functionality generically supported by roslibrust +/// +/// Implementors of this trait behave like typical ROS1 node handles. +/// Cloning the handle does not create additional underlying connections, but instead simply returns another handle +/// to interact with the underlying node. +/// +/// Implementors of this trait are expected to be "self de-registering", when the last node handle for a given +/// node is dropped, the underlying node is expected to be shut down and clean-up after itself +pub trait Ros: 'static + Send + Sync + TopicProvider + ServiceProvider + Clone {} + +/// The Ros trait is auto implemented for any type that implements the required traits +impl Ros for T {} + #[cfg(test)] mod test { // This test specifically fails because TopicProvider is not object safe diff --git a/roslibrust_mock/src/lib.rs b/roslibrust_mock/src/lib.rs index dd1511e..35fd34f 100644 --- a/roslibrust_mock/src/lib.rs +++ b/roslibrust_mock/src/lib.rs @@ -46,18 +46,19 @@ type TypeErasedCallback = Arc< /// A mock ROS implementation that can be substituted for any roslibrust backend in unit tests. /// /// Implements [TopicProvider] and [ServiceProvider] to provide basic ros functionality. +#[derive(Clone)] pub struct MockRos { // We could probably achieve some fancier type erasure than actually serializing the data // but this ends up being pretty simple - topics: RwLock>, Channel::Receiver>)>>, - services: RwLock>, + topics: Arc>, Channel::Receiver>)>>>, + services: Arc>>, } impl MockRos { pub fn new() -> Self { Self { - topics: RwLock::new(BTreeMap::new()), - services: RwLock::new(BTreeMap::new()), + topics: Arc::new(RwLock::new(BTreeMap::new())), + services: Arc::new(RwLock::new(BTreeMap::new())), } } } @@ -289,4 +290,34 @@ mod tests { assert_eq!(response.success, true); assert_eq!(response.message, "You set my bool!"); } + + #[tokio::test(flavor = "multi_thread")] + async fn test_mock_node() { + // Proves that MockRos impls the Ros trait (via auto impl in roslibrust_common) + // and can be used as such + struct MyNode { + ros: T, + } + + impl MyNode { + async fn run(self) { + let publisher = self + .ros + .advertise::("/chatter") + .await + .unwrap(); + + publisher + .publish(&std_msgs::String { + data: "Hello, world!".to_string(), + }) + .await + .unwrap(); + } + } + + let mock_ros = MockRos::new(); + let node = MyNode { ros: mock_ros }; + node.run().await; + } } diff --git a/roslibrust_ros1/src/lib.rs b/roslibrust_ros1/src/lib.rs index 3cef6dc..8d90bd4 100644 --- a/roslibrust_ros1/src/lib.rs +++ b/roslibrust_ros1/src/lib.rs @@ -64,7 +64,7 @@ pub(crate) type TypeErasedCallback = dyn Fn(Vec) -> Result, Box = crate::Publisher; type Subscriber = crate::Subscriber; @@ -163,6 +163,7 @@ impl Publish for Publisher { #[cfg(test)] mod test { + use roslibrust_common::Ros; use roslibrust_common::TopicProvider; // Prove that we've implemented the topic provider trait fully for NodeHandle @@ -178,7 +179,25 @@ mod test { let new_mock: Result = Err(anyhow::anyhow!("Expected error")); let _x = MyClient { + // Will panic here which is expect, this test just needs to compile to prove + // NodeHandle implements TopicProvider _client: new_mock.unwrap(), // panic }; } + + #[test] + #[should_panic] + fn confirm_node_handle_impls_ros() { + struct MyClient { + _client: T, + } + + let new_mock: Result = Err(anyhow::anyhow!("Expected error")); + + let _x = MyClient { + // Will panic here which is expect, this test just needs to compile to prove + // NodeHandle implements Ros + _client: new_mock.unwrap(), + }; + } } diff --git a/roslibrust_rosbridge/src/lib.rs b/roslibrust_rosbridge/src/lib.rs index 6dda9f4..d8ce3ec 100644 --- a/roslibrust_rosbridge/src/lib.rs +++ b/roslibrust_rosbridge/src/lib.rs @@ -263,4 +263,21 @@ mod test { _client: new_mock.unwrap(), // panic }; } + + #[test] + #[should_panic] + fn confirm_client_handle_impls_ros() { + struct MyClient { + _client: T, + } + + let new_mock: std::result::Result = + Err(anyhow::anyhow!("Expected error")); + + let _x = MyClient { + // Should panic here, but proves that ClientHandle implements Ros + // when this test compiles + _client: new_mock.unwrap(), + }; + } } diff --git a/roslibrust_zenoh/src/lib.rs b/roslibrust_zenoh/src/lib.rs index 4607220..6e2d640 100644 --- a/roslibrust_zenoh/src/lib.rs +++ b/roslibrust_zenoh/src/lib.rs @@ -8,6 +8,7 @@ use zenoh::bytes::ZBytes; /// A wrapper around a normal zenoh session that adds roslibrust specific functionality. /// Should be created via [ZenohClient::new], and then used via the [TopicProvider] and [ServiceProvider] traits. +#[derive(Clone)] pub struct ZenohClient { session: zenoh::Session, } @@ -386,4 +387,20 @@ mod tests { ), "7374645f737276732f536574426f6f6c/09fb03525b03e7ea1fd3992bafd87e16/service_server_rs/my_set_bool"); } + + #[test] + #[should_panic] + fn confirm_client_handle_impls_ros() { + struct MyClient { + _client: T, + } + + let new_mock: std::result::Result = Err(anyhow::anyhow!("Expected error")); + + let _x = MyClient { + // Should panic here, but proves that ClientHandle implements Ros + // when this test compiles + _client: new_mock.unwrap(), + }; + } } From dd2124502145b264e86525343faf5d59bc3a06e8 Mon Sep 17 00:00:00 2001 From: carter Date: Thu, 9 Jan 2025 11:07:55 -0700 Subject: [PATCH 2/2] Switch generic examples to being generic over Ros trait --- example_package/src/main.rs | 2 +- example_package_macro/src/main.rs | 2 +- roslibrust/examples/generic_client.rs | 6 +++--- roslibrust/examples/generic_client_services.rs | 6 +++--- roslibrust/examples/generic_message.rs | 1 + 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/example_package/src/main.rs b/example_package/src/main.rs index 438b43b..e3279ef 100644 --- a/example_package/src/main.rs +++ b/example_package/src/main.rs @@ -11,7 +11,7 @@ use roslibrust::Publish; // Writing a simple behavior that uses the generic traits from roslibrust // and the generated types from the macro above. -async fn pub_counter(ros: impl roslibrust::TopicProvider) { +async fn pub_counter(ros: impl roslibrust::Ros) { let publisher = ros .advertise::("example_counter") .await diff --git a/example_package_macro/src/main.rs b/example_package_macro/src/main.rs index 4c8440b..13c7b4d 100644 --- a/example_package_macro/src/main.rs +++ b/example_package_macro/src/main.rs @@ -12,7 +12,7 @@ roslibrust::find_and_generate_ros_messages!( // Writing a simple behavior that uses the generic traits from roslibrust // and the generated types from the macro above. -async fn pub_counter(ros: impl roslibrust::TopicProvider) { +async fn pub_counter(ros: impl roslibrust::Ros) { let publisher = ros .advertise::("example_counter") .await diff --git a/roslibrust/examples/generic_client.rs b/roslibrust/examples/generic_client.rs index f86c334..59a74b3 100644 --- a/roslibrust/examples/generic_client.rs +++ b/roslibrust/examples/generic_client.rs @@ -9,7 +9,7 @@ roslibrust_codegen_macro::find_and_generate_ros_messages!("assets/ros1_common_in #[cfg(all(feature = "rosbridge", feature = "ros1"))] #[tokio::main] async fn main() { - use roslibrust::{Publish, Subscribe, TopicProvider}; + use roslibrust::{Publish, Ros, Subscribe}; env_logger::init(); // TopicProvider cannot be an "Object Safe Trait" due to its generic parameters @@ -21,13 +21,13 @@ async fn main() { // single application. The critical part is to make TopicProvider a // generic type on you Node. - struct MyNode { + struct MyNode { ros: T, name: String, } // Basic example of a node that publishes and subscribes to itself - impl MyNode { + impl MyNode { async fn run(self) { let publisher = self .ros diff --git a/roslibrust/examples/generic_client_services.rs b/roslibrust/examples/generic_client_services.rs index c459c9f..edbeae6 100644 --- a/roslibrust/examples/generic_client_services.rs +++ b/roslibrust/examples/generic_client_services.rs @@ -8,7 +8,7 @@ roslibrust_codegen_macro::find_and_generate_ros_messages!("assets/ros1_common_in #[tokio::main] async fn main() { // Important to bring these traits into scope so we can use them - use roslibrust::{Service, ServiceProvider}; + use roslibrust::{Ros, Service}; env_logger::init(); // TopicProvider cannot be an "Object Safe Trait" due to its generic parameters @@ -20,13 +20,13 @@ async fn main() { // single application. The critical part is to make TopicProvider a // generic type on you Node. - struct MyNode { + struct MyNode { ros: T, } // Basic example of a node that publishes and subscribes to itself // This node will work with any backend - impl MyNode { + impl MyNode { fn handle_service( _request: std_srvs::SetBoolRequest, ) -> Result> { diff --git a/roslibrust/examples/generic_message.rs b/roslibrust/examples/generic_message.rs index 612dbf5..e117104 100644 --- a/roslibrust/examples/generic_message.rs +++ b/roslibrust/examples/generic_message.rs @@ -38,6 +38,7 @@ impl RosMessageType for GenericHeader { } /// Sets up a subscriber that could get either of two versions of a message +/// Note this is currently only supported by the rosbridge backend as this behavior relies on serde_json's fallback capabilities #[cfg(feature = "rosbridge")] #[tokio::main] async fn main() -> Result<(), Box> {