As we can see on https://docs.ros.org/en/foxy/Concepts/About-Tf2.html, tf2 is the transform library for ROS2. It maintains the relationship between coordinate frames in a tree structure buffered in time and lets the user transform points between any two coordinate frames at any desired point in time.
Creating a rosject
In order to learn TF2 hands-on, we need to have a system with ROS installed. We are going to use The Construct (https://app.theconstructsim.com/) for this tutorial, but if you have ROS2 installed on your own computer, you should be able to do ~everything on your own computer, except this creating a rosject part.
Let’s start by opening The Construct (https://www.theconstruct.ai/) and logging in. You can easily create a free account if you still don’t have one.
Once inside, if you did not decide to use the existing rosject, let’s click My Rosjects on the left side and then, Create a new rosject.
Create a new rosject
Let’s select ROS2 Foxyfor the ROS Distro of the rosject, let’s name the rosject as you want. You can leave the rosject public. You should see the rosject you just created in your rosjects list (the name is certainly different from the example below that was added just for learning purposes)
If you mouse over the recently created rosject, you should see a Run button. Just click that button to launch the rosject.
Creating a ROS2 Python package
After pressing the Run button, you should now have the rosject open. Now, it’s time to create a ROS2 Python package if you had to create your own rosject. If you used the rosject we shared at the beginning of this post, you don’t need to create the package. To create the package, let’s start by opening a terminal:
Open a new Terminal
Now, in this first terminal, let’s run the following command to create our package named tf2_examples:
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python --node-name my_node tf2_examples
If everything went fine, the output should be as follows:
As we can see in the logs, we already have a node called ./tf2_examples/tf2_examples/my_node.py and a ./tf2_examples/setup.py.
Preparing our python package to have launch files
In order to be able to have launch files in our package, we need to modify the ~/ros2_ws/src/tf2_examples/setup.py file.
We have basically to import glob and modify the data_files variable. To make our life easier, I’m going to paste here the full content of the setup.py file after our modifications (bear in mind that if you used the rosject provided at the beginning, the file will already contain the correct code):
On The Construct, we do not always have all packages required for all purposes. For this TF2 demonstration, we need to have some packages. Lets install them by running the following commands in a second terminal:
If you have used the rosject we provided at the beginning, you already have the my_node.py with a Frame Publisher set.
If you created a rosject manually, however, you need to manually replace the content of my_node.py with the following content taken from the official ROS2 Tutorials:
from geometry_msgs.msg import TransformStamped
import rclpy
from rclpy.node import Node
from tf2_ros import TransformBroadcaster
import tf_transformations
from turtlesim.msg import Pose
class FramePublisher(Node):
def __init__(self):
super().__init__('turtle_tf2_frame_publisher')
# Declare and acquire `turtlename` parameter
self.declare_parameter('turtlename', 'turtle')
self.turtlename = self.get_parameter(
'turtlename').get_parameter_value().string_value
# Initialize the transform broadcaster
self.br = TransformBroadcaster(self)
# Subscribe to a turtle{1}{2}/pose topic and call handle_turtle_pose
# callback function on each message
self.subscription = self.create_subscription(
Pose,
f'/{self.turtlename}/pose',
self.handle_turtle_pose,
1)
def handle_turtle_pose(self, msg):
t = TransformStamped()
t2 = TransformStamped()
# Read message content and assign it to
# corresponding tf variables
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = 'world'
t.child_frame_id = self.turtlename
# turtle/drone
t2.header.stamp = self.get_clock().now().to_msg()
t2.header.frame_id = self.turtlename
t2.child_frame_id = 'drone'
# Turtle only exists in 2D, thus we get x and y translation
# coordinates from the message and set the z coordinate to 0
t.transform.translation.x = msg.x - (11.08888/2)
t.transform.translation.y = msg.y - (11.08888/2)
t.transform.translation.z = 0.0
t2.transform.translation.x = -0.5
t2.transform.translation.y = 0.0
t2.transform.translation.z = 1.0
# For the same reason, turtle can only rotate around one axis
# and this why we set rotation in x and y to 0 and obtain
# rotation in z axis from the message
q = tf_transformations.quaternion_from_euler(0, 0, msg.theta)
t.transform.rotation.x = q[0]
t.transform.rotation.y = q[1]
t.transform.rotation.z = q[2]
t.transform.rotation.w = q[3]
t2.transform.rotation.x = 0.0
t2.transform.rotation.y = 0.0
t2.transform.rotation.z = 0.0
t2.transform.rotation.w = 1.0
# Send the transformation
self.br.sendTransform(t)
self.br.sendTransform(t2)
def main():
rclpy.init()
node = FramePublisher()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
rclpy.shutdown()
If you check carefully the code of my_node.py, you can see that around line 24 we start a TF Broadcaster:
# Initialize the transform broadcaster
self.br = TransformBroadcaster(self)
we then create a subscriber to a /turtle{1}{2}/pose topic and call handle_turtle_pose method when messages arrive on this topic. I highly recommend you have a look at the comments and the code of the handle_turtle_pose to better understand it. It not only makes you exercise your brain but also makes you better understand things by yourself.
Creating a launch file
Now that we have our code in place, we need a launch file. If you remember well, the setup.py file is already prepared for launch files. We now need to create the launch files. Bear in mind that if you are using the rosject we provided at the beginning of the post, this launch folder and its python file already exist.
Assuming you created a rosject manually, let’s create a launch folder using the terminal:
cd ~/ros2_ws/src/tf2_examples
mkdir launch
cd launch
touch tf2_example.launch.py
If you just created the tf2_example.launch.py file, it may be empty. If that is the case, just copy the following content to it:
If you are wondering why you need to compile the workspace if we are using only python, not C++, the answer is: we need to compile so that ROS2 can copy the launch files to the common share folder of the workspace.
Launching the turtle simulation using ros2 launch files
Now that our package is compiled and everything in place, let’s launch the launch file we just created:
cd ~/ros2_ws/
source install/setup.bash
ros2 launch tf2_examples tf2_example.launch.py
You should now have a simulation window that should have opened automatically with the turtlesim simulation on it.
If the simulation do not show automatically to you, you can just open the Graphical Tools:
Open Graphical Tools to see the turtlesim
If we now open a third terminal, we should be able to see our nodes there after typing ros2 node list:
ros2 node list
/broadcaster1
/sim
Open RViz2
Now that we have our simulation and our broadcaster running, we need RViz2 to see the frames.
Let’s open it running the following command in the third terminal:
ros2 run rviz2 rviz2
If rviz2 does not show automatically to you, you can just open the Graphical Tools as before.
Assuming RViz is running, remember to set the fixed frame (on the top left side) to world. You also have to click the Add button on the bottom left to add TF:
RViz add TF
After having added the TF panel in RViz, you should see the frames as it happens in the center of the image above.
Moving the robot using the keyboard
Now that we have our simulation running, rviz running, and our TF panel added, we can move our robot to better understand the TFs.
TIP: In the Graphical Tools window, to see the Turtlesim and the TFs at the same time, you can put move the turtlesim panel to the left, and put RViz on the right side.
Ok, we can now open a fourth terminal and run the turtle_teleop_key node that allows us to move the robot using the keyboard:
ros2 run turtlesim turtle_teleop_key
You should now have the instructions to move the robot around:
Reading from keyboard
---------------------------
Use arrow keys to move the turtle.
Use G|B|V|C|D|E|R|T keys to rotate to absolute orientations. 'F' to cancel a rotation.
'Q' to quit.
By pressing the arrow keys, you can see that the robot moves, and at the same time we can see the frames in RViz.
If you look carefully, in RViz we see not only the frames for the turtle and the world, but we also have a drone frame.
We did that to simulate that we have a drone following the turtle at a fixed distance. I would recommend you have a look again at the handle_turtle_pose method defined in the my_node.py to better understand it.
In the code of handle_turtle_pose, we see that the drone is 0.5 meters behind the turtle and 1 meter above:
Whenever we receive a position from the turtle, we are publishing the position of the drone based on the position of the turtle.
Checking the position of the turtle related to the world using the command line
We saw that we can do a lot of tf2-related things using Python.
It is worth mentioning that we can also use the command line to check the position/distance of the turtle with relation to the world.
ROS2 has a package named tf2_ros with many executables and one of them is tf2_echo. We can know the position of the turtle with the following command:
ros2 run tf2_ros tf2_echo world turtle1
The output should be similar to the following:
At time 1649109418.661570822
- Translation: [-2.552, 0.000, 0.000]
- Rotation: in Quaternion [0.000, 0.000, 0.000, 1.000]
At time 1649109419.668374440
- Translation: [-2.552, 0.000, 0.000]
- Rotation: in Quaternion [0.000, 0.000, 0.000, 1.000]
At time 1649109420.660446225
- Translation: [-2.552, 0.000, 0.000]
- Rotation: in Quaternion [0.000, 0.000, 0.000, 1.000]
You can also check the position of the drone related to the position of the robot:
ros2 run tf2_ros tf2_echo turtle1 drone
You can see that the translation is exactly as set in the my_node.py file (half meters behind, 1 meter above):
- Translation: [-0.500, 0.000, 1.000]
If you are wondering whether or not you can know the position of the drone related to the world, you can know it also just change the parameters passed to the tf2_echo node:
ros2 run tf2_ros tf2_echo world drone
Youtube video
So this is the post for today. Remember that we have the live version of this post on YouTube. If you liked the content, please consider subscribing to our youtube channel. We are publishing new content ~every day.
Keep pushing your ROS Learning.
Related Courses & Training
If you want to learn more about ROS and ROS2, we recommend the following courses:
Today we will be learning ROS_DOMAIN_ID, something new in ROS2 that works on top of DDS, the communication layer.
What it really does? It basically allows you to isolate two or more ROS environments in the same physical network.
Each DOMAIN_ID would allow you to have around 120 nodes.
Creating a rosject
In order to learn ROS_DOMAIN_ID with hands-on, we need to have a system with ROS installed. We are going to use The Construct (https://app.theconstructsim.com/) for this tutorial, but if you have ROS2 installed on your own computer, you should be able to do ~everything on your own computer, except this creating a rosject part.
Let’s start by opening The Construct (https://www.theconstruct.ai/) and logging in. You can easily create a free account if you still don’t have one.
Once inside, let’s click My Rosjects on the left side and then, Create a new rosject.
Create a new rosject
Let’s select ROS2 Foxyfor the ROS Distro of the rosject, let’s name the rosject as you want. You can leave the rosject public. You should see the rosject you just created in your rosjects list (the name is certainly different from the example below that was added just for learning purposes)
If you mouse over the recently created rosject, you should see a Run button. Just click that button to launch the rosject.
Running a publisher on ROS_DOMAIN_ID=0
Assuming that you have clicked the Run button and have your rosject running, let’s now create a publisher using the terminal. For that, let’s start by opening a terminal.
Open a new Terminal
Now, let’s run the following command to run create a publisher in the first terminal:
ros2 topic pub -r 1 /string_topic std_msgs/String "{data: \"Hello from my domain\"}"
We should have the following output:
publisher: beginning loop
publishing #1: std_msgs.msg.String(data='Hello from my domain')
publishing #2: std_msgs.msg.String(data='Hello from my domain')
publishing #3: std_msgs.msg.String(data='Hello from my domain')
publishing #4: std_msgs.msg.String(data='Hello from my domain')
publishing #5: std_msgs.msg.String(data='Hello from my domain')
publishing #6: std_msgs.msg.String(data='Hello from my domain')
publishing #7: std_msgs.msg.String(data='Hello from my domain')
...
If you look carefully at the command that we typed, we are basically creating a topic called string_topic with std_msgs/String data type and we are publishing Hello from my domain every 1 second.
Running a ROS2 Subscriber on ROS_DOMAIN_ID=0
If we open a second terminal, we can see the topics by typing ros2 topic list and we should be able to see our string_topic:
user:~$ ros2 topic list
/parameter_events
/rosout
/string_topic
We can also subscribe to that topic and see what is being published there with ros2 topic echo /string_topic, which should output:
user:~$ ros2 topic echo /string_topic
data: Hello from my domain
---
data: Hello from my domain
---
data: Hello from my domain
---
Up to now we did not setup any ROS_DOMAIN_ID, but we are running on Domain 0 (zero).
How do we know this? Because when we do not set any ROS_DOMAIN_ID, the default value is zero.
If we list the processes and grep for ros-domain, we can also see that the default ID is 0.
Let’s list the processes in a third terminal and search for ros-domain with the following command
ps -ax| grep ros-domain
We should see –ros-domain-id 0 at the end of the command line used to launch the ros2_daemon, which is launched automatically when you run your first ros2 command.
Now, let’s go to the first terminal, stop the current publisher by pressing CTRL+C, and let’s relaunch the publisher but now setting the ROS_DOMAIN_ID=1.
ROS_DOMAIN_ID=1 ros2 topic pub -r 1 /string_topic std_msgs/String "{data: \"Hello from my 2ND domain\"}"
You should see that the publisher is running as expected:
publisher: beginning loop
publishing #1: std_msgs.msg.String(data='Hello from my 2ND domain')
publishing #2: std_msgs.msg.String(data='Hello from my 2ND domain')
publishing #3: std_msgs.msg.String(data='Hello from my 2ND domain')
publishing #4: std_msgs.msg.String(data='Hello from my 2ND domain')
publishing #5: std_msgs.msg.String(data='Hello from my 2ND domain')
publishing #6: std_msgs.msg.String(data='Hello from my 2ND domain')
publishing #7: std_msgs.msg.String(data='Hello from my 2ND domain')
...
But if we check the output of the subscriber in the second terminal, we see that the messages stopped coming.
We are not receiving the new messages in the second terminal, and the reason for that is simple: the subscriber is on ROS_DOMAIN_ID=0, and the publisher is on ROS_DOMAIN_ID=1. They are basically on different ROS Networks, that is why they do not see each other.
If we now stop the subscriber by pressing CTRL+C on the second terminal, we can run it again but on ROS_DOMAIN_ID=1 to see the messages being published:
ROS_DOMAIN_ID=1 ros2 topic echo /string_topic
We should see now that the messages are correctly coming:
data: Hello from my 2ND domain
---
data: Hello from my 2ND domain
---
data: Hello from my 2ND domain
...
ROS2 topic list on different ROS_DOMAIN_IDs
So far we learned that the ros2 topic pub and ros2 topic echo commands can use different domains, but it is worth mentioning that the ROS_DOMAIN_ID thing is not restricted only to these two commands. Any ROS2 command should respect this ROS_DOMAIN_ID.
If in the second terminal we stop the subscriber with CTRL+C and run ros2 topic list , we will not see the string_topic there:
ros2 topic list
/parameter_events
/rosout
But if we set the ROS_DOMAIN_ID to 1, we will see the string_topic topic there:
ROS_DOMAIN_ID=1 ros2 topic list
/parameter_events
/rosout
/string_topic
All right! We could go on with more examples, but I think you should now have understood how ROS_DOMAIN_ID works.
So this is the post for today. Remember that we have the live version of this post on YouTube. If you liked the content, please consider subscribing to our youtube channel. We are publishing new content ~every day.
Keep pushing your ROS Learning.
Related Courses & Training
If you want to learn more about ROS and ROS2, we recommend the following courses:
Before we start really using ROS2 parameters, let’s understand key points about them:
Parameters in ROS2 are implemented/attached to/served by each node individually, as opposed to the parameter server associated with roscore in ROS1, therefore, when the node dies, so do its parameters. This is easier to understand if you already know that in ROS2 we do not have roscore.
Parameters can be loaded at the node startup or while the node is running
Having this in mind, our life now understanding ROS2 params is going to become easier.
Opening the rosject
In order to learn how to load and retrieve ROS2 Parameters, we need to have ROS installed in our system, and it is also useful to have some simulations. We already prepared a rosject with a simulation for that: https://app.theconstructsim.com/#/l/4875b2e0/.
You can download the rosject on your own computer if you want to work locally, but just by copying the rosject (clicking the link), you will have a setup already prepared for you.
After the rosject has been successfully copied to your own area, you should see a Run button. Just click that button to launch the rosject.
Learn ROS2 Parameters – Run rosject
After pressing the Run button, you should have the rosject loaded. Let’s now head to the next section to really get some real practice.
Launching the turtlebot simulation with ROS2
In order to launch the simulation, let’s start by opening a new terminal:
Open a new Terminal
After having the first terminal open, let’s run the following commands to launch a simulation:
Wait some seconds until the simulation is loaded. If for any reason the simulation does not load and you see error messages like the following:
[INFO] [1649093592.180346898] [spawn_entity]: Waiting for service /spawn_entity
[ERROR] [1649093597.566604708] [spawn_entity]: Service %s/spawn_entity unavailable. Was Gazebo started with GazeboRosFactory?
[ERROR] [1649093597.567565097] [spawn_entity]: Spawn service failed. Exiting.
[ERROR] [spawn_entity.py-4]: process has died [pid 1007, exit code 1, cmd '/opt/ros/galactic/lib/gazebo_ros/spawn_entity.py -entity waffle_pi -file /home/user/simulation_ws/install/turtlebot3_gazebo/share/turtlebot3_gazebo/models/turtlebot3_waffle_pi/model.sdf -x 0.0 -y 0.0 -z 0.01 --ros-args']
you can just abort the current command by pressing CTRL+C in the terminal, then run the last command “ros2 launch turtlebot3_gazebo empty_world.launch.py” again.
If everything loaded fine, you should have a Turtlebot Waffle PI simulation running:
Turtlebot Waffle PI – How to use ROS2 parameters
Checking ROS2 Topics to ensure the simulation is ok
Now that our simulation is running, we can check the topics just to make sure everything loaded as expected.
For that, let’s open a second terminal and type the following command:
ros2 topic list
If you see the following list of topics, then everything has loaded fine:
If you look carefully in the list of topics, we have the topic /cmd_vel. We are going to use it to send velocity commands to the robot using ROS parameters.
Understanding the parameter_tests package
So far so good. It is now time to check the structure of our workspace. Let’s start by opening our Code Editor:
Open the IDE – Code Editor
After having the IDE open, under ros2_ws/src you should find a package named parameter_tests.
Inside that package, there is also a folder named parameter_tests with a file named parameter_tests_node.py. Please click on that file to open it and analyze its code.
The code is the following:
import rclpy
import rclpy.node
from rcl_interfaces.msg import ParameterDescriptor
from geometry_msgs.msg import Twist
class VelParam(rclpy.node.Node):
def __init__(self):
super().__init__('param_vel_node')
self.timer = self.create_timer(0.1, self.timer_callback)
self.publisher = self.create_publisher(Twist, 'cmd_vel', 10)
self.msg = Twist()
param_descriptor = ParameterDescriptor(
description='Sets the velocity (in m/s) of the robot.')
self.declare_parameter('velocity', 0.0, param_descriptor)
# self.add_on_set_parameters_callback(self.parameter_callback)
def timer_callback(self):
my_param = self.get_parameter('velocity').value
self.get_logger().info('Velocity parameter is: %f' % my_param)
self.msg.linear.x = my_param
self.publisher.publish(self.msg)
def main():
rclpy.init()
node = VelParam()
rclpy.spin(node)
if __name__ == '__main__':
main()
If you check the main function, we are basically starting the ROS node, instantiating an object of our VelParam class, and spinning that node.
On the VelParam, one of the most important parts is where we define the param_descriptor. That param descriptor is what we use to set a parameter called velocity and define its initial value as 0.0.
Every certain amount of time (0.1 sec) the timer_callback method is called.
If we now check that timer_callback method, we see that it basically reads the velocity parameter and uses it to publish a velocity to the /cmd_vel.
Running the parameter_tests_node node
Now that we understand what our node does, it is time to run it.
For that, let’s open a third terminal and run the following command:
ros2 run parameter_tests param_vel
You should see constant messages like the following:
[INFO] [1649095105.555241669] [param_vel_node]: Velocity parameter is: 0.000000
[INFO] [1649095105.559028822] [param_vel_node]: Velocity parameter is: 0.000000
[INFO] [1649095105.583306104] [param_vel_node]: Velocity parameter is: 0.000000
...
We see that the initial value of the velocity parameter is 0 (zero).
Checking ROS Parameters using the terminal
Ok, so far we see everything is working, and we were are able to set and retrieve the ROS Param using Python.
Now let’s list the parameters to identify our velocity param. Let’s open a third terminal and type ros2 param list. The output should be similar to the following:
The node we are interested in is the param_vel_node. In the output above, we can easily identify our velocity param there:
user:~$ ros2 param list
/param_vel_node:
use_sim_time
velocity
the use_sim_time is a parameter that comes in every node.
Moving the robot using ROS Parameter
That that we have the param_vel_node with the velocity param, we can easily set a value to that parameter with the following command:
ros2 param set /param_vel_node velocity 0.2
After running this command, you should see that the robot started moving.
If you also check the terminal where we launched our node, it should say the current value or our parameter:
[INFO] [1649096658.093696410] [param_vel_node]: Velocity parameter is: 0.200000
[INFO] [1649096658.181101399] [param_vel_node]: Velocity parameter is: 0.200000
[INFO] [1649096658.281628131] [param_vel_node]: Velocity parameter is: 0.200000
Remember that you can easily set the parameter to 0.0 again to stop the robot:
ros2 param set /param_vel_node velocity 0.0
Dumping ROS Parameters into a YAML file (YAML format)
Now that we saw that we can easily move the robot back and forth using ROS parameters, in case you need to somehow dump the parameters, you can easily do it with:
cd ~/ros2_ws/src/parameter_tests/config
ros2 param dump /param_vel_node
The command will generate a file named ./param_vel_node.yaml with the following content (after we have set the velocity to 0.0 again):
Loading ROS Parameters when running Node using YAML files
All right, so far we have learned how to set parameters using Python and using the command line directly through ros2 param set. Now the time has come to also learn how to launch a node and set the parameters from a YAML file.
Before we do that, feel free to change the velocity value in the param_vel_node.yaml file.
Please, go to the second terminal where you launched the node and press CTRL+C to kill it. The simulation should be kept running.
Now, launch the node again, but now loading the parameters from the YAML file using the following command:
ros2 run parameter_tests param_vel --ros-args --params-file /home/user/ros2_ws/src/parameter_tests/config/param_vel_node.yaml
Based on the logs that are printed on the screen, you should be able to see that the parameters were correctly loaded from the YAML file.
Loading ROS Parameters using launch files directly
Up to now, we were launching our node using ros2 run, but if you are familiar with ROS, you may know that we can also use ros2 launch to launch ROS2 nodes.
If you check carefully the rosject, you will find a launch file under the following path:
Congratulations, you now know all the basics about ROS Parameters in ROS2.
Youtube video
So this is the post for today. Remember that we have the live version of this post on YouTube. If you liked the content, please consider subscribing to our youtube channel. We are publishing new content ~every day.
Keep pushing your ROS Learning.
Related Courses & Training
If you want to learn more about ROS and ROS2, we recommend the following courses: