Description
When use_sim_time=true, an RCL_ROS_TIME clock sources time from the /clock topic. When use_sim_time=false, it falls back to RCL_SYSTEM_TIME unconditionally. This fallback is hardcoded in rcl.
There is currently no native way to create a clock that uses /clock in simulation and RCL_STEADY_TIME (monotonic wall time) on real hardware. The only two options available are:
RCL_STEADY_TIME — monotonic but ignores sim time entirely
RCL_ROS_TIME — sim-time-aware, but falls back to RCL_SYSTEM_TIME (not RCL_STEADY_TIME) on real hardware
This is a problem for any node that needs different clock behaviors for different purposes within the same node. For example, a controller node typically needs:
| Use case |
Real hardware |
Simulation |
| Control loop timer |
RCL_STEADY_TIME (monotonic, NTP-immune) |
/clock |
| Message timestamps |
RCL_SYSTEM_TIME (wall clock) |
/clock |
Today, both of these are impossible to satisfy with a single RCL_ROS_TIME clock because the fallback is always RCL_SYSTEM_TIME. Using the node's clock for a control loop timer silently introduces NTP sensitivity on real hardware.
Motivation
This came up in ros-navigation/navigation2#6063, which makes various Nav2 components sim-time-aware by switching from rclcpp::WallRate to rclcpp::Rate(freq, node->get_clock()). This is the right thing to do for simulation, but it silently changes the real-hardware behavior from RCL_STEADY_TIME (monotonic) to RCL_SYSTEM_TIME (NTP-adjustable), because that's the only fallback RCL_ROS_TIME supports.
Current workaround
The only workaround is to check use_sim_time at construction and branch between create_timer and create_wall_timer:
class Controller : public rclcpp::Node
{
public:
Controller() : Node("controller")
{
if (get_parameter("use_sim_time").as_bool()) {
timer_ = create_timer(100ms, std::bind(&Controller::control_loop, this));
} else {
timer_ = create_wall_timer(100ms, std::bind(&Controller::control_loop, this));
}
}
void control_loop()
{
auto cmd = compute_velocity();
cmd.header.stamp = now();
pub_->publish(cmd);
}
};
This is fragile — use_sim_time is checked once at construction, so runtime parameter changes are ignored. Every node or library that needs monotonic + sim-aware timing must duplicate this pattern. The same issue applies to rclcpp::Rate and rclcpp::WallRate, where there is no equivalent workaround at all.
Question
What is the intended way to get sim-time-aware monotonic timing in rclcpp? Is there an existing mechanism we are missing, or does rclcpp need a way to configure the fallback clock type for RCL_ROS_TIME?
Description
When
use_sim_time=true, anRCL_ROS_TIMEclock sources time from the/clocktopic. Whenuse_sim_time=false, it falls back toRCL_SYSTEM_TIMEunconditionally. This fallback is hardcoded inrcl.There is currently no native way to create a clock that uses
/clockin simulation andRCL_STEADY_TIME(monotonic wall time) on real hardware. The only two options available are:RCL_STEADY_TIME— monotonic but ignores sim time entirelyRCL_ROS_TIME— sim-time-aware, but falls back toRCL_SYSTEM_TIME(notRCL_STEADY_TIME) on real hardwareThis is a problem for any node that needs different clock behaviors for different purposes within the same node. For example, a controller node typically needs:
RCL_STEADY_TIME(monotonic, NTP-immune)/clockRCL_SYSTEM_TIME(wall clock)/clockToday, both of these are impossible to satisfy with a single
RCL_ROS_TIMEclock because the fallback is alwaysRCL_SYSTEM_TIME. Using the node's clock for a control loop timer silently introduces NTP sensitivity on real hardware.Motivation
This came up in ros-navigation/navigation2#6063, which makes various Nav2 components sim-time-aware by switching from
rclcpp::WallRatetorclcpp::Rate(freq, node->get_clock()). This is the right thing to do for simulation, but it silently changes the real-hardware behavior fromRCL_STEADY_TIME(monotonic) toRCL_SYSTEM_TIME(NTP-adjustable), because that's the only fallbackRCL_ROS_TIMEsupports.Current workaround
The only workaround is to check
use_sim_timeat construction and branch betweencreate_timerandcreate_wall_timer:This is fragile —
use_sim_timeis checked once at construction, so runtime parameter changes are ignored. Every node or library that needs monotonic + sim-aware timing must duplicate this pattern. The same issue applies torclcpp::Rateandrclcpp::WallRate, where there is no equivalent workaround at all.Question
What is the intended way to get sim-time-aware monotonic timing in rclcpp? Is there an existing mechanism we are missing, or does rclcpp need a way to configure the fallback clock type for
RCL_ROS_TIME?