diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e27a42c..5c39fd56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. -## Release Instructions: +## Release Instructions Steps: @@ -22,14 +22,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Added + - ROS1 Native Publishers now support latching behavior - The XML RPC client for interacting directly with the rosmaster server has been exposed as a public API +- Experimental: Initial support for writing generic clients that can be compile time specialized for rosbridge or ros1 ### Fixed + - ROS1 Native Publishers correctly call unadvertise when dropped ### Changed -- ROS1 Node Handle's advertise() now requies a latching argument + +- ROS1 Node Handle's advertise() now requires a latching argument ## 0.10.2 - August 3rd, 2024 diff --git a/Cargo.lock b/Cargo.lock index 3c4c49c4..7cd6986e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,17 +90,6 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" -[[package]] -name = "async-trait" -version = "0.1.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - [[package]] name = "autocfg" version = "1.3.0" @@ -1240,7 +1229,6 @@ version = "0.10.2" dependencies = [ "abort-on-drop", "anyhow", - "async-trait", "byteorder", "dashmap", "deadqueue", @@ -2288,9 +2276,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" +checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601" [[package]] name = "zerocopy" diff --git a/README.md b/README.md index f8763c6d..08b81d8b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ [![Iron](https://github.com/Carter12s/roslibrust/actions/workflows/iron.yml/badge.svg)](https://github.com/Carter12s/roslibrust/actions/workflows/iron.yml) [![License:MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -This package aims to provide a convenient intermediary between ROS1's rosbridge and Rust similar to roslibpy and roslibjs. +This package aims to provide a convenient "async first" library for interacting with ROS. + +Currently this packaged provides support for both ROS1 native communication (TCPROS) and rosbridge's protocol which provides support for both ROS1 and ROS2 albeit with some overhead. Information about the protocol can be found [here](https://github.com/RobotWebTools/rosbridge_suite). @@ -13,7 +15,7 @@ Note on documentation: All information about the crate itself (examples, documentation, tutorials, etc.) lives in the source code and can be viewed on [docs.rs](https://docs.rs/roslibrust). This readme is for "Meta" information about developing for the crate. -Fully Supported via rosbridge: Noetic, Galactic, Humble. +Fully Supported via rosbridge: Noetic, Galactic, Humble, Iron, ## Code Generation of ROS Messages @@ -24,47 +26,46 @@ It's used like this: roslibrust_codegen_macro::find_and_generate_ros_messages!("assets/ros1_common_interfaces/std_msgs"); ``` -Code generation can also be done with a script using the same code generation backend called by the macro. See the contents of `example_package` for a detailed example of how this can be done. While the proc_macros are extremely convenient for getting started +Code generation can also be done with a build.rs script using the same code generation backend called by the macro. See the contents of `example_package` for a detailed example of how this can be done. While the proc_macros are extremely convenient for getting started there is currently no (good) way for a proc_macro to inform the compiler that it needs to be re-generated when an external file changes. Using a build script requires more setup, but can correctly handling re-building when message files are edited. -## Experimental Support for ROS1 Native - -If built with the `ros1` feature, `roslibrust` exports some experimental support for implementing nodes which talk to other ROS1 nodes using the TCPROS protocol without the need for the rosbridge as an intermediary. See `ros1_talker.rs` and `ros1_listener.rs` under `roslibrust/examples` to see usage. This implementation is relatively new, incomplete, and untested. Filing issues on bugs encountered is very appreciated! - -See this issue filter for known issues: https://github.com/Carter12s/roslibrust/labels/ros1 +Generated message types are compatible with both the ROS1 native and RosBridge backends. ## Roadmap -Rough overview of the features planned to built for this crate in what order: - | Feature | rosbridge | ROS1 | ROS2 | |------------------------------|-------------------------------------------------------------|------|------| -| examples | ✅ | ✅ | x | -| message_gen | ✅ | ✅ | ✅ | -| advertise / publish | ✅ | ✅ | x | -| unadvertise | ✅ | ✅ | x | -| subscribe | ✅ | ✅ | x | -| unsubscribe | ✅ | ✅ | x | -| services | ✅ | x | x | -| rosapi | ✅ (ROS1 only for now) | N/A | N/A | +| examples | ✅ | ✅ | x | +| message_gen | ✅ | ✅ | ✅ | +| advertise / publish | ✅ | ✅ | x | +| unadvertise | ✅ | ✅ | x | +| subscribe | ✅ | ✅ | x | +| unsubscribe | ✅ | ✅ | x | +| services | ✅ | ✅ | x | +| actions | (codgen of message types only) | +| rosapi | ✅ | x | x | | TLS / wss:// | Should be working, untested | N/A | N/A | -| ROS2 msgs length limits | Planned | N/A | x | -| cbor | Planned | N/A | N/A | -| rosbridge status access | Planned | N/A | N/A | -| rosout logger | Planned | x | x | -| auth | Planned | N/A | N/A | -| fragment / png | Uncertain if this package will support | x | N/A | -| cbor-raw | Uncertain if this package will support | N/A | N/A | +Upcoming features in rough order: + +- Ability to write generic clients via ROS trait +- In memory backend that can be used for testing +- Support for parameter server ## Contributing Contribution through reporting of issues encountered and implementation in PRs is welcome! Before landing a large PR with lots of code implemented, please open an issue if there isn't a relevant one already available and chat with a maintainer to make sure the design fits well with all supported platforms and any in-progress implementation efforts. -### Rust Version for Development +### Minimum Supported Rust Version / MSRV + +We don't have an official MSRV yet. + +Due to cargo 1.72 enabling "doctest-in-workspace" by default it is recommended to use Rust 1.72+ for development. +Previous rust versions are support but will require some incantations when executing doctests. -Due to cargo 1.72 enabling "doctest-in-workspace" by default it is reccomended to use Rust 1.72+ for develop. Previous rust versions are support but will require some incantations when executing doctests. +The experimental topic_provider feature currently relies on `async fn` in traits from Rust 1.75. +When that feature standardizes that will likely become our MSRV. ### Running Tests diff --git a/roslibrust/Cargo.toml b/roslibrust/Cargo.toml index 51183c1d..645bf452 100644 --- a/roslibrust/Cargo.toml +++ b/roslibrust/Cargo.toml @@ -13,7 +13,6 @@ categories = ["science::robotics"] [dependencies] abort-on-drop = "0.2" anyhow = "1.0" -async-trait = "0.1" byteorder = "1.4" dashmap = "5.3" deadqueue = "0.2.4" # .4+ is required to fix bug with missing tokio dep diff --git a/roslibrust/examples/generic_client.rs b/roslibrust/examples/generic_client.rs new file mode 100644 index 00000000..e850485e --- /dev/null +++ b/roslibrust/examples/generic_client.rs @@ -0,0 +1,82 @@ +//! Purpose of this example is to show how the TopicProvider 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/std_msgs" + ); + // 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, + name: String, + } + + impl MyNode { + async fn run(self) { + let publisher = self + .ros + .advertise::("/chatter") + .await + .unwrap(); + + loop { + let msg = std_msgs::String { + data: format!("Hello world from {}", self.name), + }; + publisher.publish(&msg).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + } + } + } + + // create a rosbridge handle and start node + let ros = roslibrust::ClientHandle::new("ws://localhost:9090") + .await + .unwrap(); + let node = MyNode { + ros, + name: "rosbridge_node".to_string(), + }; + 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, + name: "ros1_node".to_string(), + }; + 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 + // You should be able to run `rostopic echo /chatter` and see the two nodes print out their names. + // Note: this will not run without rosbridge running +} + +#[cfg(not(feature = "topic_provider"))] +fn main() {} diff --git a/roslibrust/src/lib.rs b/roslibrust/src/lib.rs index aca3713e..78927e8c 100644 --- a/roslibrust/src/lib.rs +++ b/roslibrust/src/lib.rs @@ -108,6 +108,13 @@ pub mod rosapi; #[cfg(feature = "ros1")] pub mod ros1; +// Topic provider is locked behind a feature until it is stabalized +// additionally because of its use of generic associated types, it requires rust >1.65 +#[cfg(feature = "topic_provider")] +/// Provides a generic trait for building clients / against either the rosbridge, +/// ros1, or a mock backend +pub mod topic_provider; + /// For now starting with a central error type, may break this up more in future #[derive(thiserror::Error, Debug)] pub enum RosLibRustError { diff --git a/roslibrust/src/ros1/node/mod.rs b/roslibrust/src/ros1/node/mod.rs index 7d0a5698..95719365 100644 --- a/roslibrust/src/ros1/node/mod.rs +++ b/roslibrust/src/ros1/node/mod.rs @@ -1,6 +1,8 @@ //! This module contains the top level Node and NodeHandle classes. //! These wrap the lower level management of a ROS Node connection into a higher level and thread safe API. +use crate::RosLibRustError; + use super::{names::InvalidNameError, RosMasterError}; use std::{ io, @@ -11,6 +13,7 @@ pub(crate) mod actor; mod handle; mod xmlrpc; use actor::*; +use anyhow::anyhow; pub use handle::NodeHandle; use tokio::sync::{mpsc, oneshot}; use xmlrpc::*; @@ -98,3 +101,22 @@ impl From> for NodeError { Self::ChannelClosedError } } + +// TODO MAJOR: this is kinda messy +// but for historic development reasons (not having a design for errors) +// we produced two different error types for the two different impls (ros1, rosbridge) +// This allows fusing the two error types together so that TopicProider can work +// but we should just better design all the error handling +impl From for RosLibRustError { + fn from(value: NodeError) -> Self { + match value { + NodeError::RosMasterError(e) => RosLibRustError::ServerError(e.to_string()), + NodeError::ChannelClosedError => { + RosLibRustError::Unexpected(anyhow!("Channel closed, something was dropped?")) + } + NodeError::InvalidName(e) => RosLibRustError::InvalidName(e.to_string()), + NodeError::XmlRpcError(e) => RosLibRustError::SerializationError(e.to_string().into()), + NodeError::IoError(e) => RosLibRustError::IoError(e), + } + } +} diff --git a/roslibrust/src/rosapi/mod.rs b/roslibrust/src/rosapi/mod.rs index a8daf3d0..37160bed 100644 --- a/roslibrust/src/rosapi/mod.rs +++ b/roslibrust/src/rosapi/mod.rs @@ -4,7 +4,6 @@ //! Ensure rosapi is running on your target system before attempting to utilize these features! use crate::{ClientHandle, RosLibRustResult}; -use async_trait::async_trait; // TODO major issue here for folks who actually try to use rosapi in their project // This macro isn't going to expand correctly when not used from this crate's workspace @@ -14,7 +13,6 @@ roslibrust_codegen_macro::find_and_generate_ros_messages!("assets/ros1_common_in /// Represents the ability to interact with the interfaces provided by the rosapi node. /// This trait is implemented for ClientHandle when the `rosapi` feature is enabled. -#[async_trait] trait RosApi { async fn get_time(&self) -> RosLibRustResult; async fn topics(&self) -> RosLibRustResult; @@ -99,7 +97,6 @@ trait RosApi { async fn get_services(&self) -> RosLibRustResult; } -#[async_trait] impl RosApi for ClientHandle { /// Get the current time async fn get_time(&self) -> RosLibRustResult { diff --git a/roslibrust/src/rosbridge/comm.rs b/roslibrust/src/rosbridge/comm.rs index 212d8b3e..2f66edb4 100644 --- a/roslibrust/src/rosbridge/comm.rs +++ b/roslibrust/src/rosbridge/comm.rs @@ -1,6 +1,5 @@ use crate::{rosbridge::Writer, RosLibRustResult}; use anyhow::bail; -use async_trait::async_trait; use futures_util::SinkExt; use log::debug; use roslibrust_codegen::RosMessageType; @@ -88,7 +87,6 @@ impl FromStr for Ops { /// So we're defining this trait on a foreign type, since we didn't end up /// using this trait for mocking. I'm inclined to replace it, and move the /// impls directly into some wrapper around [Writer] -#[async_trait] pub(crate) trait RosBridgeComm { async fn subscribe(&mut self, topic: &str, msg_type: &str) -> RosLibRustResult<()>; async fn unsubscribe(&mut self, topic: &str) -> RosLibRustResult<()>; @@ -113,7 +111,6 @@ pub(crate) trait RosBridgeComm { ) -> RosLibRustResult<()>; } -#[async_trait] impl RosBridgeComm for Writer { async fn subscribe(&mut self, topic: &str, msg_type: &str) -> RosLibRustResult<()> { let msg = json!( diff --git a/roslibrust/src/rosbridge/mod.rs b/roslibrust/src/rosbridge/mod.rs index b21edea4..a7948b7e 100644 --- a/roslibrust/src/rosbridge/mod.rs +++ b/roslibrust/src/rosbridge/mod.rs @@ -21,11 +21,6 @@ mod integration_tests; #[allow(dead_code)] type TestResult = Result<(), anyhow::Error>; -// Topic provider is locked behind a feature until it is stabalized -// additionally because of its use of generic associated types, it requires rust >1.65 -#[cfg(feature = "topic_provider")] -mod topic_provider; - /// Communication primitives for the rosbridge_suite protocol mod comm; diff --git a/roslibrust/src/rosbridge/topic_provider.rs b/roslibrust/src/rosbridge/topic_provider.rs deleted file mode 100644 index b80d10aa..00000000 --- a/roslibrust/src/rosbridge/topic_provider.rs +++ /dev/null @@ -1,115 +0,0 @@ -use async_trait::async_trait; -use roslibrust_codegen::{RosMessageType, RosServiceType}; - -use crate::RosLibRustResult; - -/// 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. -#[async_trait] -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 - type Publisher; - type Subscriber; - type ServiceHandle; - - async fn advertise( - &self, - topic: &str, - ) -> RosLibRustResult>; - - async fn subscribe( - &self, - topic: &str, - ) -> RosLibRustResult>; - - async fn call_service( - &self, - topic: &str, - request: Req, - ) -> RosLibRustResult; - - async fn advertise_service( - &self, - topic: &str, - server: fn( - T::Request, - ) - -> Result>, - ) -> RosLibRustResult; -} - -#[async_trait] -impl TopicProvider for crate::ClientHandle { - type Publisher = crate::Publisher; - type Subscriber = crate::Subscriber; - type ServiceHandle = crate::ServiceHandle; - - async fn advertise( - &self, - topic: &str, - ) -> RosLibRustResult> { - self.advertise::(topic.as_ref()).await - } - - async fn subscribe( - &self, - topic: &str, - ) -> RosLibRustResult> { - self.subscribe(topic).await - } - - async fn call_service( - &self, - topic: &str, - request: Req, - ) -> RosLibRustResult { - self.call_service(topic, request).await - } - - async fn advertise_service( - &self, - topic: &str, - server: fn( - T::Request, - ) - -> Result>, - ) -> RosLibRustResult { - self.advertise_service::(topic, server).await - } -} - -#[cfg(test)] -mod test { - use super::TopicProvider; - use crate::ClientHandle; - - // This test specifically fails because TopicProvider is not object safe - // Traits that have methods with generic parameters cannot be object safe in rust (currently) - // #[test] - // fn topic_provider_can_be_constructed() -> TestResult { - // let x: Box = Box::new(ClientHandle::new("")); - // Ok(()) - // } - - // This tests proves that you could use topic provider in a compile time api, but not object safe... - #[test_log::test] - #[should_panic] - fn topic_proivder_can_be_used_at_compile_time() { - struct MyClient { - _client: T, - } - - // Kinda a hack way to make the compiler prove it could construct a MyClient with out actually - // constructing one at runtime - let new_mock: Result = Err(anyhow::anyhow!("Expected error")); - - let _x = MyClient { - _client: new_mock.unwrap(), - }; - } -} diff --git a/roslibrust/src/topic_provider.rs b/roslibrust/src/topic_provider.rs new file mode 100644 index 00000000..ced81f56 --- /dev/null +++ b/roslibrust/src/topic_provider.rs @@ -0,0 +1,204 @@ +use roslibrust_codegen::{RosMessageType, RosServiceType}; + +use crate::{RosLibRustResult, 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 +pub trait Publish { + // 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 futures::Future> + Send; +} + +impl Publish for crate::Publisher { + async fn publish(&self, data: &T) -> RosLibRustResult<()> { + // TODO clone here is bad and we should standardized on ownership of publish + self.publish(data.clone()).await + } +} + +impl Publish for crate::ros1::Publisher { + async fn publish(&self, data: &T) -> RosLibRustResult<()> { + // TODO error type conversion here is terrible and we need to standardize error stuff badly + self.publish(data) + .await + .map_err(|e| crate::RosLibRustError::SerializationError(e.to_string())) + } +} + +/// 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. +// #[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 + type Publisher: Publish; + type Subscriber; + type ServiceHandle; + + fn advertise( + &self, + topic: &str, + ) -> impl futures::Future>> + Send; + + fn subscribe( + &self, + topic: &str, + ) -> impl futures::Future>> + Send; + + fn call_service( + &self, + topic: &str, + request: Req, + ) -> impl std::future::Future> + Send; + + fn advertise_service( + &self, + topic: &str, + server: F, + ) -> impl futures::Future> + Send + where + F: ServiceFn; +} + +// Implementation of TopicProvider trait for rosbridge client +impl TopicProvider for crate::ClientHandle { + type Publisher = crate::Publisher; + type Subscriber = crate::Subscriber; + type ServiceHandle = crate::ServiceHandle; + + async fn advertise( + &self, + topic: &str, + ) -> RosLibRustResult> { + self.advertise::(topic.as_ref()).await + } + + async fn subscribe( + &self, + topic: &str, + ) -> RosLibRustResult> { + self.subscribe(topic).await + } + + async fn call_service( + &self, + topic: &str, + request: Req, + ) -> RosLibRustResult { + self.call_service(topic, request).await + } + + async fn advertise_service( + &self, + topic: &str, + server: F, + ) -> RosLibRustResult + where + F: ServiceFn, + { + self.advertise_service::(topic, server).await + } +} + +#[cfg(feature = "ros1")] +impl TopicProvider for crate::ros1::NodeHandle { + type Publisher = crate::ros1::Publisher; + type Subscriber = crate::ros1::Subscriber; + type ServiceHandle = crate::ros1::ServiceServer; + + async fn advertise( + &self, + topic: &str, + ) -> RosLibRustResult> { + // TODO MAJOR: consider promoting queue size, making unlimited default + self.advertise::(topic.as_ref(), 10, false) + .await + .map_err(|e| e.into()) + } + + async fn subscribe( + &self, + topic: &str, + ) -> RosLibRustResult> { + // TODO MAJOR: consider promoting queue size, making unlimited default + self.subscribe(topic, 10).await.map_err(|e| e.into()) + } + + async fn call_service( + &self, + topic: &str, + request: Req, + ) -> RosLibRustResult { + // TODO this is a problem + // service_client for Ros1 wants the service type, not the req and res types + // We should change top level API of TopicProvider and probably of rosbridge + unimplemented!(); + } + + 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; + use crate::{ros1::NodeHandle, ClientHandle}; + + // This test specifically fails because TopicProvider is not object safe + // Traits that have methods with generic parameters cannot be object safe in rust (currently) + // #[test] + // fn topic_provider_can_be_constructed() -> TestResult { + // let x: Box = Box::new(ClientHandle::new("")); + // Ok(()) + // } + + // This tests proves that you could use topic provider in a compile time api, but not object safe... + #[test_log::test] + #[should_panic] + fn topic_provider_can_be_used_at_compile_time() { + struct MyClient { + _client: T, + } + + // Kinda a hack way to make the compiler prove it could construct a MyClient with out actually + // constructing one at runtime + let new_mock: Result = Err(anyhow::anyhow!("Expected error")); + + let _x = MyClient { + _client: new_mock.unwrap(), // panic + }; + } + + #[test_log::test] + #[should_panic] + fn topic_provider_can_be_used_with_ros1() { + struct MyClient { + _client: T, + } + + // Kinda a hack way to make the compiler prove it could construct a MyClient with out actually + // constructing one at runtime + let new_mock: Result = Err(anyhow::anyhow!("Expected error")); + + let _x = MyClient { + _client: new_mock.unwrap(), // panic + }; + } +}