Setting Up Odometry - Gazebo Classic

In this guide, we will be looking at how to integrate our robot’s odometry system with Nav2. First we will provide a brief introduction on odometry, plus the necessary messages and transforms that need to be published for Nav2 to function correctly. Next, we will show how to setup odometry with two different cases. In the first case, we will show how to setup an odometry system for a robot with already available wheel encoders. In the second case, we will build a demo that simulates a functioning odometry system on sam_bot (the robot that we built in the previous section) using Gazebo.

See also

The complete source code in this tutorial can be found in navigation2_tutorials repository under the sam_bot_description package. Note that the repository contains the full code after accomplishing all the tutorials in this guide.

Odometry Introduction

The odometry system provides a locally accurate estimate of a robot’s pose and velocity based on its motion. The odometry information can be obtained from various sources such as IMU, LIDAR, RADAR, VIO, and wheel encoders. One thing to note is that IMUs drift over time while wheel encoders drift over distance traveled, thus they are often used together to counter each other’s negative characteristics.

The odom frame and the transformation associated with it use a robot’s odometry system to publish localization information that is continuous but becomes less accurate over time or distance (depending on the sensor modalities and drift). In spite of this, the information can still be used by the robot to navigate its immediate vicinity (e.g collision avoidance). To obtain consistently accurate odometry information over time, the map frame provides globally accurate information that is used to correct the odom frame.

As discussed in the previous guides and in REP 105, the odom frame is connected to the rest of the system and Nav2 through the odom => base_link transform. This transform is published by a tf2 broadcaster or by frameworks such as robot_localization, which also provide additional functionalities. We will be talking more about robot_localization in a following section.

In addition to the required odom => base_link transform, Nav2 also requires the publishing of nav_msgs/Odometry message because this message provides the velocity information of the robot. In detail, the nav_msgs/Odometry message contains the following information:

# This represents estimates of position and velocity in free space.
# The pose in this message should be specified in the coordinate frame given by header.frame_id
# The twist in this message should be specified in the coordinate frame given by the child_frame_id

# Includes the frame id of the pose parent.
std_msgs/Header header

# Frame id the pose is pointing at. The twist is in this coordinate frame.
string child_frame_id

# Estimated pose that is typically relative to a fixed world frame.
geometry_msgs/PoseWithCovariance pose

# Estimated linear and angular velocity relative to child_frame_id.
geometry_msgs/TwistWithCovariance twist

This message tells us the estimates for the pose and velocity of the robot. The header message provides the timestamped data in a given coordinate frame. The pose message provides the position and orientation of the robot relative to the frame specified in header.frame_id. The twist message gives the linear and angular velocity relative to the frame defined in child_frame_id.

Setting Up Odometry on your Robot

Setting up the odometry system for Nav2 for your physical robot depends a lot on which odometry sensors are available with your robot. Due to the large number of configurations your robot may have, specific setup instructions will not be within the scope of this tutorial. Instead, we will provide some basic examples and useful resources to help you configure your robot for Nav2.

To start, we will use an example of a robot with wheel encoders as its odometry source. Note that wheel encoders are not required for Nav2 but it is common in most setups. The goal in setting up the odometry is to compute the odometry information and publish the nav_msgs/Odometry message and odom => base_link transform over ROS 2. To calculate this information, you will need to setup some code that will translate wheel encoder information into odometry information, similar to the snippet below:

linear = (right_wheel_est_vel + left_wheel_est_vel) / 2
angular = (right_wheel_est_vel - left_wheel_est_vel) / wheel_separation;

The right_wheel_est_vel and left_wheel_est_vel are the estimated velocities of the right and left wheels respectively, and the wheel separation is the distance between the wheels. The values of right_wheel_est_vel and left_wheel_est_vel can be obtained by simply getting the changes in the positions of the wheel joints over time. This information can then be used to publish the Nav2 requirements. A basic example on how to do this can be found in the Navigation documentation on odometry located here

An alternative to manually publishing this information that we recommend is through the ros2_control framework. The ros2_control framework contains various packages for real-time control of robots in ROS 2. For wheel encoders, ros2_control has a diff_drive_controller (differential drive controller) under the ros2_controller package. The diff_drive_controller takes in the geometry_msgs/Twist messages published on cmd_vel topic, computes odometry information, and publishes nav_msgs/Odometry messages on odom topic. Other packages that deal with different kind of sensors are also available in ros2_control.

See also

For more information, see the ros2_control documentation and the Github repository of diff_drive_controller.

For other types of sensors such as IMU, VIO, etc, their respective ROS drivers should have documentation on how publish the odometry information. Keep in mind that Nav2 requires the nav_msgs/Odometry message and odom => base_link transforms to be published and this should be your goal when setting up your odometry system.

Simulating an Odometry System using Gazebo

In this section, we will be using Gazebo to simulate the odometry system of sam_bot, the robot that we built in the previous section of this tutorial series. You may go through that guide first or grab the complete source here.

Note

If you are working on your own physical robot and have already set up your odometry sensors, you may opt to skip this section and head onto the next one where we fuse IMU and odometry messages to provide a smooth odom => base_link transformation.

As an overview for this section, we will first setup Gazebo and the necessary packages required to make it work with ROS 2. Next, we will be adding Gazebo plugins, which simulate an IMU sensor and a differential drive odometry system, in order to publish sensor_msgs/Imu and nav_msgs/Odometry messages respectively. Lastly, we will spawn sam_bot in a Gazebo environment and verify the published sensor_msgs/Imu and nav_msgs/Odometry messages over ROS 2.

Setup and Prerequisites

Gazebo is a 3D simulator that allows us to observe how our virtual robot will function in a simulated environment. To start using Gazebo with ROS 2, follow the installation instructions in the Gazebo Installation Documentation.

We also need to install the gazebo_ros_pkgs package to simulate odometry and control the robot with ROS 2 in Gazebo:

sudo apt install ros-<ros2-distro>-gazebo-ros-pkgs

You can test if you have successfully set up your ROS 2 and Gazebo environments by following the instructions given here.

Note that we described sam_bot using URDF. However, Gazebo uses Simulation Description Format (SDF) to describe a robot in its simulated environment. Fortunately, Gazebo automatically translates compatible URDF files into SDF. The main requirement for the URDF to be compatible with Gazebo is to have an <inertia> element within each <link> element. This requirement is already satisfied in the URDF file of sam_bot, so it can already be used in Gazebo.

See also

For more information on how to use URDF in Gazebo, see Tutorial: Using a URDF in Gazebo.

Adding Gazebo Plugins to a URDF

We will now add the IMU sensor and the differential drive plugins of Gazebo to our URDF. For an overview of the different plugins available in Gazebo, have a look at Tutorial: Using Gazebo plugins with ROS.

For our robot, we will be using the GazeboRosImuSensor which is a SensorPlugin. A SensorPlugin must be attached to a link, thus we will create an imu_link to which the IMU sensor will be attached. This link will be referenced under the <gazebo> element. Next, we will set /demo/imu as the topic to which the IMU will be publishing its information, and we will comply with REP145 by setting initalOrientationAsReference to false. We will also add some noise to the sensor configuration using Gazebo’s sensor noise model.

Now, we will set up our IMU sensor plugin according to the description above by adding the following lines before the </robot> line in our URDF:

132<link name="imu_link">
133  <visual>
134    <geometry>
135      <box size="0.1 0.1 0.1"/>
136    </geometry>
137  </visual>
138
139  <collision>
140    <geometry>
141      <box size="0.1 0.1 0.1"/>
142    </geometry>
143  </collision>
144
145  <xacro:box_inertia m="0.1" w="0.1" d="0.1" h="0.1"/>
146</link>
147
148<joint name="imu_joint" type="fixed">
149  <parent link="base_link"/>
150  <child link="imu_link"/>
151  <origin xyz="0 0 0.01"/>
152</joint>
153
154 <gazebo reference="imu_link">
155  <sensor name="imu_sensor" type="imu">
156   <plugin filename="libgazebo_ros_imu_sensor.so" name="imu_plugin">
157      <ros>
158        <namespace>/demo</namespace>
159        <remapping>~/out:=imu</remapping>
160      </ros>
161      <initial_orientation_as_reference>false</initial_orientation_as_reference>
162    </plugin>
163    <always_on>true</always_on>
164    <update_rate>100</update_rate>
165    <visualize>true</visualize>
166    <imu>
167      <angular_velocity>
168        <x>
169          <noise type="gaussian">
170            <mean>0.0</mean>
171            <stddev>2e-4</stddev>
172            <bias_mean>0.0000075</bias_mean>
173            <bias_stddev>0.0000008</bias_stddev>
174          </noise>
175        </x>
176        <y>
177          <noise type="gaussian">
178            <mean>0.0</mean>
179            <stddev>2e-4</stddev>
180            <bias_mean>0.0000075</bias_mean>
181            <bias_stddev>0.0000008</bias_stddev>
182          </noise>
183        </y>
184        <z>
185          <noise type="gaussian">
186            <mean>0.0</mean>
187            <stddev>2e-4</stddev>
188            <bias_mean>0.0000075</bias_mean>
189            <bias_stddev>0.0000008</bias_stddev>
190          </noise>
191        </z>
192      </angular_velocity>
193      <linear_acceleration>
194        <x>
195          <noise type="gaussian">
196            <mean>0.0</mean>
197            <stddev>1.7e-2</stddev>
198            <bias_mean>0.1</bias_mean>
199            <bias_stddev>0.001</bias_stddev>
200          </noise>
201        </x>
202        <y>
203          <noise type="gaussian">
204            <mean>0.0</mean>
205            <stddev>1.7e-2</stddev>
206            <bias_mean>0.1</bias_mean>
207            <bias_stddev>0.001</bias_stddev>
208          </noise>
209        </y>
210        <z>
211          <noise type="gaussian">
212            <mean>0.0</mean>
213            <stddev>1.7e-2</stddev>
214            <bias_mean>0.1</bias_mean>
215            <bias_stddev>0.001</bias_stddev>
216          </noise>
217        </z>
218      </linear_acceleration>
219    </imu>
220  </sensor>
221</gazebo>

Now, let us add the differential drive ModelPlugin. We will configure the plugin such that nav_msgs/Odometry messages are published on the /demo/odom topic. The joints of the left and right wheels will be set to the wheel joints of sam_bot. The wheel separation and wheel diameter are set according to the values of the defined values of wheel_ygap and wheel_radius respectively.

To include this plugin in our URDF, add the following lines after the </gazebo> tag of the IMU plugin:

223<gazebo>
224  <plugin name='diff_drive' filename='libgazebo_ros_diff_drive.so'>
225    <ros>
226      <namespace>/demo</namespace>
227    </ros>
228
229    <!-- wheels -->
230    <left_joint>drivewhl_l_joint</left_joint>
231    <right_joint>drivewhl_r_joint</right_joint>
232
233    <!-- kinematics -->
234    <wheel_separation>0.4</wheel_separation>
235    <wheel_diameter>0.2</wheel_diameter>
236
237    <!-- limits -->
238    <max_wheel_torque>20</max_wheel_torque>
239    <max_wheel_acceleration>1.0</max_wheel_acceleration>
240
241    <!-- output -->
242    <publish_odom>true</publish_odom>
243    <publish_odom_tf>false</publish_odom_tf>
244    <publish_wheel_tf>true</publish_wheel_tf>
245
246    <odometry_frame>odom</odometry_frame>
247    <robot_base_frame>base_link</robot_base_frame>
248  </plugin>
249</gazebo>

Launch and Build Files

We will now edit our launch file, launch/display.launch.py, to spawn sam_bot in Gazebo. Since we will be simulating our robot, we can remove the GUI for the joint state publisher by deleting the following lines inside the generate_launch_description():

joint_state_publisher_gui_node = launch_ros.actions.Node(
  package='joint_state_publisher_gui',
  executable='joint_state_publisher_gui',
  name='joint_state_publisher_gui',
  condition=launch.conditions.IfCondition(LaunchConfiguration('gui'))
)

Remove the following gui param:

DeclareLaunchArgument(name='gui', default_value='True',
                      description='Flag to enable joint_state_publisher_gui')

Remove the condition and parameters. Add arguments to the joint_state_publisher_node:

joint_state_publisher_node = launch_ros.actions.Node(
  package='joint_state_publisher',
  executable='joint_state_publisher',
  name='joint_state_publisher',
  arguments=[default_model_path], #Add this line
  parameters=[{'robot_description': Command(['xarcro ', default_model_path])}], #Remove this line
  condition=launch.conditions.UnlessCondition(LaunchConfiguration('gui')) # Remove this line
)

Next, open package.xml and delete the line:

<exec_depend>joint_state_publisher_gui</exec_depend>

To launch Gazebo, add the following before the joint_state_publisher_node, line in display.launch.py

launch.actions.ExecuteProcess(cmd=['gazebo', '--verbose', '-s', 'libgazebo_ros_init.so', '-s', 'libgazebo_ros_factory.so'], output='screen'),

We will now add a node that spawns sam_bot in Gazebo. Open launch/display.launch.py again and paste the following lines before the return launch.LaunchDescription([ line.

spawn_entity = launch_ros.actions.Node(
  package='gazebo_ros',
  executable='spawn_entity.py',
  arguments=['-entity', 'sam_bot', '-topic', 'robot_description'],
  output='screen'
)

Then add the line spawn_entity, before the rviz_node line, as shown below.

      robot_state_publisher_node,
      spawn_entity,
      rviz_node
])

Build, Run and Verification

Let us run our package to check if the /demo/imu and /demo/odom topics are active in the system.

Navigate to the root of the project and execute the following lines:

colcon build
. install/setup.bash
ros2 launch sam_bot_description display.launch.py

Gazebo should launch and you should see a 3D model of sam_bot:

../../_images/gazebo_sam_bot.png

To see the active topics in the system, open a new terminal and execute:

ros2 topic list

You should see /demo/imu and /demo/odom in the list of topics.

To see more information about the topics, execute:

ros2 topic info /demo/imu
ros2 topic info /demo/odom

You should see an output similar to below:

Type: sensor_msgs/msg/Imu
Publisher count: 1
Subscription count: 0
Type: nav_msgs/msg/Odometry
Publisher count: 1
Subscription count: 0

Observe that the /demo/imu topic publishes sensor_msgs/Imu type messages while the /demo/odom topic publishes nav_msgs/Odometry type messages. The information being published on these topics come from the gazebo simulation of the IMU sensor and the differential drive respectively. Also note that both topics currently have no subscribers. In the next section, we will create a robot_localization node that will subscribe to these two topics. It will then use the messages published on both topics to provide a fused, locally accurate and smooth odometry information for Nav2.

Conclusion

In this guide, we have discussed the messages and transforms that are expected by Nav2 from the odometry system. We have seen how to set up an odometry system and how to verify the published messages.