Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Node generics #221

Merged
merged 2 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion example_package/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<std_msgs::Int16>("example_counter")
.await
Expand Down
2 changes: 1 addition & 1 deletion example_package_macro/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<std_msgs::Int16>("example_counter")
.await
Expand Down
6 changes: 3 additions & 3 deletions roslibrust/examples/generic_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,13 +21,13 @@ async fn main() {
// single application. The critical part is to make TopicProvider a
// generic type on you Node.

struct MyNode<T: TopicProvider> {
struct MyNode<T: Ros> {
ros: T,
name: String,
}

// Basic example of a node that publishes and subscribes to itself
impl<T: TopicProvider> MyNode<T> {
impl<T: Ros> MyNode<T> {
async fn run(self) {
let publisher = self
.ros
Expand Down
6 changes: 3 additions & 3 deletions roslibrust/examples/generic_client_services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,13 +20,13 @@ async fn main() {
// single application. The critical part is to make TopicProvider a
// generic type on you Node.

struct MyNode<T: ServiceProvider + 'static> {
struct MyNode<T: Ros> {
ros: T,
}

// Basic example of a node that publishes and subscribes to itself
// This node will work with any backend
impl<T: ServiceProvider> MyNode<T> {
impl<T: Ros> MyNode<T> {
fn handle_service(
_request: std_srvs::SetBoolRequest,
) -> Result<std_srvs::SetBoolResponse, Box<dyn std::error::Error + Send + Sync>> {
Expand Down
1 change: 1 addition & 0 deletions roslibrust/examples/generic_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error>> {
Expand Down
4 changes: 2 additions & 2 deletions roslibrust_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ pub trait ServiceProvider {
F: ServiceFn<T>;
}

/// 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<T: 'static + Send + Sync + TopicProvider + ServiceProvider + Clone> Ros for T {}

#[cfg(test)]
mod test {
// This test specifically fails because TopicProvider is not object safe
Expand Down
39 changes: 35 additions & 4 deletions roslibrust_mock/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BTreeMap<String, (Channel::Sender<Vec<u8>>, Channel::Receiver<Vec<u8>>)>>,
services: RwLock<BTreeMap<String, TypeErasedCallback>>,
topics: Arc<RwLock<BTreeMap<String, (Channel::Sender<Vec<u8>>, Channel::Receiver<Vec<u8>>)>>>,
services: Arc<RwLock<BTreeMap<String, TypeErasedCallback>>>,
}

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())),
}
}
}
Expand Down Expand Up @@ -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<T: Ros> {
ros: T,
}

impl<T: Ros> MyNode<T> {
async fn run(self) {
let publisher = self
.ros
.advertise::<std_msgs::String>("/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;
}
}
21 changes: 20 additions & 1 deletion roslibrust_ros1/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub(crate) type TypeErasedCallback = dyn Fn(Vec<u8>) -> Result<Vec<u8>, Box<dyn
+ 'static;

// Implement the generic roslibrust trait
impl TopicProvider for NodeHandle {
impl TopicProvider for crate::NodeHandle {
type Publisher<T: RosMessageType> = crate::Publisher<T>;
type Subscriber<T: RosMessageType> = crate::Subscriber<T>;

Expand Down Expand Up @@ -163,6 +163,7 @@ impl<T: RosMessageType> Publish<T> for Publisher<T> {

#[cfg(test)]
mod test {
use roslibrust_common::Ros;
use roslibrust_common::TopicProvider;

// Prove that we've implemented the topic provider trait fully for NodeHandle
Expand All @@ -178,7 +179,25 @@ mod test {
let new_mock: Result<crate::NodeHandle, _> = 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<T: Ros> {
_client: T,
}

let new_mock: Result<crate::NodeHandle, _> = 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(),
};
}
}
17 changes: 17 additions & 0 deletions roslibrust_rosbridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,21 @@ mod test {
_client: new_mock.unwrap(), // panic
};
}

#[test]
#[should_panic]
fn confirm_client_handle_impls_ros() {
struct MyClient<T: Ros> {
_client: T,
}

let new_mock: std::result::Result<crate::ClientHandle, _> =
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(),
};
}
}
17 changes: 17 additions & 0 deletions roslibrust_zenoh/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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<T: Ros> {
_client: T,
}

let new_mock: std::result::Result<ZenohClient, _> = 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(),
};
}
}
Loading