Technical Specification Snapshot
| Parameter | Details |
|---|---|
| Runtime Environment | Ubuntu 18.04/20.04 |
| ROS Version | Melodic / Noetic |
| Primary Language | C++ |
| Communication / Mechanisms | Topic, Service, TF |
| Core Dependencies | roscpp, tf, geometry_msgs, turtlesim, std_msgs |
| Example Nodes | turtle_broadcaster, turtle_listener, turtle_tf |
| Applicable Scenarios | Robot frame management, target following, virtual reference frames |
| Star Count | Not provided in the source content |
TF is the core infrastructure for managing spatial relationships in robotics
TF does not solve the question, “How does the robot move?” It solves a more fundamental one: “How does the robot understand the spatial relationships between its parts?” In a real robot system, the chassis, LiDAR, camera, and robotic arm all have their own coordinate frames. Algorithms can only compute paths, obstacle locations, and control outputs after those frames are unified under a consistent reference model.
TF maintains these relationships as a tree of coordinate frames. It enforces a single-parent, multiple-children structure with no cycles, so every frame can have only one parent. This constraint may look strict, but it guarantees that every transform chain remains traceable, composable, and queryable in real time.
This experiment builds a minimal, runnable TF system
This example uses turtlesim as a low-barrier simulation environment: turtle1 is controlled from the keyboard, and turtle2 automatically tracks it by listening to TF. That reduces the core TF workflow to three steps: broadcast poses, query transforms, and compute control commands.
cd ~/workspace_hzq/src
catkin_create_pkg turtle_follow std_msgs roscpp rospy tf geometry_msgs
cd ..
catkin_make # Build the workspace
These commands create the package and complete the initial build.
The TF broadcaster publishes pose messages as coordinate transforms
The broadcaster node subscribes to the turtle pose topic and converts turtlesim/Pose into a world -> turtle_name TF transform. Translation comes from x/y, and rotation is published after converting theta into a quaternion.
#include <ros/ros.h>
#include <tf/transform_broadcaster.h>
#include <turtlesim/Pose.h>
std::string turtle_name;
void poseCallback(const turtlesim::PoseConstPtr& msg) {
static tf::TransformBroadcaster br; // Static broadcaster to avoid repeated construction
tf::Transform transform;
transform.setOrigin(tf::Vector3(msg->x, msg->y, 0.0)); // Set 2D translation
tf::Quaternion q;
q.setRPY(0, 0, msg->theta); // Rotate only around the Z axis to match turtle heading
transform.setRotation(q);
br.sendTransform(tf::StampedTransform(
transform,
ros::Time::now(), // Use the current timestamp
"world", // Parent frame
turtle_name // Child frame
));
}
This code converts the turtle’s real-time pose into TF data that the entire system can consume globally.
The key broadcaster design principle is consistent frame semantics
Choosing world as the parent frame means all turtle poses are expressed relative to a global reference frame. As long as multiple objects are attached to the same tree, a listener can query the relative relationship between any two frames across nodes and subsystems.
If the broadcaster runs at a stable frequency, the TF buffer can maintain a time-continuous pose chain. That continuity is also the foundation for stable downstream control.
The TF listener generates following control directly from relative transforms
The listener first calls the spawn service to create turtle2, then continuously queries the transform from /turtle2 to /turtle1. After obtaining the relative coordinates, it uses angular error to control turning and Euclidean distance to control forward velocity.
#include <ros/ros.h>
#include <tf/transform_listener.h>
#include <geometry_msgs/Twist.h>
#include <turtlesim/Spawn.h>
int main(int argc, char** argv) {
ros::init(argc, argv, "turtle_tf_listener");
ros::NodeHandle node;
ros::service::waitForService("spawn"); // Wait until the turtle spawn service is available
ros::ServiceClient add_turtle = node.serviceClient<turtlesim::Spawn>("spawn");
turtlesim::Spawn srv;
add_turtle.call(srv);
ros::Publisher turtle_vel = node.advertise<geometry_msgs::Twist>("turtle2/cmd_vel", 2);
tf::TransformListener listener;
ros::Rate rate(10.0);
while (node.ok()) {
tf::StampedTransform transform;
try {
listener.waitForTransform("/turtle2", "/turtle1", ros::Time(0), ros::Duration(3.0));
listener.lookupTransform("/turtle2", "/turtle1", ros::Time(0), transform); // Query the latest transform
} catch (tf::TransformException &ex) {
ROS_ERROR("%s", ex.what());
ros::Duration(1.0).sleep();
continue;
}
geometry_msgs::Twist vel_msg;
vel_msg.angular.z = 4.0 * atan2(transform.getOrigin().y(), transform.getOrigin().x()); // Turn toward the target
vel_msg.linear.x = 0.5 * sqrt(
pow(transform.getOrigin().x(), 2) +
pow(transform.getOrigin().y(), 2) // Move forward based on distance
);
turtle_vel.publish(vel_msg);
rate.sleep();
}
}
This code shows how TF can directly serve a control loop: relative position becomes the control input.
The real value here is eliminating manual coordinate transform derivation
Without TF, developers would need to manage pose subscriptions, time synchronization, rotation matrix composition, and coordinate conversion by hand. With lookupTransform(), the control node only needs to answer one question: “Where am I relative to the target?” That greatly reduces system coupling.
Build and launch configuration determine whether the project can be reproduced reliably
CMakeLists.txt must declare the executables and link them against catkin_LIBRARIES. package.xml must also include the full build-time and runtime dependencies. Otherwise, the nodes may be written correctly but still fail to build as a complete package.
find_package(catkin REQUIRED COMPONENTS
geometry_msgs
roscpp
rospy
std_msgs
tf
)
add_executable(turtle_broadcaster src/turtle_broadcaster.cpp)
add_executable(turtle_listener src/turtle_listener.cpp)
target_link_libraries(turtle_broadcaster ${catkin_LIBRARIES})
target_link_libraries(turtle_listener ${catkin_LIBRARIES})
This configuration ensures that catkin can compile and link the broadcaster and listener correctly.
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="sim"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen"/>
<node pkg="turtle_follow" type="turtle_broadcaster" args="/turtle1" name="turtle1_tf_broadcaster" />
<node pkg="turtle_follow" type="turtle_broadcaster" args="/turtle2" name="turtle2_tf_broadcaster" />
<node pkg="turtle_follow" type="turtle_listener" name="listener" />
</launch>
This launch file starts the simulation, broadcasters, and following control chain with a single command.
Adding the carrot1 frame upgrades TF from following an entity to following a target point
The advanced section is not about adding one more node. It is about understanding that a TF tree can describe virtual reference frames. By making carrot1 a child frame of turtle1 with a fixed offset of (2, 2, 0), turtle2 can follow a target point instead of the turtle body itself.
#include <ros/ros.h>
#include <tf/transform_broadcaster.h>
int main(int argc, char** argv) {
ros::init(argc, argv, "turtle_tf_broadcaster");
ros::NodeHandle node;
tf::TransformBroadcaster br;
tf::Transform transform;
ros::Rate rate(10.0);
while (node.ok()) {
transform.setOrigin(tf::Vector3(2.0, 2.0, 0.0)); // Create an offset target in front of turtle1
transform.setRotation(tf::Quaternion(0, 0, 0, 1)); // Do not add extra rotation
br.sendTransform(tf::StampedTransform(transform, ros::Time::now(), "turtle1", "carrot1"));
rate.sleep();
}
}
This code attaches a virtual target point to the existing TF tree.
Changing the listener target to carrot1 completes the control semantics switch
listener.waitForTransform("/turtle2", "/carrot1", ros::Time(0), ros::Duration(3.0));
listener.lookupTransform("/turtle2", "/carrot1", ros::Time(0), transform); // Switch from following turtle1 to following carrot1
These two lines switch the following target from a physical frame to a virtual frame.
TF debugging tools quickly verify whether the coordinate tree is correct
When the system behaves unexpectedly, check TF connectivity before tuning the control algorithm. Use rqt_tf_tree to inspect the tree structure, tf_echo to inspect real-time values between two frames, and rostopic echo /tf to confirm that broadcasts are still being published.
rosrun rqt_tf_tree rqt_tf_tree
rosrun tf tf_echo /turtle1 /carrot1
rostopic echo /tf # Check whether TF messages are being published continuously
These commands help verify the TF tree structure, transform values, and message flow.
This example transfers directly to real robot systems
Turtle following is only the simplest demonstration. The underlying idea transfers directly to mobile robots, robotic grasping, and multi-sensor fusion. Replace turtle1 with a LiDAR, camera, or end effector, and replace carrot1 with a local goal point, and TF will continue to serve as the central hub for spatial relationships.
For further optimization, add PID control, velocity limiting, low-pass filtering, and time synchronization strategies to make the following behavior smoother and more resistant to jitter.
FAQ
Q1: Why does the TF tree not allow cycles?
A: Cycles would make coordinate derivation ambiguous because the system could no longer determine a unique parent path. TF guarantees a unique transform chain for every frame through its single-parent, multiple-children rule, which keeps query results stable and interpretable.
Q2: What does ros::Time(0) mean in a TF query?
A: It requests the latest available transform in the current buffer. This is common in real-time control because developers usually want the most recently synchronized pose relationship.
Q3: Why can following control work using only relative coordinates?
A: Because the listener receives the target position in its own coordinate frame. atan2(y, x) directly gives the steering error, and the distance norm directly gives the forward motion intensity, so no additional global coordinate conversion is required.
Core summary: This article uses ROS1 and turtlesim to explain TF coordinate trees, the collaboration between broadcasters and listeners, and a complete C++ implementation of turtle following plus a custom carrot1 frame, covering package creation, coding, compilation, launch, and debugging end to end.