Localization & SLAM · SLAM with ROS 2 · M7-L2 · ROS 2 project · Intermediate
Fix a TF Configuration Error
Three small naming and ownership mistakes, one broken tree. Learn to read the tree, then repair it.
Repair a broken TF tree (duplicate parents, wrong frame names, missing static transform).
The problem
The robot localises in the lab, but in this project’s launch file the map is empty and RViz is full of red “transform” errors. The robot icon shivers in place.
You only have the TF tools. The task is to find who publishes what, and to restore the REP 105 tree of M7-T2.
Expected behaviour. One connected tree map → odom → base_footprint → base_link → base_scan, every frame with a single parent, the lidar frame equal to the one in the scan messages, and a map that builds.
Observable failure. RViz shows “Frame [map] does not exist” or “Could not transform”; tf2_echo odom base_footprint alternates between two values; SLAM Toolbox logs that it cannot transform the scan.
This is a real ROS 2 project, not a browser lab. You run it on your own machine with ROS 2, a simulator and the tools of Module 7. The website does not run ROS 2 and does not grade it; you grade yourself with the criteria below and the
check.shscript.
Time: about 60 to 90 minutes · Level: Intermediate · Theory: M7-T2 TF Frames and Coordinate Consistency
Prerequisites and setup
You need a computer with ROS 2 Humble on Ubuntu 22.04 (a virtual machine or container with a display is fine). Jazzy works for the SLAM and Nav2 parts; the simulator launch differs, and the project notes say where. Install once:
sudo apt update sudo apt install ros-humble-turtlebot3-gazebo ros-humble-turtlebot3-teleop ros-humble-slam-toolbox \ ros-humble-navigation2 ros-humble-nav2-bringup ros-humble-tf2-tools ros-humble-tf2-ros ros-humble-rviz2 source /opt/ros/humble/setup.bash export TURTLEBOT3_MODEL=burger
Get the project files: ros2_labs/l2-fix-a-tf-configuration-error in the site’s repository (clone it, or copy the files listed below from this page). The folder ros2_labs/common holds scripts shared by several labs.
- Terminal 1:
export TURTLEBOT3_MODEL=burger, thenros2 launch launch/lab2.launch.py. - Terminal 2:
ros2 run turtlebot3_teleop teleop_keyboard; drive a little. - Terminal 3: debugging. Start RViz with
rviz2and set the fixed frame toodom, thenmap, and watch the errors.
Verification status. The project files follow the documented Humble interfaces but have not been executed by the site’s automated tests (they cannot run ROS 2). If a command or parameter is rejected by your version, the error names it: treat that as part of the exercise and tell us.
The project files
The launch file starts the simulator, a relay that republishes the lidar as /scan_relay with a configurable frame, a fault injector for TF, and SLAM Toolbox. You fix the problems with launch arguments and parameters; you do not need to edit the Python nodes.
launch/lab2.launch.py
"""Lab M7-L2: the scan arrives with the wrong frame, a second node publishes odom -> base_footprint, and the SLAM map frame is misnamed.
Run: ros2 launch launch/lab2.launch.py
Launch arguments (change them as part of the fix): scan_frame, duplicate_odom
"""
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
here = os.path.dirname(os.path.realpath(__file__))
common = os.path.join(here, '..', '..', 'common')
params = os.path.join(here, '..', 'config', 'slam_params.yaml')
tb3 = os.path.join(get_package_share_directory('turtlebot3_gazebo'), 'launch', 'turtlebot3_world.launch.py')
return LaunchDescription([
DeclareLaunchArgument('scan_frame', default_value='laser'),
DeclareLaunchArgument('duplicate_odom', default_value='true'),
IncludeLaunchDescription(PythonLaunchDescriptionSource(tb3)),
Node(executable='python3', arguments=[os.path.join(common, 'scan_relay.py')], name='scan_relay', output='screen',
parameters=[{'use_sim_time': True, 'frame_id': LaunchConfiguration('scan_frame')}]),
Node(executable='python3', arguments=[os.path.join(here, '..', 'tf_faults.py')], name='tf_faults', output='screen',
parameters=[{'use_sim_time': True, 'duplicate_odom': LaunchConfiguration('duplicate_odom')}]),
Node(package='slam_toolbox', executable='async_slam_toolbox_node', name='slam_toolbox', output='screen', parameters=[params]),
])
tf_faults.py
#!/usr/bin/env python3
"""Fault injector for lab M7-L2: publishes a second, wrong odom -> base_footprint transform when duplicate_odom is true."""
import rclpy
from rclpy.node import Node
from tf2_ros import TransformBroadcaster
from geometry_msgs.msg import TransformStamped
class TfFaults(Node):
def __init__(self):
super().__init__('tf_faults')
self.declare_parameter('duplicate_odom', True)
self.br = TransformBroadcaster(self)
self.create_timer(0.05, self.tick)
def tick(self):
if not self.get_parameter('duplicate_odom').value:
return
t = TransformStamped()
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = 'odom'
t.child_frame_id = 'base_footprint'
t.transform.translation.x = 0.3 # a visibly wrong pose, so the conflict shows even when the robot is still
t.transform.rotation.w = 1.0
self.br.sendTransform(t)
def main():
rclpy.init()
rclpy.spin(TfFaults())
if __name__ == '__main__':
main()
config/slam_params.yaml
# SLAM Toolbox parameters for lab M7-L2.
slam_toolbox:
ros__parameters:
solver_plugin: solver_plugins::CeresSolver
ceres_linear_solver: SPARSE_NORMAL_CHOLESKY
ceres_preconditioner: SCHUR_JACOBI
ceres_trust_strategy: LEVENBERG_MARQUARDT
ceres_dogleg_type: TRADITIONAL_DOGLEG
ceres_loss_function: None
use_sim_time: true
odom_frame: odom
map_frame: world
base_frame: base_footprint
scan_topic: /scan_relay
mode: mapping
debug_logging: false
throttle_scans: 1
transform_publish_period: 0.02
map_update_interval: 5.0
resolution: 0.05
max_laser_range: 3.5
minimum_time_interval: 0.5
transform_timeout: 0.2
tf_buffer_duration: 30.
stack_size_to_use: 40000000
use_scan_matching: true
use_scan_barycenter: true
minimum_travel_distance: 0.3
minimum_travel_heading: 0.3
do_loop_closing: true
loop_search_maximum_distance: 3.0
loop_match_minimum_response_coarse: 0.35
loop_match_minimum_response_fine: 0.45
What to do
Follow the loop of this track: Problem → Experiment → Diagnose → Fix → Evaluate → Understand.
- Experiment. Launch and drive. Run
ros2 run tf2_tools view_frames, open the .gv file or the PDF, and note the frames and their parents. - Diagnose with the checker. Paste the edges of the tree into the tool below, with the base frame
base_footprintand the scan frame fromros2 topic echo /scan_relay --once --field header.frame_id. Read every finding. - Diagnose the duplicate. Run
ros2 run tf2_ros tf2_echo odom base_footprintfor a few seconds. What do you see? Thenros2 topic info /tf -vto list the publishers and decide which one to remove. - Fix. Use launch arguments (
scan_frame:=…,duplicate_odom:=…) and edit one line of the SLAM parameters. Restart after each change. - Evaluate. Run
./check.shwhile driving a little. Also confirm in RViz (fixed framemap) that the scan lines up with the walls of the map. - Understand. Draw the final tree on paper, annotating who publishes each link. Why does
map → odom, notmap → base_footprint, come from SLAM?
Pre-flight tool
TF tree checker
Paste one edge per line as parent -> child, or the contents of the frames_*.gv file written by view_frames. Optionally name the SLAM base frame and the frame of the scan messages.
This tool only reads the text you paste. It does not connect to your robot, run ROS 2 or send anything anywhere.
Expected output
Typical output when things are right. Exact numbers, timestamps and wording differ between machines and versions.
A correct tree
map -> odom -> base_footprint -> base_link -> base_scan (and base_link -> imu_link, wheel_left_link, wheel_right_link ...)
A steady transform
ros2 run tf2_ros tf2_echo odom base_footprint At time 123.456 - Translation: [0.000, 0.000, 0.000] <- steady while the robot is still, then smooth
What the duplicate looks like
At time 123.456 - Translation: [0.300, 0.000, 0.000] At time 123.476 - Translation: [0.000, 0.000, 0.000] <- two publishers alternating
check.sh when everything is fixed
PASS only one node publishes odom -> base_footprint PASS transform map -> odom exists PASS transform map -> base_footprint exists PASS the scan frame is connected to base_footprint 4 passed, 0 failed
Troubleshooting
| Symptom | Likely cause | What to try |
|---|---|---|
view_frames says “Could not get transform” | It needs a few seconds of TF data; or the node cannot see /tf | Wait and retry; check the domain id and the sim time |
| The PDF viewer is unavailable | No PDF tools on the machine | Read the .gv text file instead: the tool above accepts it |
After setting scan_frame the map is still empty | The map frame is still misnamed, or the duplicate publisher remains | Run the checker again; read the SLAM log line by line |
| RViz still shows errors after the fix | Old displays hold on to an old frame; the fixed frame is wrong | Reset RViz, set the fixed frame to map |
tf2_echo prints nothing | The two frames are not connected | This is the disconnected-tree symptom: use the checker |
Evaluation criteria
You are done when every criterion passes. check.sh automates most of them (run it from the project folder, with the launch file running and ROS 2 sourced). The remaining ones you judge yourself.
| Criterion | How to check | Passes when |
|---|---|---|
Exactly one publisher of odom → base_footprint | tf2_echo is steady; check.sh | no repeated large jumps (automated) |
The map frame is called map | tf2_echo map odom | a transform is printed (automated) |
| The scan frame is attached to the robot | /scan_relay frame id equals base_scan | the transform exists and the id matches (automated) |
| The map builds and the scan matches the walls | RViz with fixed frame map | straight walls, no ghost copies (self-assessed) |
| You can explain why SLAM publishes map → odom | your notes from task 6 | one correct paragraph (self-assessed) |
cd ros2_labs/l2-fix-a-tf-configuration-error ./check.sh
Worked explanation
Try the lab before opening this.
Show the diagnosis and the fix
Three faults, three fixes.
- Wrong scan frame. The relay overwrote
header.frame_idwithlaser, a frame that does not exist in the tree. The checker reports it. Fix:scan_frame:=base_scan. - Duplicate
odom → base_footprint. The fault injector publishes a second transform with a fixed offset of 0.3 m while Gazebo’s differential-drive plugin publishes the real one.tf2_echoalternates andros2 topic info /tf -vlists the extra publisher (tf_faults). Fix:duplicate_odom:=false. - Misnamed map frame.
map_frame: worldin the parameters: SLAM publishesworld → odom, so there is nomapframe for RViz or Nav2. Fix:map_frame: map.
Why it happens
A TF tree is a contract: one parent per frame, one publisher per link, names identical everywhere (M7-T2). None of the faults is a bug in an algorithm; each is a violation of the contract, and each has a different signature in the tools: a missing frame in the checker, an alternating value in tf2_echo, a parent that is not the one you expected in view_frames. SLAM publishes map → odom because base_footprint already has the parent odom, and the correction is carried by the link above.
What this project does not cover
- The simulator’s extrinsics are right. In practice a wrongly mounted sensor gives a correct tree and a wrong map; look at the scan in
base_footprintto catch it. - Only one robot and no namespaces; multi-robot systems add frame prefixes.
- The project files have not been run by the site’s tests.
What’s next
Other projects in this module
- M7-L1: Debug a ROS 2 SLAM Pipeline
- M7-L2: Fix a TF Configuration Error: this project.
- M7-L3: Diagnose Sensor Timestamp Problems
- M7-L4: Configure Mapping and Localization
- M7-L5: Debug SLAM-to-Nav2 Integration