Skip to content

Commit

Permalink
Service client generics
Browse files Browse the repository at this point in the history
  • Loading branch information
carter committed Aug 26, 2024
1 parent aa862a7 commit 36d4b99
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 6 deletions.
94 changes: 94 additions & 0 deletions roslibrust/examples/generic_client_services.rs
Original file line number Diff line number Diff line change
@@ -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<T: ServiceProvider + 'static> {
ros: T,
}

// Basic example of a node that publishes and subscribes to itself
impl<T: ServiceProvider> MyNode<T> {
fn handle_service(
_request: std_srvs::SetBoolRequest,
) -> Result<std_srvs::SetBoolResponse, Box<dyn std::error::Error + Send + Sync>> {
// Not actually doing anything here just example
// Note: if we did want to set a bool, we'd probably want to use Arc<Mutex<bool>>
Ok(std_srvs::SetBoolResponse {
success: true,
message: "You set my bool!".to_string(),
})
}

async fn run(self) {
let _handle = self
.ros
.advertise_service::<std_srvs::SetBool, _>("/my_set_bool", Self::handle_service)
.await
.unwrap();

let client = self
.ros
.service_client::<std_srvs::SetBool>("/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() {}
19 changes: 17 additions & 2 deletions roslibrust/src/rosbridge/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<T>(&self, topic: &str) -> RosLibRustResult<ServiceClient<T>>
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) {
Expand Down
20 changes: 20 additions & 0 deletions roslibrust/src/rosbridge/mod.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<T> {
_marker: std::marker::PhantomData<T>,
// Need a client handle so we can fwd to call_service
client: ClientHandle,
// Need the topic we are calling on
topic: String,
}

impl<T: RosServiceType> ServiceClient<T> {
pub async fn call(&self, request: T::Request) -> RosLibRustResult<T::Response> {
self.client
.call_service::<T>(self.topic.as_str(), request)
.await
}
}

/// Our underlying communication socket type (maybe move to comm?)
type Socket = tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<TcpStream>>;

Expand Down
97 changes: 93 additions & 4 deletions roslibrust/src/topic_provider.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -72,25 +72,25 @@ impl<T: RosMessageType> Subscribe<T> for crate::ros1::Subscriber<T> {
/// 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<T: RosMessageType>: Publish<T> + Send + 'static;
type Subscriber<T: RosMessageType>: Subscribe<T> + 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<T: RosMessageType>(
&self,
topic: &str,
) -> impl futures::Future<Output = RosLibRustResult<Self::Publisher<T>>> + Send;

/// 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<T: RosMessageType>(
&self,
topic: &str,
Expand Down Expand Up @@ -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<T: RosServiceType> {
fn call(
&self,
request: &T::Request,
) -> impl futures::Future<Output = RosLibRustResult<T::Response>> + Send;
}

impl<T: RosServiceType> Service<T> for crate::ServiceClient<T> {
async fn call(&self, request: &T::Request) -> RosLibRustResult<T::Response> {
// TODO sort out the reference vs clone stuff here
ServiceClient::call(&self, request.clone()).await
}
}

impl<T: RosServiceType> Service<T> for crate::ros1::ServiceClient<T> {
async fn call(&self, request: &T::Request) -> RosLibRustResult<T::Response> {
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<T: RosServiceType>: Service<T> + Send + 'static;
type ServiceServer;

fn service_client<T: RosServiceType + 'static>(
&self,
topic: &str,
) -> impl futures::Future<Output = RosLibRustResult<Self::ServiceClient<T>>> + Send;

fn advertise_service<T: RosServiceType + 'static, F>(
&self,
topic: &str,
server: F,
) -> impl futures::Future<Output = RosLibRustResult<Self::ServiceServer>> + Send
where
F: ServiceFn<T>;
}

impl ServiceProvider for crate::ClientHandle {
type ServiceClient<T: RosServiceType> = crate::ServiceClient<T>;
type ServiceServer = crate::ServiceHandle;

async fn service_client<T: RosServiceType + 'static>(
&self,
topic: &str,
) -> RosLibRustResult<Self::ServiceClient<T>> {
self.service_client::<T>(topic).await
}

fn advertise_service<T: RosServiceType + 'static, F>(
&self,
topic: &str,
server: F,
) -> impl futures::Future<Output = RosLibRustResult<Self::ServiceServer>> + Send
where
F: ServiceFn<T>,
{
self.advertise_service(topic, server)
}
}

impl ServiceProvider for crate::ros1::NodeHandle {
type ServiceClient<T: RosServiceType> = crate::ros1::ServiceClient<T>;
type ServiceServer = crate::ros1::ServiceServer;

async fn service_client<T: RosServiceType + 'static>(
&self,
topic: &str,
) -> RosLibRustResult<Self::ServiceClient<T>> {
// TODO bad error mapping here...
self.service_client::<T>(topic).await.map_err(|e| e.into())
}

async fn advertise_service<T: RosServiceType + 'static, F>(
&self,
topic: &str,
server: F,
) -> RosLibRustResult<Self::ServiceServer>
where
F: ServiceFn<T>,
{
self.advertise_service::<T, F>(topic, server)
.await
.map_err(|e| e.into())
}
}

#[cfg(test)]
mod test {
use super::TopicProvider;
Expand Down

0 comments on commit 36d4b99

Please sign in to comment.