Skip to content

Commit

Permalink
Merge pull request #185 from Carter12s/fixes-from-plane
Browse files Browse the repository at this point in the history
Initial Work on Generic ROS Client
  • Loading branch information
Carter12s authored Aug 6, 2024
2 parents 4d2dced + d7a673a commit b9c6d91
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 170 deletions.
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

All notable changes to this project will be documented in this file.

## Release Instructions:
## Release Instructions

Steps:

Expand All @@ -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

Expand Down
16 changes: 2 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 28 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
[![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).

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

Expand All @@ -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

Expand Down
1 change: 0 additions & 1 deletion roslibrust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 82 additions & 0 deletions roslibrust/examples/generic_client.rs
Original file line number Diff line number Diff line change
@@ -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<T: TopicProvider> {
ros: T,
name: String,
}

impl<T: TopicProvider> MyNode<T> {
async fn run(self) {
let publisher = self
.ros
.advertise::<std_msgs::String>("/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() {}
7 changes: 7 additions & 0 deletions roslibrust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
22 changes: 22 additions & 0 deletions roslibrust/src/ros1/node/mod.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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::*;
Expand Down Expand Up @@ -98,3 +101,22 @@ impl<T> From<mpsc::error::SendError<T>> 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<NodeError> 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),
}
}
}
3 changes: 0 additions & 3 deletions roslibrust/src/rosapi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<rosapi::GetTimeResponse>;
async fn topics(&self) -> RosLibRustResult<rosapi::TopicsResponse>;
Expand Down Expand Up @@ -99,7 +97,6 @@ trait RosApi {
async fn get_services(&self) -> RosLibRustResult<rosapi::ServicesResponse>;
}

#[async_trait]
impl RosApi for ClientHandle {
/// Get the current time
async fn get_time(&self) -> RosLibRustResult<rosapi::GetTimeResponse> {
Expand Down
3 changes: 0 additions & 3 deletions roslibrust/src/rosbridge/comm.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<()>;
Expand All @@ -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!(
Expand Down
5 changes: 0 additions & 5 deletions roslibrust/src/rosbridge/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading

0 comments on commit b9c6d91

Please sign in to comment.