[ROS2 Mastery #10] TF2 Programming – The Complete Guide to Creating a Static Broadcaster

Hello and welcome, university students and researchers who are deeply exploring the world of autonomous driving and robotic system control. We are delighted to welcome you to Part 10 of the “ROS2 Mastery Series,” based on educational materials from Quad Drone Lab.

In Parts 8 and 9, we explored the core principles of TF2 (Transform System), a powerful framework that simplifies the mathematical complexity of robotics. We also used terminal commands and visualization tools to directly observe 3D transformations for both single coordinate frames and moving robot models.

While the static_transform_publisher command we previously used is extremely convenient, professional robotics research and development require the ability to directly create, manage, and publish coordinate transformation data within your own Python or C++ code. This programming-based approach becomes essential when loading sensor calibration parameters for devices such as LiDARs and cameras, or when dynamically configuring reference frames within complex robotic systems.

In this tenth installment, we will build a Static Broadcaster node from scratch using Python and learn how to publish a static coordinate frame directly into the ROS2 network, covering every step from A to Z.


Package Creation and Setup (learning_tf2_py)

The first step is to create a new ROS2 package that will contain our TF2-related nodes. For both this tutorial and the next one (which will cover dynamic frames and listeners), we will use a common package named learning_tf2_py.

Open a terminal, navigate to the src directory inside your ROS2 workspace, and execute the following commands:

Bash
# Source the ROS2 environment
source /opt/ros/jazzy/setup.bash

# Navigate to the src directory of your workspace
cd ~/ros2_ws/src

After executing the command, you should see a confirmation message indicating that the learning_tf2_py package has been successfully created, along with the essential files (package.xml, setup.py, etc.) and directory structure required for ROS2 package development.


Writing the Static Broadcaster Node Code

Once the package has been created, navigate to the directory where the node source code will reside (~/ros2_ws/src/learning_tf2_py/learning_tf2_py) and create a Python file named static_turtle_tf2_broadcaster.py.

Bash
cd ~/ros2_ws/src/learning_tf2_py/learning_tf2_py
touch static_turtle_tf2_broadcaster.py
chmod +x static_turtle_tf2_broadcaster.py

Now, open the newly created Python file in your preferred text editor, such as VS Code, and enter the code shown below. It is highly recommended that you carefully read through the comments and type the code yourself to better understand each step of the implementation.

Python
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node

# Import the TF2 message type and broadcaster library required for transformations.
from geometry_msgs.msg import TransformStamped
from tf2_ros.static_transform_broadcaster import StaticTransformBroadcaster

class StaticFramePublisher(Node):
    def __init__(self):
        # 1. Initialize the node with the name 'static_turtle_tf2_broadcaster'.
        super().__init__('static_turtle_tf2_broadcaster')

        # 2. Create a static transform broadcaster object.
        self.tf_static_broadcaster = StaticTransformBroadcaster(self)

        # 3. Call the function that publishes the static transform once when the node starts.
        self.make_transforms()

    def make_transforms(self):
        # 4. Create a TransformStamped object, which serves as the message template
        #    for publishing transformations to the TF tree.
        t = TransformStamped()

        # 5. Configure header metadata.
        # Set the current timestamp using the node's clock.
        t.header.stamp = self.get_clock().now().to_msg()

        # Set the parent frame (reference frame) name to 'world'.
        t.header.frame_id = 'world'

        # Set the child frame (new coordinate frame) name to 'mystaticturtle'.
        t.child_frame_id = 'mystaticturtle'

        # 6. Define the translation component of the 6D pose.
        # Position the turtle 1 meter above the origin along the Z-axis.
        t.transform.translation.x = 0.0
        t.transform.translation.y = 0.0
        t.transform.translation.z = 1.0

        # 7. Define the rotation component of the 6D pose using a quaternion.
        # Set an identity quaternion (no rotation).
        t.transform.rotation.x = 0.0
        t.transform.rotation.y = 0.0
        t.transform.rotation.z = 0.0
        t.transform.rotation.w = 1.0

        # 8. Broadcast the completed transform message.
        self.tf_static_broadcaster.sendTransform(t)
        self.get_logger().info(
            'Successfully published static frame transform: world -> mystaticturtle'
        )

def main(args=None):
    rclpy.init(args=args)
    node = StaticFramePublisher()

    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

💡 In-Depth Code Analysis for Researchers

Understanding frame transformation code thoroughly is essential for robotic system control and development. Let’s break down the code into its core components and examine how each part works.

  1. TransformStamped Message: Defined in geometry_msgs/msg, this is the most fundamental data structure used within the TF tree. This message contains all the information required to describe a transformation, including when (stamp), which frame serves as the reference (frame_id), which frame is being defined (child_frame_id), and how much translation and rotation should be applied (transform).
  2. StaticTransformBroadcaster: This is a dedicated broadcaster provided by the tf2_ros package rather than a standard ROS2 Publisher. When data is published through this broadcaster, it is internally transmitted via the /tf_static topic. One major advantage of ROS2 static transforms is that, unlike dynamic transforms published on /tf, they only need to be sent once when the node starts. After that, the transform remains permanently available throughout the ROS2 network without requiring periodic retransmission.
  3. Quaternion-Based Rotation: Instead of using intuitive Euler angles such as Roll, Pitch, and Yaw, TF2 uses quaternion values consisting of x, y, z, and w. In the code above, the quaternion [0.0, 0.0, 0.0, 1.0] represents an Identity Quaternion, meaning that no rotation is applied at all. To prevent gimbal lock issues, all TF2 transformations in ROS2 use quaternions as the default format for representing orientation.


Updating Configuration Files: package.xml and setup.py

Since we have created a new package and node, we must update the configuration files so that ROS2’s build system, Colcon, can correctly resolve dependencies and map executable entry points. This ensures that the package can be built successfully and that the node can be launched using standard ROS2 commands.

1. Adding Dependencies to package.xml

Within the code, we imported packages such as geometry_msgs, rclpy, and tf2_ros_py. Open the package.xml file located in the package root directory (~/ros2_ws/src/learning_tf2_py) and add the following dependencies directly below the <license> tag.

XML
<depend>geometry_msgs</depend>
  <depend>python3-numpy</depend>
  <depend>rclpy</depend>
  <depend>tf2_ros_py</depend>
  <depend>turtlesim</depend>

If you skip this step, package dependency errors may occur when building the project on another computer for deployment. Therefore, as a researcher or developer, you should develop the habit of managing dependencies carefully and consistently in every ROS2 project.

2. Adding an Entry Point to setup.py

This step allows us to launch the script we created directly from the command line using ros2 run. Open the setup.py file located in the same package directory and modify the entry_points section as shown below.

Python
    entry_points={
        'console_scripts': [
            'static_turtle_tf2_broadcaster = learning_tf2_py.static_turtle_tf2_broadcaster:main',
        ],
    },


Building the Package and the Moment of Truth: Running the Node

All the programming work is now complete! Open a terminal, navigate to the root of your workspace, and build the package. As a best practice, you should always use rosdep before building to check for and install any missing package dependencies.

Bash
# Navigate to the workspace root directory
cd ~/ros2_ws

# Automatically install any missing dependencies
rosdep install -i --from-path src --rosdistro jazzy -y

# Build the package
colcon build --symlink-install

If the package builds successfully without any errors, it is time to run the node and verify that the turtle frame we programmed is being published and displayed correctly.

[💡 Execution Steps]

First terminal (running the broadcaster)

In the terminal, a log message saying “Successfully published static frame transform” is displayed, and the node enters a waiting state.

Second terminal (topic verification)

To confirm that the data has been published to the /tf_static channel as intended, we check it using the echo command.

From the log output, you can see that the static transform we defined in the code is being broadcast across the entire network. The mystaticturtle frame is positioned 1 meter above the world frame (z = 1.0), appearing as if it is floating in the air.


Practical tip for practitioners: Using CLI tools and launch files

Today’s tutorial was a very important hands-on exercise for understanding the underlying principle of how TF2 is handled using Python code.

However, in real research and development environments (for example, when setting coordinate offsets between a robot arm base frame and a LiDAR sensor), you do not create a new Python file every time a static transform is needed. Instead, in ROS 2, the static_transform_publisher executable is directly declared inside a launch file (robot startup script) and handled in a much more concise way.

Rather than writing CLI commands as Python code as learned in Part 9, you simply pass arguments directly in the terminal or within a launch file. Below is an example of a command that specifies translation (x, y, z in meters) and rotation (Roll, Pitch, Yaw in radians).

Method 1: Input using Euler angles (Roll, Pitch, Yaw) — most commonly used

ros2 run tf2_ros static_transform_publisher 0 0 1 0 0 0 world mystaticturtle

Method 2: Input using quaternions (x, y, z, w)

ros2 run tf2_ros static_transform_publisher 0 0 1 0 0 0 1 world mystaticturtle

The first approach uses Euler angles, which are more intuitive for humans and therefore used more frequently in practice. The second approach uses quaternions, which avoid gimbal lock and are more stable for internal computations in systems like ROS 2 TF transformations.

In a launch Python file, you can easily insert it in the form of a node like below.

Python
Node(
    package='tf2_ros',
    executable='static_transform_publisher',
    arguments=['0', '0', '1', '0', '0', '0', 'world', 'mystaticturtle']
)


Wrapping up

In this 10th installment of ROS2 Mastery, we explored how to directly work with the core TF2 classes in ROS 2 using Python, specifically StaticTransformBroadcaster and the TransformStamped message.

At this point, you now have the essential skills to define coordinate frames freely within a ROS2 environment. This means you can integrate various sensors or fixed environmental reference frames into a complete coordinate tree structure and manage them with confidence during robot development.

However, robots are not static systems; they are constantly moving. In the next installment, ROS2 Mastery 11, we will build on this foundation and move into dynamic transformations. You will learn how to implement real-time TF2 broadcasters and listeners where a turtle (or robot) continuously updates its coordinate frame as it moves.

Stay consistent and keep pushing your understanding of robotics systems. The next part will take things one step further into truly dynamic spatial tracking.

Author: Aiden, Marketing Team at QUAD Drone Research Lab

Date: June 10, 2026

Similar Posts

답글 남기기