Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Use simulated time

How-to · Level 3: Production and tuning

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 writeIt advances withFollows /clock
create_wall_timer(period, cb)the platform’s monotonic clockno
create_timer(clock, period, cb)the clock you passyes, 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 /clock source: a simulator, or ros2 bag play --clock.

Steps

  1. Enable the feature in a Rust node package. The /clock subscription 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",
    ] }
    
  2. 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_time itself; nothing in your code reads it:

    talker:
      ros__parameters:
        use_sim_time: true
    

    An image built with parameter services declares use_sim_time = false for every node, as rclcpp does, so this works on a node whose author never mentioned simulated time:

    ros2 param set /talker use_sim_time true
    

    A declaration of your own still wins. To attach the source in code instead, call node.install_ros_time_source()?, or node.install_ros_time_source_on("/sim/clock")? for a remapped clock.

  3. 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));
  1. Read the time. A node’s clock is ROS time, and with no /clock source it reads system time, as rclcpp::Clock does, 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?
    
  2. 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, CatchUp replays 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_callback and rcl_jump_threshold_t have no counterpart.
  • No wait_until_started or sleep_until. Poll started() 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 /clock looks 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 turn use_sim_time off. The warning comes once; Executor::sim_time_silence_warnings() answers the same question in code.

  • Message ages are enormous. The max-age-runtime rule compares header.stamp with the epoch clock, not ROS time. Stamp from the epoch clock, or leave that rule off for the topic under simulation.

See also

Design background