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:
-
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_commsmust be enabled on both the publishing and subscribing nodes, either via the launch file or inNodeOptions.- The publisher must pass messages as
std::unique_ptrand give up ownership (zero-copy only applies on this path. Publishing byconst &orSharedPtrfalls back to copying). - The subscriber callback must take
ConstSharedPtrorUniquePtr.ConstSharedPtrshares one buffer across all subscribers (read-only).UniquePtrmoves ownership into the single subscriber. A mutableSharedPtrcallback forcesrclcppto copy, defeating zero-copy. - The QoS profiles of the publisher and the subscriber must be compatible.
// publisher nodeauto msg = std::make_unique<geometry_msgs::msg::Point>();pub.publish(std::move(msg)); -
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, ...)
-
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.
- Callbacks can execute immediately upon creation, potentially accessing uninitialized variables if the setup isn't complete.
-
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_effortbut subscriber is usingreliable) - Callback group issues
- Subscriber assigned to a Callback Group that isn't added to the Executor.
- Subscriber sharing a
MutuallyExclusive CallbackGroupwith a blocking service call (causing a deadlock).
- Possible problems:
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_msgsetc. 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
_msgssuffix. This allows easy message reusage without depending on the full package. - Avoid using primitive type messages from the
std_msgs, such asFloat32,BoolandString, as they are meant to be used only for quick prototyping. Instead, use a custom message with a semantic meaning.
- Bad: Publishing a
Float32on a topic namedtemperaturewithout any context. - Good: Using
sensor_msgs::msg::Temperature(standard) or defining a customSystemStatus.msgthat 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.
rclcpp::TimerNative 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()orstd::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.