Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 { .. }
        ));
    }
}