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"
publish = false
# 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"] }
serde_json = "1.0"
# 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);
}
#[test]
fn generated_registry_round_trips_backend_selected_json() {
assert!(MESSAGE_REGISTRY
.descriptors()
.windows(2)
.all(|pair| pair[0].ros_type_name < pair[1].ros_type_name));
let descriptor = MESSAGE_REGISTRY
.get("std_msgs/msg/Int16")
.expect("generated registry should accept ROS2 type spelling");
assert_eq!(descriptor.ros_type_name, "std_msgs/Int16");
let message = MESSAGE_REGISTRY
.message_from("std_msgs/Int16", &serde_json::json!({ "data": 42 }))
.expect("valid dynamic message");
let json = roslibrust::rosbridge::serialize_dynamic_message(&message)
.expect("rosbridge JSON encoding");
assert_eq!(json, br#"{"data":42}"#);
let decoded_json = roslibrust::rosbridge::deserialize_dynamic_message(
MESSAGE_REGISTRY
.get("std_msgs::msg::dds_::Int16_")
.expect("generated registry should accept DDS type spelling"),
&json,
)
.expect("rosbridge JSON decoding");
assert_eq!(decoded_json.value(), message.value());
let malformed =
roslibrust::rosbridge::deserialize_dynamic_message(descriptor, b"{").unwrap_err();
assert!(matches!(
malformed,
roslibrust::DynamicMessageError::Deserialize { .. }
));
}
#[test]
fn generated_registry_reports_lookup_and_validation_errors() {
let unknown = MESSAGE_REGISTRY
.from_value("not_a_package/Unknown", roslibrust::DynamicValue::Unit)
.unwrap_err();
assert!(matches!(
unknown,
roslibrust::DynamicMessageError::UnknownType(_)
));
let invalid = MESSAGE_REGISTRY
.from_value(
"std_msgs/Int16",
roslibrust::DynamicValue::Message(vec![roslibrust::DynamicField {
name: "data".to_owned(),
value: roslibrust::DynamicValue::String("not an integer".to_owned()),
}]),
)
.unwrap_err();
assert!(matches!(
invalid,
roslibrust::DynamicMessageError::InvalidValue { .. }
));
}
}