Interface nav2 with custom ROS2 nodes#
Added in version Jazzy: This section.
Warning
This topic is under construction and this might not even be its final form. Please feel free to open an issue if you spot any typos or other problems.
Objective#
We are going to create custom code to interact with the navigation stack, nav2. This will be based on the demo
tb3_simulation_launch.py, but it is generally applicable. In the example, rviz2 is used for interactivity.
In this example, we will be able to operate nav2 automatically, without relying on the visualizer.
Create a publisher to send the initial pose.
Create an action client to send navigation goals.
Important
Differently from everything else used in this tutorial, this tb3_simulation_launch.py demo does not always finishes
cleanly on my machines. It has hanged sometimes, sometimes Gazebo does not show, and sometimes it does not shutdown properly.
It is highly recommended to use a docker container, such as UoMMScRobotics/sfr_gazebo_nav2, to minimise disappointment.
When dealing with a new stack you might not be familiar with, the first step is to try to make sense of the interfaces available.
Initial pose topic#
When the tb3_simulation_launch.py example is running, if we run ros2 topic list you will see a long
list of topics. Let us filter them out with the keyword pose, giving that we’re trying to find things related to
pose. We can do so with the command below.
ros2 topic list | grep pose
The output of that will be the following topics.
/amcl_pose
/detected_dock_pose
/dock_pose
/filtered_dock_pose
/goal_pose
/initialpose
/staging_pose
This is convenient for us because /initialpose describes exactly what we want to do in the first step. If you are
annoyed by the fact that the words initial and pose are not separated by a _, so am I.
We can check if this is in fact the correct topic used by rviz2. With the demo running, we run, in another terminal, the following command.
ros2 topic echo /initialpose
Then, we send the initial position through rviz2. We can see that in fact rviz2 used this topic to set the initial pose, therefore our guess was correct. Below is not representative output.
Output of ros2 topic echo
header:
stamp:
sec: 247
nanosec: 791000000
frame_id: map
pose:
pose:
position:
x: -2.1046903133392334
y: -0.3082020580768585
z: 0.0
orientation:
x: 0.0
y: 0.0
z: 0.0
w: 1.0
covariance:
- 0.25
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.25
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.0
- 0.06853891909122467
---
We can understand more about the topic with the following command.
ros2 topic info /initialpose
This will show us that the topic uses geometry_msgs/msg/PoseWithCovarianceStamped. Thence, all we have to do
is create a subscriber with that message type to the topic /initialpose.
Type: geometry_msgs/msg/PoseWithCovarianceStamped
Publisher count: 1
Subscription count: 1
Although it might look somewhat trivial at this stage, there are many ROS2 concepts at play so that these connections can be understood.
Navigate to pose action#
When operating the demo through rviz2, you might have noticed that sending navigation goals behaves as an action. It has a goal, feedback, and a result. With that in mind, we look through the active action, once more filtering for pose. We can do so with the following command.
ros2 action list | grep pose
This results in the following actions.
/compute_path_through_poses
/compute_path_to_pose
/navigate_through_poses
/navigate_to_pose
Good sense should indicate that, given that we want to navigate to a pose, the correct topic in question is
/navigate_to_pose. We can check more information about it with the following command.
ros2 action info /navigate_to_pose
The information clearly states that rviz2 is connected to it, which is a good indicator that we are on the right path.
Action: /navigate_to_pose
Action clients: 5
/rviz
/rviz_navigation_dialog_action_client
/bt_navigator
/waypoint_follower
/docking_server
Action servers: 1
/bt_navigator
With the following command we see what type of action we have to support.
ros2 action type /navigate_to_pose
The output of the command will be the following action type.
nav2_msgs/action/NavigateToPose
Thence, our next step is to see what are the fields that we need to support in our action client. The long way to do so is with the command we are already aware of.
ros2 interface show nav2_msgs/action/NavigateToPose
Output of ros2 interface show
#goal definition
geometry_msgs/PoseStamped pose
std_msgs/Header header
builtin_interfaces/Time stamp
int32 sec
uint32 nanosec
string frame_id
Pose pose
Point position
float64 x
float64 y
float64 z
Quaternion orientation
float64 x 0
float64 y 0
float64 z 0
float64 w 1
string behavior_tree
---
#result definition
# Error codes
# Note: The expected priority order of the errors should match the message order
uint16 NONE=0
uint16 error_code
string error_msg
---
#feedback definition
geometry_msgs/PoseStamped current_pose
std_msgs/Header header
builtin_interfaces/Time stamp
int32 sec
uint32 nanosec
string frame_id
Pose pose
Point position
float64 x
float64 y
float64 z
Quaternion orientation
float64 x 0
float64 y 0
float64 z 0
float64 w 1
builtin_interfaces/Duration navigation_time
int32 sec
uint32 nanosec
builtin_interfaces/Duration estimated_time_remaining
int32 sec
uint32 nanosec
int16 number_of_recoveries
float32 distance_remaining
A more condensed version of the action can be obtained with the following commands.
cd $(ros2 pkg prefix nav2_msgs --share)
cat action/NavigateToPose.action
The output of this command will be the following.
#goal definition
geometry_msgs/PoseStamped pose
string behavior_tree
---
#result definition
# Error codes
# Note: The expected priority order of the errors should match the message order
uint16 NONE=0
uint16 error_code
string error_msg
---
#feedback definition
geometry_msgs/PoseStamped current_pose
builtin_interfaces/Duration navigation_time
builtin_interfaces/Duration estimated_time_remaining
int16 number_of_recoveries
float32 distance_remaining
So what?#
After going through this investigative process looking through topics and actions, we reached a conclusion of what must be done in a more concrete sense.
- Create a publisher to send the initial pose.
The message type is
geometry_msgs/msg/PoseWithCovarianceStamped.
- Create an action client to send navigation goals.
The action type is
nav2_msgs/action/NavigateToPose.
That’s it!
Create the package#
We’ll start by creating a suitable ROS2 package. We already now, from the previous investigation, what package we need to depend on.
cd ~/ros2_tutorial_workspace/src
ros2 pkg create python_package_that_uses_nav2 \
--build-type ament_python \
--dependencies rclpy geometry_msgs nav2_msgs
ros2 pkg create output
going to create a new package
package name: python_package_that_uses_nav2
destination directory: ~/ros2_tutorial_workspace/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['root <murilo.marinho@manchester.ac.uk>']
licenses: ['TODO: License declaration']
build type: ament_python
dependencies: ['rclpy', 'geometry_msgs', 'nav2_msgs']
creating folder ./python_package_that_uses_nav2
creating ./python_package_that_uses_nav2/package.xml
creating source folder
creating folder ./python_package_that_uses_nav2/python_package_that_uses_nav2
creating ./python_package_that_uses_nav2/setup.py
creating ./python_package_that_uses_nav2/setup.cfg
creating folder ./python_package_that_uses_nav2/resource
creating ./python_package_that_uses_nav2/resource/python_package_that_uses_nav2
creating ./python_package_that_uses_nav2/python_package_that_uses_nav2/__init__.py
creating folder ./python_package_that_uses_nav2/test
creating ./python_package_that_uses_nav2/test/test_copyright.py
creating ./python_package_that_uses_nav2/test/test_flake8.py
creating ./python_package_that_uses_nav2/test/test_pep257.py
[WARNING]: Unknown license 'TODO: License declaration'. This has been set in the package.xml, but no LICENSE file has been created.
It is recommended to use one of the ament license identifiers:
Apache-2.0
BSL-1.0
BSD-2.0
BSD-2-Clause
BSD-3-Clause
GPL-3.0-only
LGPL-3.0-only
MIT
MIT-0
Files#
In this section, we will be creating or modifying the following highlighted files.
python_package_that_uses_nav2/
|-- package.xml
|-- python_package_that_uses_nav2
| |-- __init__.py
| |-- nav2_initial_pose_publisher_node.py
| `-- nav2_navigate_to_pose_action_client_node.py
|-- resource
| `-- python_package_that_uses_nav2
|-- setup.cfg
|-- setup.py
`-- test
|-- test_copyright.py
|-- test_flake8.py
`-- test_pep257.py
Sending the initial pose to nav2#
Given that this node is a simple publisher, we can jump straight to it.
nav2_initial_pose_publisher_node.py
1import time
2
3import rclpy
4from rclpy.node import Node
5from geometry_msgs.msg import PoseWithCovarianceStamped
6
7class Nav2InitialPosePublisherNode(Node):
8 """A ROS2 Node that publishes the initial pose for nav2."""
9
10 def __init__(self):
11 super().__init__('nav2_initial_pose_publisher_node')
12
13 self._topic = '/initialpose'
14
15 self.publisher = self.create_publisher(
16 msg_type=PoseWithCovarianceStamped,
17 topic=self._topic,
18 qos_profile=1)
19
20 publisher_count = 0
21 while publisher_count < 2:
22 publisher_count = self.count_publishers(self._topic)
23 print(f"Waiting for publisher to be connected to {self._topic}.")
24 print(f"Publisher count is {publisher_count}.")
25 time.sleep(1)
26
27 def send_initial_pose_with_covariance(self):
28 """Method to create the PoseWithCovarianceStamped."""
29
30 print(f"Publishing pose to topic: {self._topic}.")
31
32 pwcs = PoseWithCovarianceStamped()
33 pwcs.header.stamp = self.get_clock().now().to_msg()
34 pwcs.header.frame_id = 'map'
35
36 pwcs.pose.pose.position.x = -2.1
37 pwcs.pose.pose.position.y = -0.3
38 pwcs.pose.pose.position.z = 0.0
39
40 pwcs.pose.pose.orientation.w = 1.0
41 pwcs.pose.pose.orientation.x = 0.0
42 pwcs.pose.pose.orientation.y = 0.0
43 pwcs.pose.pose.orientation.z = 0.0
44
45 pwcs.pose.covariance[0] = 0.25
46 pwcs.pose.covariance[7] = 0.25
47 pwcs.pose.covariance[35] = 0.06853891909122467
48
49 self.publisher.publish(pwcs)
50
51def main(args=None):
52 """
53 The main function.
54 :param args: Not used directly by the user, but used by ROS2 to configure
55 certain aspects of the Node.
56 """
57 try:
58 rclpy.init(args=args)
59 node = Nav2InitialPosePublisherNode()
60 node.send_initial_pose_with_covariance()
61 rclpy.spin(node)
62 except KeyboardInterrupt:
63 pass
64 except Exception as e:
65 print(e)
66
67if __name__ == '__main__':
68 main()
There might be two minor novelties in this example. The first one is similar to what we did in tf2 when guaranteeing
that the publisher was connected before attempting to publish for the first time. In this case, because in this demo
rviz2 is already connected to that topic, we check that there are two subscribers connected before proceeding
with the initialisation.
publisher_count = 0
while publisher_count < 2:
publisher_count = self.count_publishers(self._topic)
print(f"Waiting for publisher to be connected to {self._topic}.")
print(f"Publisher count is {publisher_count}.")
time.sleep(1)
The second part that might be unfamiliar is the covariance used in the message. The covariance matrix holds the variance in the diagonal and off the diagonal any pair-wise variances between different states. All of this to say that the covariance will be used to say how confident we are in a given field of the pose and if their variation is correlated. In this case, we choose the same, or similar, values that were sent by rviz2. This is enough for our purposes. The covariance is highlighted below.
def send_initial_pose_with_covariance(self):
"""Method to create the PoseWithCovarianceStamped."""
print(f"Publishing pose to topic: {self._topic}.")
pwcs = PoseWithCovarianceStamped()
pwcs.header.stamp = self.get_clock().now().to_msg()
pwcs.header.frame_id = 'map'
pwcs.pose.pose.position.x = -2.1
pwcs.pose.pose.position.y = -0.3
pwcs.pose.pose.position.z = 0.0
pwcs.pose.pose.orientation.w = 1.0
pwcs.pose.pose.orientation.x = 0.0
pwcs.pose.pose.orientation.y = 0.0
pwcs.pose.pose.orientation.z = 0.0
pwcs.pose.covariance[0] = 0.25
pwcs.pose.covariance[7] = 0.25
pwcs.pose.covariance[35] = 0.06853891909122467
self.publisher.publish(pwcs)
It is important not to be distracted by the nested pose. The first one os the PoseWithCovariance, part of the
PoseWithCovarianceStamped. The second one is the Pose part of the PoseWithCovariance.
That’s it. This node is relatively simple: a publisher that publishes a single message. There will be other ways to achieve this. The one shown herein is the most standard one according to what has been shown in the tutorial.
Handling nav2 pose navigation actions#
I’m conflicted whether this one is even easier. I suppose it’s more complex, being an action client. However, it follows the same formula as we did in the action tutorial.
nav2_navigate_to_pose_action_client_node.py
1import rclpy
2from rclpy.action import ActionClient
3from rclpy.action.client import ClientGoalHandle
4from rclpy.node import Node
5from rclpy.task import Future
6
7from geometry_msgs.msg import Pose, PoseStamped
8from nav2_msgs.action import NavigateToPose
9
10class Nav2NavigateToPoseActionClient(Node):
11 """A ROS2 Node with an Action Client for Nav2NavigateToPoseActionClient."""
12
13 def __init__(self):
14 super().__init__('nav2_navigate_to_pose_action_client')
15
16 self.action_client = ActionClient(self, NavigateToPose, '/navigate_to_pose')
17
18 self.send_goal_future = None # This will be used in `send_goal`
19 self.get_result_future = None # This will be used in 'goal_response_callback'
20
21 def send_goal_async(self, desired_pose: Pose, behaviour_tree: str) -> None:
22 goal_msg = NavigateToPose.Goal()
23 goal_msg.pose.header.stamp = self.get_clock().now().to_msg()
24 goal_msg.pose.header.frame_id = 'map'
25 goal_msg.pose.pose = desired_pose
26 goal_msg.behavior_tree = behaviour_tree
27
28 while not self.action_client.wait_for_server(timeout_sec=1.0):
29 self.get_logger().info(f'action {self.action_client} not available, waiting...')
30
31 self.get_logger().info(f'Sending goal: {goal_msg}.')
32
33 self.send_goal_future = self.action_client.send_goal_async(goal_msg, feedback_callback=self.action_feedback_callback)
34 self.send_goal_future.add_done_callback(self.goal_response_callback)
35
36 def goal_response_callback(self, future: Future) -> None:
37 goal: ClientGoalHandle = future.result()
38
39 if not goal.accepted:
40 self.get_logger().info('Goal was rejected by the server.')
41 return
42 self.get_logger().info('Goal was accepted by the server.')
43
44 self.get_result_future = goal.get_result_async()
45 self.get_result_future.add_done_callback(self.action_result_callback)
46
47 def action_result_callback(self, future: Future) -> None:
48 result: NavigateToPose.Result = future.result()
49 self.get_logger().info(f'Final position was: {result.result}.')
50
51 def action_feedback_callback(self, feedback_msg: NavigateToPose.Feedback) -> None:
52 feedback = feedback_msg.feedback
53 self.get_logger().info(f'Received feedback distance: {feedback.distance_remaining}.')
54
55
56def main(args=None):
57 """
58 The main function.
59 :param args: Not used directly by the user, but used by ROS2 to configure certain aspects of the Node.
60 """
61 try:
62 rclpy.init(args=args)
63
64 node = Nav2NavigateToPoseActionClient()
65
66 desired_pose = Pose()
67 desired_pose.position.x = 1.0
68 desired_pose.position.y = -1.0
69 desired_pose.orientation.w = 1.0
70
71 node.send_goal_async(desired_pose, "")
72
73 rclpy.spin(node)
74 except KeyboardInterrupt:
75 pass
76 except Exception as e:
77 print(e)
78
79
80if __name__ == '__main__':
81 main()
We can highlight two possible pinch points. Starting by the main() function, we instantiate the node and create
a desired pose, highlighted below. Indeed, we are going to use a stamped pose. However, it is simpler to grab the
stamp from a node, so we do that in a method. We send an empty behaviour tree, because otherwise would be out of the
scope of this tutorial.
node = Nav2NavigateToPoseActionClient()
desired_pose = Pose()
desired_pose.position.x = 1.0
desired_pose.position.y = -1.0
desired_pose.orientation.w = 1.0
node.send_goal_async(desired_pose, "")
Then, in our implementation we populate the fields of the goal with the pose we just created and the stamp obtained from the node. We receive an empty behaviour tree and that is also added to the goal.
def send_goal_async(self, desired_pose: Pose, behaviour_tree: str) -> None:
goal_msg = NavigateToPose.Goal()
goal_msg.pose.header.stamp = self.get_clock().now().to_msg()
goal_msg.pose.header.frame_id = 'map'
goal_msg.pose.pose = desired_pose
goal_msg.behavior_tree = behaviour_tree
Besides the slightly different action type, the process to make an action client is mostly unchanged.
Adjusting the setup.py#
1from setuptools import find_packages, setup
2
3package_name = 'python_package_that_uses_nav2'
4
5setup(
6 name=package_name,
7 version='0.0.0',
8 packages=find_packages(exclude=['test']),
9 data_files=[
10 ('share/ament_index/resource_index/packages',
11 ['resource/' + package_name]),
12 ('share/' + package_name, ['package.xml']),
13 ],
14 install_requires=['setuptools'],
15 zip_safe=True,
16 maintainer='root',
17 maintainer_email='murilo.marinho@manchester.ac.uk',
18 description='TODO: Package description',
19 license='TODO: License declaration',
20 extras_require={
21 'test': [
22 'pytest',
23 ],
24 },
25 entry_points={
26 'console_scripts': [
27 'nav2_initial_pose_publisher_node = python_package_that_uses_nav2.nav2_initial_pose_publisher_node:main',
28 'nav2_navigate_to_pose_action_client_node = python_package_that_uses_nav2.nav2_navigate_to_pose_action_client_node:main'
29 ],
30 },
31)
Build and source#
Before we proceed, let us build and source once.
cd ~/ros2_tutorial_workspace
colcon build
source install/setup.bash
Note
For additional explanation and troubleshooting tips, see Always source after you build.
Warning
colcon will not work properly if your terminal has an active venv.
Testing#
The first step is to run the same demo as before, with tb3_simulation_launch.py. We can do that with the command
below.
ros2 launch nav2_bringup \
tb3_simulation_launch.py \
use_sim_time:=True \
headless:=False \
sigterm_timeout:=120
Then, in another terminal, we can run the following command. This one will be rather tolerant of what it’s started because it will wait for the correct number of publishers before publishing.
ros2 run python_package_that_uses_nav2 nav2_initial_pose_publisher_node
The following command should only be attempted after the initial pose was set. The navigation stack will not accept goals unless an initial estimate is given first.
ros2 run python_package_that_uses_nav2 nav2_navigate_to_pose_action_client_node
The qualitative behaviour can be seen in the following video.
Exercises
Other possible interactions with these interfaces could be as follows.
What would change if the same node was supposed to both send the initial pose and, after that is done, send the goal?
What would have to be modified if you had three goals, goal
a, goalb, and goalc. Suppose that we initially send goala. If the goal is reached, then go to goalb. If not reached, go to goalc. This is a very basic state machine. An embryo, but somewhat the motivation for behaviour tress.