Skip to the project
FixTheRobot Explore Robotics / Localization & SLAM / M7 / M7-L2

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.sh script.

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.

  1. Terminal 1: export TURTLEBOT3_MODEL=burger, then ros2 launch launch/lab2.launch.py.
  2. Terminal 2: ros2 run turtlebot3_teleop teleop_keyboard; drive a little.
  3. Terminal 3: debugging. Start RViz with rviz2 and set the fixed frame to odom, then map, 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.

  1. 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.
  2. Diagnose with the checker. Paste the edges of the tree into the tool below, with the base frame base_footprint and the scan frame from ros2 topic echo /scan_relay --once --field header.frame_id. Read every finding.
  3. Diagnose the duplicate. Run ros2 run tf2_ros tf2_echo odom base_footprint for a few seconds. What do you see? Then ros2 topic info /tf -v to list the publishers and decide which one to remove.
  4. Fix. Use launch arguments (scan_frame:=…, duplicate_odom:=…) and edit one line of the SLAM parameters. Restart after each change.
  5. Evaluate. Run ./check.sh while driving a little. Also confirm in RViz (fixed frame map) that the scan lines up with the walls of the map.
  6. Understand. Draw the final tree on paper, annotating who publishes each link. Why does map → odom, not map → 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

    Troubleshooting guide
    SymptomLikely causeWhat to try
    view_frames says “Could not get transform”It needs a few seconds of TF data; or the node cannot see /tfWait and retry; check the domain id and the sim time
    The PDF viewer is unavailableNo PDF tools on the machineRead the .gv text file instead: the tool above accepts it
    After setting scan_frame the map is still emptyThe map frame is still misnamed, or the duplicate publisher remainsRun the checker again; read the SLAM log line by line
    RViz still shows errors after the fixOld displays hold on to an old frame; the fixed frame is wrongReset RViz, set the fixed frame to map
    tf2_echo prints nothingThe two frames are not connectedThis 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.

    Evaluation criteria
    CriterionHow to checkPasses when
    Exactly one publisher of odom → base_footprinttf2_echo is steady; check.shno repeated large jumps (automated)
    The map frame is called maptf2_echo map odoma transform is printed (automated)
    The scan frame is attached to the robot/scan_relay frame id equals base_scanthe transform exists and the id matches (automated)
    The map builds and the scan matches the wallsRViz with fixed frame mapstraight walls, no ghost copies (self-assessed)
    You can explain why SLAM publishes map → odomyour notes from task 6one 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.

    1. Wrong scan frame. The relay overwrote header.frame_id with laser, a frame that does not exist in the tree. The checker reports it. Fix: scan_frame:=base_scan.
    2. 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_echo alternates and ros2 topic info /tf -v lists the extra publisher (tf_faults). Fix: duplicate_odom:=false.
    3. Misnamed map frame. map_frame: world in the parameters: SLAM publishes world → odom, so there is no map frame 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_footprint to 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

    Next

    M7-L3: Diagnose Sensor Timestamp Problems

    Find clock, stamp and QoS problems that make SLAM drop scans or jump.