Skip to main content

ROS related practices

You can find more examples or specific examples for certain applications in ROS2 best practices section.


General Recommendations​

  • Components (formerly "nodelets" in ROS 1) allow multiple nodes to be loaded as plugins into a single process (a component container). This unlocks two related but distinct benefits:
    1. Intra-Process Communication (IPC) allows zero-copy communication between nodes by passing messages by pointer, skipping serialization entirely. This is NOT the default behavior! The developer is responsible for setting the following:

      • use_intra_process_comms must be enabled on both the publishing and subscribing nodes, either via the launch file or in NodeOptions.
      • The publisher must pass messages as std::unique_ptr and give up ownership (zero-copy only applies on this path. Publishing by const & or SharedPtr falls back to copying).
      • The subscriber callback must take ConstSharedPtr or UniquePtr. ConstSharedPtr shares one buffer across all subscribers (read-only). UniquePtr moves ownership into the single subscriber. A mutable SharedPtr callback forces rclcpp to copy, defeating zero-copy.
      • The QoS profiles of the publisher and the subscriber must be compatible.
      // publisher node
      auto msg = std::make_unique<geometry_msgs::msg::Point>();
      pub.publish(std::move(msg));
    2. Process-level resource sharing, which applies whether or not IPC is enabled:

      • One DDS participant per container instead of one per node (much less discovery traffic and lower memory footprint).
      • Shared executor (control over callback scheduling and threading)
      • Shared in-process resources (TF buffers, hardware driver handles, CUDA/GPU contexts, ...)
LIDAR and camera messages

IPC provides no meaningful performance gain for small messages. However, you should ALWAYS enable IPC and follow the zero-copy rules above when working with images, point clouds, or other large payloads, as avoiding the copy and serialization steps dramatically reduces latency.

  • Safe Initialization

    • Callbacks can execute immediately upon creation, potentially accessing uninitialized variables if the setup isn't complete.
      • Best Practice: Always instantiate subscribers, timers, and service servers at the very end of your constructor or initialization method.
      • Safety Net: For complex initializations, use an std::atomic<bool> is_initialized_ flag. Set it to true only when setup is complete, and check it at the start of every callback.
  • Subscriptions Always check whether all subscribed messages are coming. If not, print a warning. Then you know the problem is not in your node and you know to look for the problem in topic remapping or the node publishing it.

    • Possible problems:
      • Wrong topic name (typo, missing remapping, etc.)
      • Publisher node not running or not publishing
      • QoS mismatch (e.g., publisher is using best_effort but subscriber is using reliable)
      • Callback group issues
        • Subscriber assigned to a Callback Group that isn't added to the Executor.
        • Subscriber sharing a MutuallyExclusive CallbackGroup with a blocking service call (causing a deadlock).
Separate Callback Groups

Can have separate callback group for subscribers and services to avoid deadlocks. For example, assign all subscribers to a cbkgrp_subs_ and all service servers to a cbkgrp_servers_, etc. and add the groups to the executor.


Message Interfaces​

  • Aim to reuse existing message interfaces, for example from the mrs_msgs package or other well-known packages in the ROS 2 ecosystem. Examples: geometry_msgs, sensor_msgs, nav_msgs, tf2_msgs etc. This promotes interoperability and reduces maintenance overhead. - If there isn't a viable existing message type that fits well for your use-case, create custom a message interface.
  • Custom message interfaces should be in their own packages with _msgs suffix. This allows easy message reusage without depending on the full package.
  • Avoid using primitive type messages from the std_msgs, such as Float32, Bool and String, as they are meant to be used only for quick prototyping. Instead, use a custom message with a semantic meaning.
Example
  • Bad: Publishing a Float32 on a topic named temperature without any context.
  • Good: Using sensor_msgs::msg::Temperature (standard) or defining a custom SystemStatus.msg that includes the temperature field.

Using MRS Lib wrappers​

Can find all the following in examples we provide using mrs_lib wrappers in template package

Node Creation​

Use the mrs_lib::Node instead of rclcpp::Node, which actually extends rclcpp::Node wrapper that allows you to access the node shared pointer in the constructor, enabling safe and more convenient initialization of subscribers, timers, and service servers, etc.

Loading Parameters​

Use mrs_lib::ParamLoader instead of native rclcpp interfaces.

  • Why: It simplifies loading from launch/config files and automatically verifies if parameters were successfully loaded, saving debugging time.
  • Bonus: Loading matrices into config files becomes much simpler.

Subscribing and Publishing​

Use mrs_lib::SubscribeHandler and mrs_lib::PublishHandler

  • They are more robust than native ROS 2 interfaces.
  • They come with default QoS profiles (can be modified if needed).

Services and Clients​

Use mrs_lib::ServiceServerHandler and mrs_lib::ServiceClientHandler.

  • ROS 2 changed from synchronous to asynchronous services, which can be more complex to handle as you need to manage futures and callbacks for responses.
  • These handlers simplify the process by providing intuitive interfaces for both synchronous and asynchronous calls.

Timers​

Use mrs_lib::ThreadTimer for periodic execution instead of sleep loops. This ensures your node remains responsive.

Avoid rclcpp::Timer

Native ROS 2 timers have known issues that can lead to high CPU usage. Always prefer mrs_lib::ThreadTimer.

Implementation Pattern: Define the timer type once to allow easy switching:

#if USE_ROS_TIMER == 1
typedef mrs_lib::ROSTimer TimerType;
#else
typedef mrs_lib::ThreadTimer TimerType; // Preferred
#endif

Logging​

  • Always use ROS 2 loggers instead of print() or std::cout.
  • Prioritize log levels appropriately:
    • INFO: Normal operation, logged for informational purposes.
    • WARN: Unexpected but recoverable, might require attention.
    • ERROR: Critical failure; system no longer operates correctly, requires immediate action.

Pro Tip: Avoid log spam in high-frequency callbacks! Use throttled logs (RCLCPP_INFO_THROTTLE) to keep output clean.