diff --git a/roslibrust/examples/generic_client_services.rs b/roslibrust/examples/generic_client_services.rs new file mode 100644 index 0000000..5460a1e --- /dev/null +++ b/roslibrust/examples/generic_client_services.rs @@ -0,0 +1,94 @@ +//! Purpose of this example is to show how the ServiceProvider trait can be use +//! to create code that is generic of which communication backend it will use. + +#[cfg(feature = "topic_provider")] +#[tokio::main] +async fn main() { + simple_logger::SimpleLogger::new() + .with_level(log::LevelFilter::Debug) + .without_timestamps() // required for running wsl2 + .init() + .unwrap(); + + use roslibrust::topic_provider::*; + + roslibrust_codegen_macro::find_and_generate_ros_messages!( + "assets/ros1_common_interfaces/ros_comm_msgs/std_srvs" + ); + // TopicProvider cannot be an "Object Safe Trait" due to its generic parameters + // This means we can't do: + + // Which specific TopicProvider you are going to use must be known at + // compile time! We can use features to build multiple copies of our + // executable with different backends. Or mix and match within a + // single application. The critical part is to make TopicProvider a + // generic type on you Node. + + struct MyNode { + ros: T, + } + + // Basic example of a node that publishes and subscribes to itself + impl MyNode { + fn handle_service( + _request: std_srvs::SetBoolRequest, + ) -> Result> { + // Not actually doing anything here just example + // Note: if we did want to set a bool, we'd probably want to use Arc> + Ok(std_srvs::SetBoolResponse { + success: true, + message: "You set my bool!".to_string(), + }) + } + + async fn run(self) { + let _handle = self + .ros + .advertise_service::("/my_set_bool", Self::handle_service) + .await + .unwrap(); + + let client = self + .ros + .service_client::("/my_set_bool") + .await + .unwrap(); + + loop { + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + println!("sleeping"); + + client + .call(&std_srvs::SetBoolRequest { data: true }) + .await + .unwrap(); + } + } + } + + // create a rosbridge handle and start node + let ros = roslibrust::ClientHandle::new("ws://localhost:9090") + .await + .unwrap(); + let node = MyNode { ros }; + tokio::spawn(async move { node.run().await }); + + // create a ros1 handle and start node + let ros = roslibrust::ros1::NodeHandle::new("http://localhost:11311", "/my_node") + .await + .unwrap(); + let node = MyNode { ros }; + tokio::spawn(async move { node.run().await }); + + loop { + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + println!("sleeping"); + } + + // With this executable running + // RUST_LOG=debug cargo run --features ros1,topic_provider --example generic_client_services + // You should see log output from both nodes +} + +#[cfg(not(feature = "topic_provider"))] +fn main() {} diff --git a/roslibrust/src/rosbridge/client.rs b/roslibrust/src/rosbridge/client.rs index fe1b59b..337534c 100644 --- a/roslibrust/src/rosbridge/client.rs +++ b/roslibrust/src/rosbridge/client.rs @@ -17,8 +17,8 @@ use tokio::time::Duration; use tokio_tungstenite::tungstenite::Message; use super::{ - MessageQueue, PublisherHandle, Reader, ServiceCallback, Socket, Subscription, Writer, - QUEUE_SIZE, + MessageQueue, PublisherHandle, Reader, ServiceCallback, ServiceClient, Socket, Subscription, + Writer, QUEUE_SIZE, }; /// Builder options for creating a client @@ -467,6 +467,21 @@ impl ClientHandle { }) } + /// Creates a service client that can be used to repeatedly call a service. + /// + /// Note: Unlike with ROS1 native service, this provides no performance benefit over call_service, + /// and is just a thin wrapper around call_service. + pub async fn service_client(&self, topic: &str) -> RosLibRustResult> + where + T: RosServiceType, + { + Ok(ServiceClient { + _marker: Default::default(), + client: self.clone(), + topic: topic.to_string(), + }) + } + // Internal method for removing a service, this is expected to be automatically called // by dropping the relevant service handle. Intentionally not async as a result. pub(crate) fn unadvertise_service(&self, topic: &str) { diff --git a/roslibrust/src/rosbridge/mod.rs b/roslibrust/src/rosbridge/mod.rs index a7948b7..c6ad3a5 100644 --- a/roslibrust/src/rosbridge/mod.rs +++ b/roslibrust/src/rosbridge/mod.rs @@ -1,6 +1,7 @@ // Subscriber is a transparent module, we directly expose internal types // Module exists only to organize source code. mod subscriber; +use roslibrust_codegen::RosServiceType; pub use subscriber::*; // Publisher is a transparent module, we directly expose internal types @@ -70,6 +71,25 @@ impl Drop for ServiceHandle { } } +/// Rosbridge doesn't have the same concept of service client that ros1 native has +/// This type is used to replicate the api of ros1::ServiceClient, but really it +/// just a thin wrapper around call_service() and has no performance benefits +pub struct ServiceClient { + _marker: std::marker::PhantomData, + // Need a client handle so we can fwd to call_service + client: ClientHandle, + // Need the topic we are calling on + topic: String, +} + +impl ServiceClient { + pub async fn call(&self, request: T::Request) -> RosLibRustResult { + self.client + .call_service::(self.topic.as_str(), request) + .await + } +} + /// Our underlying communication socket type (maybe move to comm?) type Socket = tokio_tungstenite::WebSocketStream>; diff --git a/roslibrust/src/topic_provider.rs b/roslibrust/src/topic_provider.rs index d32fc43..9f954eb 100644 --- a/roslibrust/src/topic_provider.rs +++ b/roslibrust/src/topic_provider.rs @@ -1,6 +1,6 @@ use roslibrust_codegen::{RosMessageType, RosServiceType}; -use crate::{RosLibRustResult, ServiceFn}; +use crate::{RosLibRustResult, ServiceClient, ServiceFn}; /// Indicates that something is a publisher and has our expected publish /// Implementors of this trait are expected to auto-cleanup the publisher when dropped @@ -72,17 +72,17 @@ impl Subscribe for crate::ros1::Subscriber { /// Fundamentally, it assumes that topics are uniquely identified by a string name (likely an ASCII assumption is buried in here...). /// It assumes topics only carry one data type, but is not expected to enforce that. /// It assumes that all actions can fail due to a variety of causes, and by network interruption specifically. -// #[async_trait] pub trait TopicProvider { // These associated types makeup the other half of the API // They are expected to be "self-deregistering", where dropping them results in unadvertise or unsubscribe operations as appropriate + // We require Publisher and Subscriber types to be Send + 'static so they can be sent into different tokio tasks once created type Publisher: Publish + Send + 'static; type Subscriber: Subscribe + Send + 'static; type ServiceHandle; /// Advertises a topic to be published to and returns a type specific publisher to use. /// - /// The returned publisher is expected to be "self-deregistering", where dropping the publisher results in the appropriate unadvertise operation. + /// The returned publisher is expected to be "self de-registering", where dropping the publisher results in the appropriate unadvertise operation. fn advertise( &self, topic: &str, @@ -90,7 +90,7 @@ pub trait TopicProvider { /// Subscribes to a topic and returns a type specific subscriber to use. /// - /// The returned subscriber is expected to be "self-deregistering", where dropping the subscriber results in the appropriate unsubscribe operation. + /// The returned subscriber is expected to be "self de-registering", where dropping the subscriber results in the appropriate unsubscribe operation. fn subscribe( &self, topic: &str, @@ -198,6 +198,95 @@ impl TopicProvider for crate::ros1::NodeHandle { } } +/// Defines what it means to be something that is callable as a service +pub trait Service { + fn call( + &self, + request: &T::Request, + ) -> impl futures::Future> + Send; +} + +impl Service for crate::ServiceClient { + async fn call(&self, request: &T::Request) -> RosLibRustResult { + // TODO sort out the reference vs clone stuff here + ServiceClient::call(&self, request.clone()).await + } +} + +impl Service for crate::ros1::ServiceClient { + async fn call(&self, request: &T::Request) -> RosLibRustResult { + self.call(request).await + } +} + +/// This trait is analogous to TopicProvider, but instead provides the capability to create service servers and service clients +pub trait ServiceProvider { + type ServiceClient: Service + Send + 'static; + type ServiceServer; + + fn service_client( + &self, + topic: &str, + ) -> impl futures::Future>> + Send; + + fn advertise_service( + &self, + topic: &str, + server: F, + ) -> impl futures::Future> + Send + where + F: ServiceFn; +} + +impl ServiceProvider for crate::ClientHandle { + type ServiceClient = crate::ServiceClient; + type ServiceServer = crate::ServiceHandle; + + async fn service_client( + &self, + topic: &str, + ) -> RosLibRustResult> { + self.service_client::(topic).await + } + + fn advertise_service( + &self, + topic: &str, + server: F, + ) -> impl futures::Future> + Send + where + F: ServiceFn, + { + self.advertise_service(topic, server) + } +} + +impl ServiceProvider for crate::ros1::NodeHandle { + type ServiceClient = crate::ros1::ServiceClient; + type ServiceServer = crate::ros1::ServiceServer; + + async fn service_client( + &self, + topic: &str, + ) -> RosLibRustResult> { + // TODO bad error mapping here... + self.service_client::(topic).await.map_err(|e| e.into()) + } + + async fn advertise_service( + &self, + topic: &str, + server: F, + ) -> RosLibRustResult + where + F: ServiceFn, + { + self.advertise_service::(topic, server) + .await + .map_err(|e| e.into()) + } +} + #[cfg(test)] mod test { use super::TopicProvider;