ROS 2 reference
Frames & tf2
Almost every robot bug that looks like 'the sensor is wrong' is actually a frame bug. tf2 is the subsystem that answers 'where is X relative to Y, right now', and when its tree is broken, everything downstream lies quietly.
What tf2 actually is
A robot is a pile of coordinate frames: map, odom, base_link, every wheel, every sensor. tf2 maintains the time-varying relationships between them as a tree, where each frame has exactly one parent. Ask it “where is the laser relative to the map at time t” and it walks the tree, composing transforms, and answers.
Two rules cause most of the pain. First, every frame has one and only one parent; two nodes both publishing a parent for base_link corrupt the tree. Second, transforms are timestamped, and a lookup at a time tf2 has no data for fails, even if the frames exist.
Static vs dynamic transforms
A static transform never changes: the laser is bolted 15 cm above base_link and stays there. Publish it once, on a latched topic, with static_transform_publisher or a fixed joint in the URDF. A dynamic transform moves: odom→base_link updates every time the robot drives. It must be published continuously, on time, or lookups go stale.
The failures, and their tells
- "Could not transform" / lookup would require extrapolation into the future
- The requested timestamp is newer than the latest transform tf2 has. Usually a publisher that stopped, lags, or a clock mismatch (sim time vs wall time).
- Frame X exists but is disconnected from the tree
- No transform chain links it to the root. A missing static publisher, a typo'd frame_id, or robot_state_publisher not running.
- Two parents for the same frame
- Two nodes both publish a parent for one child (e.g. both odometry and SLAM writing odom->base_link). The tree is no longer a tree.
- Sensor data appears rotated or mirrored
- A static transform with the wrong rotation, or a frame_id on the message that does not match the URDF. The data is fine; the frame lies.
- Everything drifts / jumps periodically
- map->odom correction fighting odom->base_link, or a use_sim_time mismatch between publisher and consumer.
Debugging a frame tree
See the whole tree
Before anything else, look at the graph. This writes a PDF of the entire tf tree with per-edge publish rates and the most recent timestamp, which immediately reveals disconnected frames and dead publishers.
ros2 run tf2_tools view_frames
# writes frames.pdf in the working directory
# List every frame tf2 currently knows about
ros2 run tf2_ros tf2_monitorAsk for one specific transform
tf2_echo is the workhorse. It prints the live transform between any two frames, or fails loudly with the exact reason, which is usually the whole diagnosis.
# Where is base_link relative to odom, live?
ros2 run tf2_ros tf2_echo odom base_link
# A missing chain prints:
# "Invalid frame ID ... does not exist"
# A timing problem prints:
# "lookup would require extrapolation into the past/future"Check the clock
When transforms exist but every lookup fails on time, suspect the clock. A node with use_sim_time wrong, mixed with one that has it right, means their timestamps never line up.
# Is a sim clock being published at all?
ros2 topic echo /clock --once
# What does a given node think use_sim_time is?
ros2 param get /robot_state_publisher use_sim_timeA real AMR frame bug
Here is one from a real warehouse robot, not a textbook. The vehicle localized off a downward-facing guide camera that reads position tags in the floor. It worked on the front camera. Switched to the rear camera for a docking approach, the robot became convinced it was driving the opposite direction from reality, and every position fix yanked its heading around.
The tags were read correctly. The camera was mounted rotated 180 degreesrelative to the robot’s base, so the angle it reported was the robot’s heading plus pi. Nothing in the transform tree encoded that mount rotation, so the localization node published a pose that was correct in position but backwards in yaw.
The fix was to account for the mount in the heading the node derived from the tag angle (the real commit literally added pi to the yaw and set a camera_mount_yaw of 180 degrees), and to publish a clean map→odom correction instead of fighting odom→base_link. A jump gate was also raised so a single bad fix could not teleport the estimate across the map.
# The derived heading ignored the 180-degree mount, so it was off by pi:
- robot_yaw = tag_angle - mount_yaw
+ robot_yaw = tag_angle - mount_yaw + math.pi # rear camera is mounted flipped
# And the mount was declared, instead of assumed zero:
- pgv_camera_mount_yaw_deg: 0
+ pgv_camera_mount_yaw_deg: 180The lesson is the one this whole page is about: the sensor was fine, the math was fine, the frame relationship was undeclared. Encode every mount in the transform tree and the bug cannot happen.
Fix it in a lab
The frame faults above are not hypothetical. Cases like a sensor whose scan lands on the wrong side, or a robot whose obstacles appear mirrored, are frame bugs you diagnose and repair in a live Gazebo session. Open the mobile-robot path and find the broken transform yourself.