Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanups from re org #218

Merged
merged 10 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/noetic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ jobs:
- name: Start rosbridge
run: source /opt/ros/noetic/setup.bash; roslaunch rosbridge_server rosbridge_websocket.launch & disown; rosrun rosapi rosapi_node & sleep 1
- name: Integration Tests
run: source /opt/ros/noetic/setup.bash; source /root/.cargo/env; RUST_LOG=debug cargo test --features ros1_test,ros1,running_bridge,rosapi,rosbridge,zenoh -- --test-threads 1
run: source /opt/ros/noetic/setup.bash; source /root/.cargo/env; RUST_LOG=debug cargo test --features ros1_test,all -- --test-threads 1
26 changes: 10 additions & 16 deletions Cargo.lock

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

141 changes: 87 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,74 +1,107 @@
# RosLibRust
[![Noetic](https://github.com/Carter12s/roslibrust/actions/workflows/noetic.yml/badge.svg)](https://github.com/Carter12s/roslibrust/actions/workflows/noetic.yml)
[![Galactic](https://github.com/Carter12s/roslibrust/actions/workflows/galactic.yml/badge.svg)](https://github.com/Carter12s/roslibrust/actions/workflows/galactic.yml)
[![Humble](https://github.com/Carter12s/roslibrust/actions/workflows/humble.yml/badge.svg)](https://github.com/Carter12s/roslibrust/actions/workflows/humble.yml)
[![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 "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 rosbridge 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, Iron.

Fully Supported via ROS1 native: Noetic
[![Noetic](https://github.com/roslibrust/roslibrust/actions/workflows/noetic.yml/badge.svg)](https://github.com/roslibrust/roslibrust/actions/workflows/noetic.yml)
[![Galactic](https://github.com/roslibrust/roslibrust/actions/workflows/galactic.yml/badge.svg)](https://github.com/roslibrust/roslibrust/actions/workflows/galactic.yml)
[![Humble](https://github.com/roslibrust/roslibrust/actions/workflows/humble.yml/badge.svg)](https://github.com/roslibrust/roslibrust/actions/workflows/humble.yml)
[![Iron](https://github.com/roslibrust/roslibrust/actions/workflows/iron.yml/badge.svg)](https://github.com/roslibrust/roslibrust/actions/workflows/iron.yml)
[![License:MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Code Generation of ROS Messages
This crate provides a convenient "async first" library for interacting with ROS.
This crate defines generic traits for interacting with ROS-like systems, and implementations of those traits for various backends.

This crate is **pure rust and requires no ROS1 or ROS2 dependencies or installation**.

This allows writing generic behaviors like:

```no_run
# use roslibrust_test::ros1::*;
use roslibrust::{TopicProvider, Publish, Subscribe};

async fn relay<T: TopicProvider>(ros: T) -> roslibrust::Result<()> {
let mut subscriber = ros.subscribe::<std_msgs::String>("in").await?;
let mut publisher = ros.advertise::<std_msgs::String>("out").await?;
while let Ok(msg) = subscriber.next().await {
println!("Got message: {}", msg.data);
publisher.publish(&msg).await?;
}
Ok(())
}

#[tokio::main]
async fn main() -> roslibrust::Result<()> {
// Relay messages over a rosbridge connection with either ROS1 or ROS2!
#[cfg(feature = "rosbridge")]
{
let ros = roslibrust::rosbridge::ClientHandle::new("ws://localhost:9090").await?;
relay(ros).await?;
}

// Relay messages over a native ROS1 connection
#[cfg(feature = "ros1")]
{
let ros = roslibrust::ros1::NodeHandle::new("http://localhost:11311", "relay").await?;
relay(ros).await?;
}

// Relay messages over a mock ROS connection for testing
#[cfg(feature = "mock")]
{
let ros = roslibrust::mock::MockRos::new();
relay(ros).await?;
}

#[cfg(feature = "zenoh")]
{
// Relay messages over a zenoh connection compatible with zenoh-ros1-plugin / zenoh-ros1-bridge
let ros = roslibrust::zenoh::ZenohClient::new(zenoh::open(zenoh::Config::default()).await.unwrap());
relay(ros).await?;
}

// TODO - not supported yet!
// Relay messages over a native ROS2 connection
// let ros = roslibrust::ros2::NodeHandle::new("http://localhost:11311", "relay").await?;
// relay(ros).await?;
Ok(())
}
```

The crates `roslibrust_codegen` and `roslibrust_codegen_macro` support code generation in Rust for ROS1 and ROS2 message, service, and action files. Many of the examples use the macro for convenience. `find_and_generate_ros_messages` is a macro which accepts an optional list of paths relative to the project workspace directory and will additionally check the `ROS_PACKAGE_PATH` environment variable for paths to ROS packages.
All of this is backed by common traits for ROS messages, topics, and services. `roslibrust_codegen` provides generation of Rust types from both ROS1 and ROS2 .msg/.srv files and
`roslibrust_codegen_macro` provides a convenient macro for generating these types:

It's used like this:
```rust
roslibrust_codegen_macro::find_and_generate_ros_messages!("assets/ros1_common_interfaces/std_msgs");
```no_compile
// Will generate types from all packages in ROS_PACKAGE_PATH
roslibrust_codegen_macro::find_and_generate_ros_messages!();
```

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.
If you want to see what the generated code looks like check [here](https://github.com/RosLibRust/roslibrust/blob/master/roslibrust_test/src/ros1.rs).
While the macro is useful for getting started, we recommend using `roslibrust_codegen` with a `build.rs` as shown in [example_package](https://github.com/RosLibRust/roslibrust/tree/master/example_package).
This allows cargo to know when message files are edited and automatically re-generate the code.

Generated message types are compatible with both the ROS1 native and RosBridge backends.
## Getting Started / Examples

## Roadmap
Examples can be found in [examples](https://github.com/RosLibRust/roslibrust/tree/master/roslibrust/examples).
We recommend looking at the examples prefixed with `generic_` first, these examples show the recommended style of using `roslibrust` through the generic traits.
Code written this way can be used with any backend, and critically can be tested with the mock backend.

| Feature | rosbridge | ROS1 | ROS2 |
|------------------------------|-------------------------------------------------------------|------|------|
| 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 |
Examples prefixed with `ros1_`, `rosbridge_`, and `zenoh_` show direct use of specific backends if you are only interested in a single backend.
Some backends may provide additional functionality not available through the generic traits.

Upcoming features in rough order:
To get started with writing a node with `roslibrust` we recommend looking at [example_package](https://github.com/RosLibRust/roslibrust/tree/master/example_package) and setting up your
`Cargo.toml` and `build.rs` in a similar way.
Some important tips to keep in mind with using the crate:

- Ability to write generic clients via ROS trait
- In memory backend that can be used for testing
- Support for parameter server
* This crate is built around the [tokio runtime](https://docs.rs/tokio/latest/tokio/) and requires tokio to work. All backends expect to be created inside a tokio runtime.
* The generic traits `TopicProvider` and `ServiceProvider` are not [object safe](https://doc.rust-lang.org/reference/items/traits.html#object-safety) due to their generic parameters. This means you cannot use them as trait objects with `Box<dyn TopicProvider>` or `Box<dyn ServiceProvider>`. Instead, they should be used as compile time generics like `fn foo(ros: impl TopicProvider)` or `struct MyNode<T: TopicProvider> { ros: T }`.
* By default the roslibrust crate does not include any backends. You must enable the specific backends you want to use with features in `Cargo.toml` like `roslibrust = { version = "0.12", features = ["ros1"] }`.

## 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.

### 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.
We uphold the rust lang [Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct).

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.
### Minimum Supported Rust Version / MSRV

### Running Tests
MSRV is currently set to 1.75 to enable `async fn` in traits.

There are various unit tests and integration tests behind feature flags. For tests with ROS1, both through rosbridge and native clients, you'll need a locally running `rosbridge_websocket` node and `rosmaster`. Then run with `cargo test --features "ros1_test ros1"`. For tests with ROS2, you'll need a running rosbridge server, then run with `cargo test --features "ros2_test"`. You can find relevant `Dockerfile`s and docker compose configurations udner the `docker` directory.
We are likely to increase the MSRV to 1.83 when support for `async closures` lands.
17 changes: 11 additions & 6 deletions example_package/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ version = "0.1.0"
edition = "2021"

[dependencies]
# The code generated by roslibrust_codegen has dependendencies
# We need to depend on the crate at build time so that the generate code has access to these dependencies
roslibrust_common = { path = "../roslibrust_common" }
# THIS SHOULDN'T BE NEEDED, BUT IS FOR NOW GOTTA FIX LEAKING DEPENDENCIES
roslibrust_codegen = { path = "../roslibrust_codegen" }
# Example uses rosbridge backend, but any could be substituted in
# We do need the codegen feature as the types that are generated in build.rs require types from codegen
roslibrust = { path = "../roslibrust", features = ["rosbridge", "codegen"] }
tokio = { version = "1.42", features = ["sync", "macros"] }

[dev-dependencies]
# Tests use the mock backend from roslibrust
roslibrust = { path = "../roslibrust", features = ["mock"] }
# Tests also use test-util to pause time
tokio = { version = "1.42", features = ["test-util"] }

[build-dependencies]
# We depend on codegen as a build dependency as we invoke it in build.rs
roslibrust_codegen = { path = "../roslibrust_codegen" }
roslibrust = { path = "../roslibrust", features = ["codegen"] }
# This crate is very helpful for build.rs files but not required
cargo-emit = "0.2"
2 changes: 1 addition & 1 deletion example_package/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// What we get back is a TokenStream the type normally returned by a proc_macro in rust.
// For a build.rs we typically want to serialize this data to a file for later import
let (source, dependent_paths) =
roslibrust_codegen::find_and_generate_ros_messages_without_ros_package_path(p)?;
roslibrust::codegen::find_and_generate_ros_messages_without_ros_package_path(p)?;

// It is important for build scripts to only output files to OUT_DIR.
// This guidance can be ignored for end applications. However, crates published and downloaded with cargo
Expand Down
83 changes: 73 additions & 10 deletions example_package/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,81 @@
// messages via `use`
include!(concat!(env!("OUT_DIR"), "/messages.rs"));

// Example of 'use' pointing to code created by the include! macro
mod submodule {
#[allow(unused_imports)]
use crate::std_msgs::Header;
// Important to bring traits we need into scope from roslibrust
// In this case we need to the Publish trait in scope so we can access the .publish() function on our Publisher
use roslibrust::Publish;

// Writing a simple behavior that uses the generic traits from roslibrust
// and the generated types from the macro above.
async fn pub_counter(ros: impl roslibrust::TopicProvider) {
let publisher = ros
.advertise::<std_msgs::Int16>("example_counter")
.await
.unwrap();
let mut counter = 0;
loop {
publisher
.publish(&std_msgs::Int16 { data: counter })
.await
.unwrap();
println!("Published {counter}");
counter += 1;
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}

// Our actual "main" here doesn't do much, just shows the generate types
// are here and real.
fn main() {
// Note: within our assets there is a folder named ros1_test_msgs which contains a ros package
// The ros package in its package.xml refers to the name of that package as test_msgs
// The module that is generated will use the name in package.xml and not the directory
let data = test_msgs::Constants::TEST_STR;
println!("Hello World! {data}");
#[tokio::main]
async fn main() {
// Create a rosbridge client we can use
let ros = roslibrust::rosbridge::ClientHandle::new("ws://localhost:9090")
.await
.unwrap();
// Start our behavior while waiting for ctrl_c
tokio::select! {
_ = pub_counter(ros) => {}
_ = tokio::signal::ctrl_c() => {}
}
}

// Setup a test of our pub_counter behavior
#[cfg(test)]
mod test {
use super::*;
use roslibrust::{Subscribe, TopicProvider};

#[tokio::test]
async fn test_pub_counter() {
// See: https://tokio.rs/tokio/topics/testing
// This test doesn't take 1 second to run even thou it looks like it should!
// Tokio simulates time in tests if you call pause()
// This test takes 0.00s to run on a reasonable machine
tokio::time::pause();
let ros = roslibrust::mock::MockRos::new();

// Subscribe to the topic we're publishing to
let mut subscriber = ros
.subscribe::<std_msgs::Int16>("example_counter")
.await
.unwrap();

// Start publishing in the background
tokio::spawn(async move { pub_counter(ros).await });

// Confirm we get the first message
let msg = subscriber.next().await.unwrap();
assert_eq!(msg.data, 0);

// Confirm second message quickly times out
let msg =
tokio::time::timeout(tokio::time::Duration::from_millis(10), subscriber.next()).await;
assert!(msg.is_err());

// Wait a bit
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
// Now get second message
let msg = subscriber.next().await.unwrap();
assert_eq!(msg.data, 1);
}
}
Loading
Loading