roslibrust_common 0.20.0

Common types and traits used throughout the roslibrust ecosystem.
Documentation
use crate::topic_name::*;
use crate::{Result, ServiceError};
use std::future::Future;

/// Fundamental traits for message types this crate works with
/// This trait will be satisfied for any types generated with this crate's message_gen functionality
pub trait RosMessageType:
    'static + serde::de::DeserializeOwned + Send + serde::Serialize + Sync + Clone + std::fmt::Debug
{
    /// Expected to be the combination pkg_name/type_name string describing the type to ros
    /// Example: std_msgs/Header
    const ROS_TYPE_NAME: &'static str;
    /// The computed md5sum of the message file and its dependencies
    /// This field is optional, and only needed when using ros1 native communication
    const MD5SUM: &'static str = "";
    /// The definition from the msg, srv, or action file
    /// This field is optional, and only needed when using ros1 native communication
    const DEFINITION: &'static str = "";
    /// The fully qualified type name we need to work with ROS2 zenoh
    /// e.g. std_msgs::msg::dds_::String_
    /// This field is optional, and only needed when using ros2 native communication
    const ROS2_TYPE_NAME: &'static str = "";
    /// The computed ROS2 hash of the message file and its dependencies
    /// This field is optional, and only needed when using ros2 native communication
    const ROS2_HASH: &'static [u8; 32] = &[0; 32];
}

// This special impl allows for services with no args / returns
impl RosMessageType for () {
    const ROS_TYPE_NAME: &'static str = "";
    const MD5SUM: &'static str = "";
    const DEFINITION: &'static str = "";
}

/// Represents a ROS service type definition corresponding to a `.srv` file.
///
/// Typically this trait will not be implemented by hand but instead be generated by using [roslibrust's codegen functionality](https://docs.rs/roslibrust/latest/roslibrust/codegen).
/// This trait is used by the [ServiceProvider] trait to define types that can be used with [ServiceProvider::call_service] and [ServiceProvider::advertise_service]
pub trait RosServiceType: 'static + Send + Sync {
    /// Name of the ros service e.g. `rospy_tutorials/AddTwoInts` in ROS1
    const ROS_SERVICE_NAME: &'static str;
    /// The computed md5sum of the message file and its dependencies
    /// This field is optional, and only needed when using ros1 native communication
    const MD5SUM: &'static str = "";
    /// The computed ROS2 hash of the message file and its dependencies
    /// This field is optional, and only needed when using ros2 native communication
    const ROS2_HASH: &'static [u8; 32] = &[0; 32];
    /// The fully qualified type name we need to work with ROS2 zenoh
    /// e.g. std_srvs::srv::dds_::SetBool_
    const ROS2_TYPE_NAME: &'static str = "";
    /// The type of data being sent in the request
    type Request: RosMessageType;
    /// The type of the data
    type Response: RosMessageType;
}

/// This trait describes a function which can validly act as a ROS service
/// server with roslibrust. We're really just using this as a trait alias
/// as the full definition is overly verbose and trait aliases are unstable.
///
/// Note: The error type intentionally does NOT have a 'static bound to allow
/// for more flexible lifetime inference when used with tokio::spawn and similar.
pub trait ServiceFn<T: RosServiceType>:
    Fn(T::Request) -> std::result::Result<T::Response, ServiceError> + Send + Sync + 'static
{
}

/// Automatic implementation of ServiceFn for Fn
impl<T, F> ServiceFn<T> for F
where
    T: RosServiceType,
    F: Fn(T::Request) -> std::result::Result<T::Response, ServiceError> + Send + Sync + 'static,
{
}

// ANCHOR: publish
/// Indicates that something is a publisher and has our expected publish
/// Implementors of this trait are expected to auto-cleanup the publisher when dropped
pub trait Publish<T: RosMessageType> {
    // Note: this is really just syntactic de-sugared `async fn`
    // However see: https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html
    // This generates a warning is rust as of writing due to ambiguity around the "Send-ness" of the return type
    // We only plan to work with multi-threaded work stealing executors (e.g. tokio) so we're manually specifying Send
    fn publish(&self, data: &T) -> impl Future<Output = Result<()>> + Send;
}
// ANCHOR_END: publish

/// Represents that an object can act as a subscriber.
/// Types returned by calling [TopicProvider::subscribe], implement this trait.
/// Types implementing this trait are expected to auto-cleanup the subscriber when dropped, and de-register themselves with ROS as needed.
pub trait Subscribe<T: RosMessageType>
where
    Self: Sized,
{
    // TODO need to solidify how we want errors to work with subscribers...
    // TODO ros1 currently requires mut for next, we should change that

    /// Returns the next message on the topic, or an Err as appropriate.
    /// [crate::Error] is currently quite generic, and the different backends can return different error variants in different circumstances.
    /// We hope to clean-up this error type substantially in the future.
    fn next(&mut self) -> impl Future<Output = Result<T>> + Send;

    /// Converts the subscriber into an async [futures_core::Stream].
    /// This allows using the various adaptors in either [tokio_stream::StreamExt](https://docs.rs/tokio-stream/latest/tokio_stream/trait.StreamExt.html)
    /// or  [futures::stream::StreamExt](https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html) to manipulate the stream.
    ///
    /// See [examples/tokio_stream_operations.rs](https://github.com/RosLibRust/roslibrust/blob/master/roslibrust/examples/tokio_stream_operations.rs) for usage.
    ///
    /// Warning: The returned stream is infinite, and will automatically reconnect if the connection is lost.
    /// Calling collect() or fold() on the stream is likely to result in a deadlock.
    fn into_stream(mut self) -> impl futures_core::Stream<Item = Result<T>> {
        use async_stream::stream;
        stream! {
            loop {
                yield self.next().await;
            }
        }
    }
}

// ANCHOR: topic_provider
/// This trait generically describes the capability of something to act as an async interface to a set of topics
///
/// This trait is largely based on ROS concepts, but could be extended to other protocols / concepts.
/// 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.
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 + Sync + 'static;
    type Subscriber<T: RosMessageType>: Subscribe<T> + Send + Sync + 'static;

    /// Advertises a topic to be published to and returns a type specific publisher to use.
    ///
    /// Topic can be any type convertible to a [GlobalTopicName] this includes `&str` and `String`.
    /// If you pass in these types validation will occur each time advertise is called.
    /// If you wish to avoid repeated validation you can create a [GlobalTopicName] yourself and pass it in.
    ///
    /// The returned publisher is expected to be "self de-registering", where dropping the publisher results in the appropriate unadvertise operation.
    fn advertise<MsgType: RosMessageType>(
        &self,
        topic: impl ToGlobalTopicName,
    ) -> impl Future<Output = Result<Self::Publisher<MsgType>>> + Send;

    /// Subscribes to a topic and returns a type specific subscriber to use.
    ///
    /// The returned subscriber is expected to be "self de-registering", where dropping the subscriber results in the appropriate unsubscribe operation.
    fn subscribe<MsgType: RosMessageType>(
        &self,
        topic: impl ToGlobalTopicName,
    ) -> impl Future<Output = Result<Self::Subscriber<MsgType>>> + Send;
}
// ANCHOR_END: topic_provider

/// 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 Future<Output = Result<T::Response>> + Send;
}

/// 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 + Sync + 'static;
    type ServiceServer: Send + Sync + 'static;

    /// A "oneshot" service call good for low frequency calls or where the service_provider may not always be available.
    fn call_service<SrvType: RosServiceType>(
        &self,
        service: impl ToGlobalTopicName,
        request: SrvType::Request,
    ) -> impl Future<Output = Result<SrvType::Response>> + Send;

    /// An optimized version of call_service that returns a persistent client that can be used to repeatedly call a service.
    /// Depending on backend this may provide a performance benefit over call_service.
    /// Dropping the returned client will perform all needed cleanup.
    fn service_client<SrvType: RosServiceType + 'static>(
        &self,
        service: impl ToGlobalTopicName,
    ) -> impl Future<Output = Result<Self::ServiceClient<SrvType>>> + Send;

    /// Advertise a service function to be available for clients to call.
    /// A handle is returned that manages the lifetime of the service.
    /// Dropping the handle will perform all needed cleanup.
    /// The service will be active until the handle is dropped.
    /// The service will always be called inside a [tokio::task::spawn_blocking](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html) call.
    /// It is generally okay to perform blocking actions inside the service function.
    ///  - See [roslibrust/examples/ros1_service_server.rs](https://github.com/RosLibRust/roslibrust/blob/master/roslibrust/examples/ros1_service_server.rs) for a sync example of using this function.
    ///  - See [roslibrust/examples/ros1_async_service_server.rs](https://github.com/RosLibRust/roslibrust/blob/master/roslibrust/examples/ros1_async_service_server.rs) for an async example of using this function.
    fn advertise_service<SrvType: RosServiceType + 'static, F: ServiceFn<SrvType>>(
        &self,
        service: impl ToGlobalTopicName,
        server: F,
    ) -> impl Future<Output = Result<Self::ServiceServer>> + Send;
}

// ANCHOR: ros_trait
/// Represents all "standard" ROS functionality generically supported by roslibrust
///
/// Implementors of this trait behave like typical ROS 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 {}
// ANCHOR_END: ros_trait

/// The Ros trait is auto implemented for any type that implements the required traits.
///
/// This trait is intended to be used in code that is generic over the ROS backend implementation,
/// and allows mocking ROS communication for testing purposes.
///
/// See the generic examples in [roslibrust](https://github.com/RosLibRust/roslibrust/tree/master/roslibrust/examples) for ideas on how to use this trait.
impl<T: 'static + Send + Sync + TopicProvider + ServiceProvider + Clone> Ros for T {}