Quick Getting Started
Brief guide for setting up roslibrust, if you get stuck or want a more in depth guide checkout Extended Getting Started
Add roslibrust as a dependency in Cargo.toml
:
[package]
name = "example_package"
version = "0.1.0"
edition = "2021"
# What crates your code needs when built regularly (e.g. `cargo build` or `cargo run`)
[dependencies]
# Here we specify what features from roslibrust we want our main code to have access to
# We'll need at least one backend, rosbridge is shown here, but any could be substituted in
# We also need the codegen feature as the types that are generated in build.rs rely on types from codegen
roslibrust = { path = "../roslibrust", features = ["rosbridge", "codegen"] }
# We need a tokio runtime to setup our async behaviors, these are the minimum tokio features we need
tokio = { version = "1", features = ["sync", "macros"] }
# Brining in a logger to help with debugging
env_logger = "0.11"
# What crates your code needs for testing and examples
[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", features = ["test-util"] }
# What crates your code needs to run it's build.rs file
[build-dependencies]
# We need the codegen feature when running build.rs to be able to actually generate the types
roslibrust = { path = "../roslibrust", features = ["codegen"] }
Setup a build.rs
file to generate ROS types, and set your search_paths
to point at your ROS messages:
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Define our search paths
// Note: these currently point towards the assets folder in the roslibrust repository,
// you'll want to point this at the location of your own .msg/.srv files
let p = vec![
"../assets/ros2_common_interfaces".into(),
"../assets/ros2_required_msgs/rcl_interfaces/builtin_interfaces".into(),
];
// Actually invoke code generation on our search paths.
let (source, dependent_paths) =
roslibrust::codegen::find_and_generate_ros_messages_without_ros_package_path(p)?;
// This returns two things:
// 1) A TokenStream which is the rust code we want to generate
// 2) A list of paths that if modified would require the code to be regenerated. We use this to inform Cargo
// of when to re-run our build script.
// It is important for build scripts to only output files to OUT_DIR which is an environment variable set by Cargo.
let out_dir = std::env::var_os("OUT_DIR").unwrap();
// Name of the file in out_dir we want to write our generated code to
let dest_path = std::path::Path::new(&out_dir).join("messages.rs");
// Write the generated code to disk
std::fs::write(dest_path, source.to_string())?;
// If we stopped at this point, our code would still work, but Cargo would not know to rebuild
// our package when a message file changed.
// Cargo recognizes certain command line strings that build scripts print out:
for path in &dependent_paths {
// Tell cargo to re-run our build script if any of these files change
println!("cargo:rerun-if-changed={}", path.display());
}
Ok(())
}
Write a basic generic node with tests, and profit:
//! This file shows how to correctly import files generated by build.rs:
// This macro trick correctly "imports" messages.rs into our crate
// This should only be invoked once in the crate and other locations can access the
// messages via `use`
include!(concat!(env!("OUT_DIR"), "/messages.rs"));
// 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::Ros) {
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.
#[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);
}
}