Skip to main content

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:

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.
  • 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.

Use mrs_lib wrappers for ROS interfaces

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.

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_ptr since 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 MultiThreadedExecutor or ReentrantCallbackGroup, 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.
Scoped lock

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.
Best Practice

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.

Always test

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.
Support and Troubleshooting :D

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.