r2r

repository·master·Indexed 19 days ago

https://github.com/sequenceplanner/r2r

Runtime-agnostic, async Rust bindings for ROS2 (version 0.9.6). R2R uses C introspection to generate Rust types directly from C code, bypassing the standard ROS2 build pipeline. It supports publish/subscribe, services, actions, parameter handling, and simulated time across Linux, OSX, and Windows. Compatible with ROS2 distributions including Dashing, Eloquent, Foxy, Galactic, Humble, Iron, Jazzy, and Lyrical.

Tokens
11.4K
Snippets
49
Records
54
Agent score
65%

What's inside r2r

  1. Supported ROS2 distributions and features

    master

    R2R is compatible with the following ROS2 distributions:

    • Dashing, Eloquent, Foxy, Galactic, Humble, Iron, Jazzy, and Lyrical.

    Core Features Supported:

    • Building Rust types
    • Publish/Subscribe
    • Services
    • Actions
    • Parameter handling
    • Simulated time (requires rosgraph_msgs to be sourced during build)

    Supported Platforms:

    • Linux, OSX, and Windows.
  2. Understand the R2R async model and runtime

    master

    R2R differs from rclpy and rclcpp by eliminating synchronous callbacks. Instead, it uses Rust futures and streams, allowing you to use the await syntax for ROS services and actions.

    Key Architectural Note: R2R does not choose an async runtime. The user is responsible for task spawning and managing the execution of futures. The API is intentionally limited to what futures-rs provides.

  3. Install and set up R2R

    master

    To use R2R in your Rust project, follow these steps:

    1. Install Dependencies: Ensure libclang is installed on your system (e.g., libclang-dev on Ubuntu).
    2. Add Dependency: Add r2r to your Cargo.toml.
    3. Source ROS2: You must source your ROS2 installation in your shell before building or running your application.
    4. Automatic Rebuilds: The bindings will automatically rebuild if you source new workspaces.
    5. Force Recompilation: If you modify existing message types, force the recompilation of Rust message types by running: cargo clean -p r2r_msg_gen
    # Cargo.toml
    r2r = "0.9.6"
  4. How R2R handles ROS2 message types

    master

    R2R circumvents the standard ROS2 .msg/.idl pipeline by relying on already generated C code. It uses C introspection libraries to create convenience Rust types.

    By default, R2R builds bindings to the RCL and all message types found in your currently sourced ROS environment. Message types are available via the r2r::generated_msgs module (e.g., r2r::std_msgs::msg::String).

  5. How to use simulated time with TimeSource

    master

    In ROS2, simulated time allows you to synchronize your node's clock with an external source (like a simulator) via the /clock topic. The TimeSource struct manages this synchronization by subscribing to /clock and distributing the received time to all attached Clock instances of type RosTime.

    You can enable simulated time in two ways:

    1. Programmatically: Call TimeSource::enable_sim_time(&mut node).
    2. Via ROS2 Parameters: Launch your node with the parameter use_sim_time:=true (this requires a registered parameter handler).

    When enabled, TimeSource creates a subscriber to the rosgraph_msgs::msg::Clock topic. When a new clock message is received, it updates all attached clocks. To stop using simulated time, call TimeSource::disable_sim_time().

    // Example of enabling simulated time programmatically
    time_source.enable_sim_time(&mut node)?;
  6. Handle asynchronous service requests with ServiceRequest

    master

    In R2R, service requests are handled asynchronously. Instead of a synchronous callback, you receive a ServiceRequest<T> object. This object can be moved across threads or tasks. To complete the request and send a response back to the client, call the .respond(msg) method, where msg is the response type associated with the service.

    If the underlying service has been dropped before you call respond, it will return Error::RCL_RET_ACTION_SERVER_INVALID.

    // Assuming 'request' is a ServiceRequest<T> received from a channel
    let response = T::Response::default(); // Construct your response
    request.respond(response)?; // Consumes the request and sends the response
  7. How typed and untyped publishers differ

    master

    R2R provides two ways to publish data depending on your need for type safety and performance:

    1. Publisher<T> (Typed):

      • Pros: Compile-time type checking, support for zero-copy (loaned) messages, and high performance.
      • Use case: Standard ROS2 development where message types are known at compile time.
    2. PublisherUntyped (Untyped):

      • Pros: Flexibility; can handle messages via JSON or raw byte buffers.
      • Use case: Dynamic systems, bridging between different serialization formats, or when message types are determined at runtime.
  8. Reduce build times using IDL_PACKAGE_FILTER

    master

    By default, R2R builds bindings for all message types found in your currently sourced ROS2 environment, which can lead to very long build times in large workspaces.

    You can limit the scope of generated bindings by setting the IDL_PACKAGE_FILTER environment variable. This allows you to declare only the specific message packages you need.

    Note: There is no automatic dependency resolution for nested message types; you must explicitly include all message packages used in your project. You can set this variable in .cargo/config.toml for convenience.

  9. Quickstart: Create a basic ROS2 Node with Pub/Sub

    master

    To use R2R, you must first source your ROS2 environment. R2R uses C introspection to build bindings to the RCL and existing message types without requiring the ROS2 build infrastructure.

    This example demonstrates how to initialize a Context, create a Node, and set up an asynchronous publisher and subscriber using a LocalPool executor.

    use futures::{executor::LocalPool, future, stream::StreamExt, task::LocalSpawnExt};
    use r2r::QosProfile;
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let ctx = r2r::Context::create()?;
        let mut node = r2r::Node::create(ctx, "node", "namespace")?;
        let subscriber =
            node.subscribe::<r2r::std_msgs::msg::String>("/topic", QosProfile::default())?;
        let publisher =
            node.create_publisher::<r2r::std_msgs::msg::String>("/topic", QosProfile::default())?;
        let mut timer = node.create_wall_timer(std::time::Duration::from_millis(1000))?;
    
        // Set up a simple task executor.
        let mut pool = LocalPool::new();
        let spawner = pool.spawner();
    
        // Run the subscriber in one task, printing the messages
        spawner.spawn_local(async move {
            subscriber
                .for_each(|msg| {
                    println!("got new msg: {}", msg.data);
                    future::ready(())
                })
                .await
        })?;
    
        // Run the publisher in another task
        spawner.spawn_local(async move {
            let mut counter = 0;
            loop {
                let _elapsed = timer.tick().await.unwrap();
                let msg = r2r::std_msgs::msg::String {
                    data: format!("Hello, world! ({})", counter),
                };
                publisher.publish(&msg).unwrap();
                counter += 1;
            }
        })?;
    
        // Main loop spins ros.
        loop {
            node.spin_once(std::time::Duration::from_millis(100));
            pool.run_until_stalled();
        }
    }
  10. Inspect individual message members with MessageMember

    master

    A MessageMember represents a single field within a ROS2 message. You can use it to query the field's name, its Rust-compatible name, its data type, and its memory layout.

    Key methods:

    • name(): Returns the original field name.
    • rust_name(): Returns the field name mangled for Rust compatibility.
    • type_id(): Returns a MemberType enum representing the data type.
    • offset(): Returns the byte offset of the member within the message.
    • is_array(): Returns true if the member is an array.
    • array_info(): Returns an Option<ArrayInfo> containing size and is_upper_bound if the member is an array.
    • members(): If the member type is MemberType::Message, this returns the TypeSupport for the nested message.
    // Example usage of MessageMember methods
    let name = member.name();
    let r_name = member.rust_name();
    let m_type = member.type_id();
    let offset = member.offset();
    
    if member.is_array() {
        if let Some(info) = member.array_info() {
            println!("Array size: {}", info.size);
        }
    }
  11. Use PublisherUntyped to publish JSON or raw bytes

    master

    A PublisherUntyped allows publishing messages without compile-time type safety. This is useful for dynamic messaging or when the type is only known at runtime.

    Key methods:

    • publish(&self, msg: serde_json::Value): Converts a serde_json::Value into the expected ROS message type and publishes it. The user is responsible for ensuring the JSON structure matches the ROS message definition.
    • publish_raw(&self, data: &[u8]): Publishes pre-serialized ROS message data as a byte slice.
    • get_inter_process_subscription_count(&self): Returns the number of external subscribers.
    • wait_for_inter_process_subscribers(&self): Returns a Future that resolves when an external subscriber is detected.
    // Publishing via JSON
    publisher_untyped.publish(serde_json::json!({ "field": 42 }))?;
    
    // Publishing raw serialized bytes
    publisher_untyped.publish_raw(&serialized_data)?;
  12. Spin the ROS2 Node

    master

    The spin_once method must be called repeatedly (typically in a loop) to process ROS2 events. It handles wakeups for subscribers, services, timers, and action clients/servers. The timeout parameter specifies how long the function should block if no events are pending.

    loop {
        node.spin_once(Duration::from_millis(10));
        // Your async logic here
    }