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 --verboseThis 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 /scanros2 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_nodeandslam_toolboxare 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.