Skip to content

Practice

The following practice illustrates handling transformations in ROS2 using C++.

Python Equivalent

The Python version of the C++ code is also available at github.com/sze-info/arj_packages. It is worth comparing the C++ and Python codes.

Let's update the arj_packages repo to the latest version. If it updates or you get the Already up to date. message, you don't need to clone it. If you get the ~/ros2_ws/src/arj_packages: No such file or directory message after running cd ~/ros2_ws/src/arj_packages, then clone the repo.

cd ~/ros2_ws/src/arj_packages
git pull

If you get the No such file or directory message, clone it with the following commands:

Warning

The following two commands are only needed if arj_packages does not exist:

cd ~/ros2_ws/src
git clone https://github.com/sze-info/arj_packages

If it already exists, just update it instead of the previous step.

cd ~/ros2_ws/src/arj_packages/
git status
The git checkout -- .: Discards all unstaged changes locally. In VS Code, this is similar to the "discard all changes" command.
git checkout -- .
git pull

After that, you can build:

cd ~/ros2_ws
colcon build --packages-select arj_transforms_cpp

It is advisable to source in a new terminal and then run:

source ~/ros2_ws/install/setup.bash
ros2 run arj_transforms_cpp pub_transforms

Let's examine the raw output:

ros2 topic echo /tf

The response will be similar to this:

transforms:
- header:
    stamp:
      sec: 1693475112
      nanosec: 95339579
    frame_id: orbit1
  child_frame_id: orbit2
  transform:
    translation:
      x: -2.487199068069458
      y: 0.25266680121421814
      z: 0.0
    rotation:
      x: 0.0
      y: 0.0
      z: 0.0
      w: 1.0
---
transforms:
- header:
    stamp:
      sec: 1693475112
      nanosec: 145005518
    frame_id: map
  child_frame_id: orbit1
  transform:
    translation:
      x: -4.109088897705078
      y: 2.8487515449523926
      z: 0.0
    rotation:
      x: 0.0
      y: 0.0
      z: -0.46381551598382736
      w: 0.8859318072699817

As we can see, the map frame's child_frame_id is orbit1. The orbit1 frame's child_frame_id is orbit2. So, if we consider map as the grandparent, then orbit2 is the grandchild. It is more illustrative to examine this using rqt_tf_tree.

ros2 run rqt_tf_tree rqt_tf_tree

frames01

If the above command does not work, it can be installed with sudo apt install ros-humble-rqt-tf-tree. This should not be necessary on lab computers.

Let's look at it using RVIZ2, it will look like this:

ros2 launch arj_transforms_cpp rviz1.launch.py

transforms01

Let's examine the pub_transforms.cpp file. (For Python, the transforms.py file in the _py package.)

cd ~/ros2_ws/src/arj_packages/arj_transforms_cpp
code .

The most interesting part now is probably the loop function. According to the Pythagorean theorem, tr1.transform.translation.x and y will always lie on a circle due to the sine and cosine trigonometric functions. The loop_count_ variable continuously increases, so it makes the circle in the direction of the clock hands according to the speed1 value, which can be accelerated using rqt_reconfigure (we will look at this later). The size of the circle, the distance from the origin, can be increased using distance1. A similar situation applies to tr2.transform, which gives the orbit1 -> orbit2 transform. The tr1.transform also represents an additional rotation using a quaternion. We only rotate around the Z-axis from the roll, pitch, yaw values.

void loop()
{
    // Publish transforms
    tr1.header.stamp = this->get_clock()->now();
    tr1.header.frame_id = "map";
    tr1.child_frame_id = "orbit1";
    tr1.transform.translation.x = sin(loop_count_ * speed1) * distance1;
    tr1.transform.translation.y = cos(loop_count_ * speed1) * distance1;
    tf2::Quaternion quaternion1;
    quaternion1.setRPY(0.0, 0.0, loop_count_ * speed1);
    quaternion1=quaternion1.normalize();
    tr1.transform.rotation.x = quaternion1.x();
    tr1.transform.rotation.y = quaternion1.y();
    tr1.transform.rotation.z = quaternion1.z();
    tr1.transform.rotation.w = quaternion1.w();
    tf_broadcaster_->sendTransform(tr1);
    tr2.header.stamp = this->get_clock()->now();
    tr2.header.frame_id = "orbit1";
    tr2.child_frame_id = "orbit2";
    tr2.transform.translation.x = sin(loop_count_ * speed2) * distance2;
    tr2.transform.translation.y = cos(loop_count_ * speed2) * distance2;
    tf_broadcaster_->sendTransform(tr2);
    loop_count_++;
}

Let's add orbit3 with a static transform:

ros2 run tf2_ros static_transform_publisher --x 1.0 --y 0.2 --z 1.4 --qx 0.0 --qy 0.0 --qz 0.0 --qw 1.0 --frame-id orbit2 --child-frame-id orbit3

Set the speeds and distances:

ros2 run rqt_reconfigure rqt_reconfigure

Advertise a marker and add it in RVIZ2. This command advertises a green cube on orbit2:

ros2 topic pub --rate 40 --print 40 /marker_topic2 visualization_msgs/msg/Marker '{header: {frame_id: "orbit2"}, ns: "markers2", id: 2, type: 1, action: 0, pose: {position: {x: 0.0, y: 0.0, z: 0.0}, orientation: {x: 0.0, y: 0.0, z: 0.0, w: 1.0}}, scale: {x: 1.0, y: 1.0, z: 1.0}, color: {r: 0.2, g: 0.4, b: 0.3, a: 1.0}}'

This command advertises a red arrow on orbit1:

ros2 topic pub --rate 40 --print 40 /marker_topic3 visualization_msgs/msg/Marker '{header: {frame_id: "orbit1"}, ns: "markers3", id: 3, type: 0, action: 0, pose: {position: {x: 0.0, y: 0.0, z: 0.0}, orientation: {x: 0.0, y: 0.0, z: 0.0, w: 1.0}}, scale: {x: 1.8, y: 0.4, z: 0.4}, color: {r: 0.8, g: 0.2, b: 0.2, a: 1.0}}'

The Marker message's type attribute specifies whether the marker is, for example, ARROW=0 or CUBE=1:

ros2 interface show  visualization_msgs/msg/Marker

...
type:
int32 ARROW=0
int32 CUBE=1
int32 SPHERE=2
int32 CYLINDER=3
int32 LINE_STRIP=4
int32 LINE_LIST=5
int32 CUBE_LIST=6
int32 SPHERE_LIST=7
int32 POINTS=8
int32 TEXT_VIEW_FACING=9
int32 MESH_RESOURCE=10
int32 TRIANGLE_LIST=11

...

For more color examples, see below. For more on colors: github.com/jkk-research/colors.

. . .
0.96 0.26 0.21
0.53 0.05 0.31
0.19 0.11 0.57
0.73 0.87 0.98
0.13 0.59 0.95
0.00 0.59 0.53
0.78 0.90 0.79
0.30 0.69 0.31
0.80 0.86 0.22
1.00 0.93 0.70
1.00 0.76 0.03
1.00 0.44 0.00
0.84 0.80 0.78
0.47 0.33 0.28
0.24 0.15 0.14
0.96 0.96 0.96
0.62 0.62 0.62
0.13 0.13 0.13

Independent Task

As an independent task, create a package named my_launch_pkg, in which a run_transforms_and_markers.launch.py starts:

  • the node that publishes the map, orbit1, and orbit2 frames (ros2 run arj_transforms_cpp pub_transforms)
  • the rqt_reconfigure (ros2 run rqt_reconfigure rqt_reconfigure)
  • the static orbit3 frame (ros2 run tf2_ros static_transform_publisher --x 1.0 --y 0.2 --z 1.4 --qx 0.0 --qy 0.0 --qz 0.0 --qw 1.0 --frame-id orbit2 --child-frame-id orbit3)
  • and the launch that starts Rviz2 (ros2 launch arj_transforms_cpp rviz1.launch.py)

Check the correct operation in rviz2.

So, at the end of the independent task, it should be startable with the following command:

ros2 launch my_launch_pkg run_transforms_and_markers.launch.py

Solution: available among the independent tasks

Help for the Independent Task

The ros2 run tf2_ros static_transform_publisher --x 1.0 --y 0.2 --z 1.4 --qx 0.0 --qy 0.0 --qz 0.0 --qw 1.0 --frame-id orbit2 --child-frame-id orbit3 can be easily assembled based on previous lessons:

Node(
    package='tf2_ros',
    executable='static_transform_publisher',
    arguments=['1.0', '0.2', '1.4','0', '0', '0', '1', 'orbit2','orbit3'],
),     

Warning

We have a harder time with Rviz2, as we need to call a launch file, not a node.

The first, but less elegant option is to copy the original launch file and extend it:

from launch import LaunchDescription
from launch_ros.actions import Node
import os
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():

    pkg_name = 'arj_transforms_cpp'
    pkg_dir = get_package_share_directory(pkg_name)

    return LaunchDescription([
        Node(
            package='rviz2',
            namespace='',
            executable='rviz2',
            name='rviz2',
            arguments=['-d', [os.path.join(pkg_dir, 'rviz', 'rviz1.rviz')]]
        )
    ])

The second, much more elegant option is to include the launch file in the launch file:

from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.substitutions import FindPackageShare

def generate_launch_description():
    return LaunchDescription([
        # ros2 launch arj_transforms_cpp rviz1.launch.py
        IncludeLaunchDescription(
            PythonLaunchDescriptionSource([
                FindPackageShare("arj_transforms_cpp"), '/launch/', 'rviz1.launch.py'])
        ),
    ])

Further Reading

Python notebook transform

Python notebook quaternion

gps_utm.ipynb

rh

lexus 3d

Reading Material