Use simulated time
Needs first: Parameters and launch files
Use simulated time when a node runs against a simulator or a replayed bag, and its timers should follow the simulation’s clock instead of the wall.
nano-ros implements ROS 2’s mechanism: rosgraph_msgs/msg/Clock on /clock,
the use_sim_time parameter, and timers that state which clock advances
them. As in rclcpp, the method name carries the choice:
| You write | It advances with | Follows /clock |
|---|---|---|
create_wall_timer(period, cb) | the platform’s monotonic clock | no |
create_timer(clock, period, cb) | the clock you pass | yes, for a ROS-time clock |
Keep a wall timer for a watchdog or a keep-alive, anything whose job is to
notice that real time passed; a paused simulator must not pause it. Older
nano-ros code used create_timer for the wall timer. It no longer means that,
and there is no alias.
Before you start
- A node package whose timers should follow the simulation.
- A
/clocksource: a simulator, orros2 bag play --clock.
Steps
-
Enable the feature in a Rust node package. The
/clocksubscription costs an entity slot and a receive buffer, so it is opt-in:nros = { version = "*", default-features = false, features = [ "alloc", "rmw-cffi", "macros", "sim-time", ] } -
Turn it on with the parameter, as in ROS 2: from a launch file, a parameters file, or at run time. The runtime acts on
use_sim_timeitself; nothing in your code reads it:talker: ros__parameters: use_sim_time: trueAn image built with parameter services declares
use_sim_time = falsefor every node, as rclcpp does, so this works on a node whose author never mentioned simulated time:ros2 param set /talker use_sim_time trueA declaration of your own still wins. To attach the source in code instead, call
node.install_ros_time_source()?, ornode.install_ros_time_source_on("/sim/clock")?for a remapped clock. -
Write the timers that should follow it on the ROS clock:
#![allow(unused)]
fn main() {
executor.register_timer_on_clock(
TimerDuration::from_millis(100),
TimerClockSource::Ros,
|| { /* … */ },
)?;
}
TimerClockSource::Steady is the default and costs nothing extra: it uses
the spin delta the executor already measured. Ros and System read their
clock on every poll, a platform call on a board, which is why they have their
own entry point.
rclcpp::Timer t;
RCLCPP_TRY(node.create_timer(t, *node.get_clock(), 100, on_tick));
-
Read the time. A node’s clock is ROS time, and with no
/clocksource it reads system time, asrclcpp::Clockdoes, so the node still runs standalone:rclcpp::Time now = node.get_clock()->now(); node.get_clock()->ros_time_is_active(); // is a /clock source driving it? node.get_clock()->started(); // has a first sample arrived? -
Start the source and run the node:
ros2 bag play my_bag --clock
Example
A talker that publishes every 100 ms of simulated time, switched on from its parameters file:
# config/talker.param.yaml
talker:
ros__parameters:
use_sim_time: true
#![allow(unused)]
fn main() {
executor.register_timer_on_clock(
TimerDuration::from_millis(100),
TimerClockSource::Ros,
|| { /* publish */ },
)?;
}
ros2 bag play my_bag --clock
What happens at the edges
- Paused: the delta is zero, so a ROS-time timer does not fire. Wall timers on the same executor keep their cadence.
- Replayed at 0.5× or 2×: the timer tracks the rate, because it advances by the clock’s own delta.
- A jump backwards (a bag looping, a simulator reset) restarts the period.
- A jump forwards lands as a backlog, and the timer overrun policy
decides:
Skip(the default) fires once,CatchUpreplays every period. - Unsubscribing keeps the last simulated time rather than snapping back
to the wall. Clear it on purpose with
Clock::clear_ros_time_override().
Where nano-ros differs from rclcpp
- One simulated clock per image. The override is process-wide; two nodes in one image cannot be on different simulated times.
- No jump callbacks.
create_jump_callbackandrcl_jump_threshold_thave no counterpart. - No
wait_until_startedorsleep_until. Pollstarted()from your spin loop. - Timers only. Message delivery, QoS deadlines and transport leases stay on the monotonic clock, so a link stays up while the simulation is paused.
If it doesn’t work
-
ROS-time timers never fire. A never-connected
/clocklooks exactly like a paused one. Five seconds after the source attaches with no sample, the image logs one warning:use_sim_time is true but no /clock sample has arrived in 5s: ROS-time timers are running on SYSTEM time meanwhile. Publish rosgraph_msgs/msg/Clock (ros2 bag play --clock), or set use_sim_time false.Publish
/clock, or turnuse_sim_timeoff. The warning comes once;Executor::sim_time_silence_warnings()answers the same question in code. -
Message ages are enormous. The
max-age-runtimerule comparesheader.stampwith the epoch clock, not ROS time. Stamp from the epoch clock, or leave that rule off for the topic under simulation.
See also
- Pick a scheduling model: the timer overrun policy.
- ROS 2 clock and time: the upstream design.