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"

# 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);
    }
}