What and What Not to do
You can find (and should read) general good coding practices in our documentation. We will mention only several important points:
ROS-Related coding practices
You can find more examples or specific examples for certain applications in ROS2 best practices section.
-
Components (formerly "nodelets" in ROS 1) enable zero-copy communication when running in the same process container. This eliminates serialization overhead by passing messages as pointers
-
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.
Use mrs_lib wrappers for ROS interfaces
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.
C++ specific
Memory Management
- Do not use raw pointers! Smart pointers from
<memory>free resources automatically, thus preventing memory leaks. - Avoid using references for
std::shared_ptrsince that subverts the reference counting. If the original instance goes out of scope and the reference is being used it accesses freed memory.
Thread Safety
- When using
MultiThreadedExecutororReentrantCallbackGroup, multiple callbacks may execute in parallel. Protect shared mutable state: If a member variable is read/written by multiple concurrent callbacks, you must protect it with a mutex (e.g.,std::lock_guard). Note: Read-only data (const) or variables accessed by only one thread do not require locking.
Use C++17 scoped_lock which unlocks the mutex after leaving the scope. This way, you can't forget to unlock the mutex.
Testing
- At minimum, write thorough unit tests for core application logic, using mocks where appropriate to isolate the tested functionality. If the application logic is well-separated from the ROS 2 nodes, the nodes themselves do not require unit tests.
- Test ROS 2 nodes, including their communication behavior, as part of integration testing.
- Aim for high test coverage (90-100%).
- Avoid using arbitrary sleeps in tests, as they can make tests non-deterministic and flaky. Instead of sleeping to wait for a ROS message, use synchronization mechanisms or wait for the expected result with a proper timeout.
When doing a PR, and tests are already available, make sure to run them before pushing your code. If you are adding new functionality, make sure to add tests for it and run them before pushing.
Do not push untested code to master branches on Git (or main devel branches)! Doing so can ruin experiments and drones at best!
General points
- Avoid hardcoding values like timeouts, control gains, or buffer sizes. Field conditions often require tuning these values on the fly.
- Rule of Thumb: If a number affects behavior, make it a ROS parameter loaded from
config.yaml. - Why: Recompiling C++ on a drone in freezing weather is miserable. Editing a config file is fast.
- Rule of Thumb: If a number affects behavior, make it a ROS parameter loaded from
If you cannot figure something out, ask in Software Google Spaces for help. If you figure something out that did not work before, note somewhere how you solved it and you can share it in chat. There is a high chance that you (or someone else) will have to do the same thing again.