Docs Architecture Workspace repo
Docs menu

QoS and timing

The delivery settings each topic is published with, which subscriber settings can receive it, and the rates, timeouts and delays the driving code is built around.

Every ROS 2 publisher and subscriber carries a quality of service (QoS) profile: rules for how messages are delivered. When a subscriber’s rules ask for something the publisher does not offer, ROS connects nothing and the subscriber receives no messages, with no error by default. This page lists the profiles the workspace’s own nodes use, and the timing the driving code depends on.

The three settings that matter here

Setting Options Meaning
Reliability RELIABLE or BEST_EFFORT Reliable retries a lost message; best effort sends each once and moves on. Sensor drivers often use best effort, because a late scan is worth less than the next one.
Durability VOLATILE or TRANSIENT_LOCAL Transient local keeps the last messages for a subscriber that joins later, which ROS 1 called “latched”. Volatile delivers only what is published after a subscriber connects.
History and depth KEEP_LAST with a depth How many messages wait in the queue. A depth of 1 keeps only the newest.

create_publisher(Type, topic, 10) in Python means reliable, volatile, keep the last 10. Most of the workspace uses exactly that.

Which combinations connect

The publisher must offer at least what the subscriber requests:

Publisher Subscriber Connects?
Reliable Reliable Yes
Reliable Best effort Yes
Best effort Best effort Yes
Best effort Reliable No. The subscriber receives nothing.
Transient local Transient local Yes, and a late subscriber gets the stored messages
Transient local Volatile Yes, but a late subscriber gets nothing stored
Volatile Transient local No. The subscriber receives nothing.

The two “No” rows are the ones that cause silent failures: the topic appears in ros2 topic list, the publisher is running, and the callback never fires.

Profiles in the workspace

These are read from each node’s create_publisher and create_subscription calls. “Default” means reliable, volatile, depth 10.

Topic Publisher, profile Subscribers, profile
/drive gap_follow_node, pure_pursuit_node, neural_racer_node (with shadow:=false): default ackermann_mux: system default (reliable, volatile)
/teleop joy_teleop ackermann_mux: system default
/ackermann_cmd ackermann_mux: reliable, volatile, depth 1 ackermann_to_vesc_node: default
/commands/motor/speed, /commands/servo/position ackermann_to_vesc_node: default vesc_driver_node: default
/sensors/core, /sensors/imu/raw vesc_driver_node: default vesc_to_odom_node, imu_bridge_node: default. odom_calibration: best effort.
/odom vesc_to_odom_node: default Driving nodes and imu_bridge_node: default. vehicle_state_node: best effort (sensor data). Particle filter: depth 1.
/drive_intent Each driving node: default The dashboard
/racing_line pure_pursuit_node: transient local, reliable, depth 1 The dashboard
/auto_map_race/controller, /auto_map_race/localization_source, /auto_map_race/frozen_map auto_map_race_node: transient local, reliable, depth 1 pure_pursuit_node (the last two) and neural_racer_node (the frozen map): transient local. The dashboard reads the controller.
/map slam_toolbox or the map server pure_pursuit_node: transient local, reliable, depth 1
/imu/status imu_bridge_node: transient local, reliable, depth 1 neural_racer_node: transient local
/imu/data, /imu/*/data imu_bridge_node: default vehicle_state_node, neural_racer_node: default
/pf/viz/inferred_pose Particle filter: depth 1 pure_pursuit_node: default

The topics marked transient local are the ones that change rarely and must reach a node that starts late: a map published once, the racing line, which localizer is in charge, the IMU’s health. Each subscriber to them asks for transient local too, so it receives the stored message at once instead of waiting for the next one, which may never come.

/scan (from urg_node), /joy (from joy_node) and /map (from slam_toolbox) come from packages installed with apt, not from the workspace, so their profiles are not listed here. Read them on the car, as below.

Check a topic’s profile

ros2 topic info /scan --verbose

This lists every publisher and subscriber of the topic with its full profile: Reliability, Durability, History (Depth). A subscriber whose callback never fires, on a topic whose publisher is running, is usually one of the two incompatible rows above.

In a new node

Subscribe with the publisher’s own profile. For sensor and command topics, create_subscription(Type, topic, callback, 10) matches the workspace’s publishers. For a latched topic, copy the publisher’s QoSProfile, as pure_pursuit_node does for /map:

map_qos = QoSProfile(depth=1,
                     reliability=QoSReliabilityPolicy.RELIABLE,
                     durability=QoSDurabilityPolicy.TRANSIENT_LOCAL)
self.map_sub = self.create_subscription(
    OccupancyGrid, self.map_topic, self.map_callback, map_qos)

Rates

Signal Rate Source
/scan About 40 Hz The Hokuyo’s scan rate
/drive from gap_follow One per scan, about 40 Hz Published from the scan callback
/drive from pure_pursuit 40 Hz control_rate_hz, a timer
/sensors/core, /odom 50 Hz The VESC’s state reports
/joy 20 Hz autorepeat_rate in joy_teleop.yaml, which repeats the last state even when nothing changes
/imu/bno/data About 250 Hz The BNO085, car 1
/imu/vesc/data About 50 Hz The VESC IMU
/drive_intent At most 20 Hz intent_rate_hz
/steering/measured 50 Hz steering_rate_hz

At 40 Hz, each control tick has 25 ms. A node that takes longer than that falls behind its own input.

Timeouts

Every safety check in the driving code is a timeout on the age of some input. A message counts as stale once it is this old:

What Timeout Where What happens
/joy, for the LB check 0.5 s Every driving node, joy_timeout_sec Speed 0
/scan 0.5 s gap_follow, pure_pursuit gap_follow stops publishing; pure_pursuit stops
/odom 0.5 s gap_follow (for TTC), pure_pursuit (for the lookahead) gap_follow stops; pure_pursuit uses the profiled speed instead
The pose 0.5 s pure_pursuit, pose_timeout_sec Stop
/drive and /teleop at the mux 0.2 s mux.yaml The input is ignored; the mux forwards the other one, or nothing
Scan, odometry, IMU, pose 75 ms, 0.1 s, 0.1 s, 0.2 s neural_racer Speed 0

The mux’s 0.2 s is why teleop_launch.py blocks autonomy completely: joy_teleop repeats at 20 Hz, a message every 50 ms, so /teleop never goes 0.2 s without a message and its priority of 100 always wins over /drive’s 10. When both inputs go quiet, the mux publishes nothing, not a zero, so the motor keeps its last command until the VESC’s own command timeout stops it.

Delays

A command reaches the wheels some time after the scan it was computed from. The workspace’s controllers each model part of that delay, from measurements on the car or in the calibrated simulator:

Delay Value Measured as Used by
Steering loop: servo lag plus one scan period About 0.175 s On the car, from run logs gap_follow’s speed-scheduled steering gain
Command to wheels, for braking 0.25 s (braking_latency_sec); about 0.3 s measured On the car gap_follow’s and neural_racer’s braking caps
Pose age, transport and servo About 0.1 s The simulator’s model of the car Stanley’s, LQR’s and MPC’s delay compensation

See Follow the gap for how the steering delay limits the gain, and Stanley for how the steering laws predict ahead to cancel it.

Measure it on the car

ros2 topic hz /scan
ros2 topic delay /scan

ros2 topic hz gives the rate and its jitter. ros2 topic delay compares each message’s header timestamp with the time it arrived, which shows how old messages are by the time a subscriber sees them.

Limitations

  • The profiles of urg_node, joy_node and slam_toolbox are not listed, because their code is not in the workspace.
  • The VESC’s own command timeout is set in VESC Tool, not in the workspace, and is not recorded here.

Checked against car 1 commit 0265263 and car 2 commit b81b291, before the two workspaces merged.